From d4c9d93d93c1e0e8ee9bcb06e53bfa83fc55225d Mon Sep 17 00:00:00 2001
From: Teczer
Date: Mon, 24 Nov 2025 17:11:49 +0100
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(validation):=20add=20username?=
=?UTF-8?q?=20length=20validation=20and=20refactor=20lobby=20(v2.3.2)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 108 ++++++++++++++-
README.md | 2 +-
VERSIONS.md | 51 +++++++
apps/api/libs/shared/src/constants/index.ts | 1 +
.../api/libs/shared/src/constants/username.ts | 2 +
apps/api/src/game/dto/join-room.dto.ts | 9 +-
apps/web/app/lobby/page.tsx | 124 ++----------------
apps/web/app/settings/page.tsx | 69 ++++++++--
.../create-lobby-form.component.tsx | 68 ++++++++++
apps/web/features/lobby/components/index.ts | 4 +
.../components/join-lobby-form.component.tsx | 62 +++++++++
.../components/lobby-divider.component.tsx | 10 ++
.../username-input-modal.component.tsx | 69 +++++++---
...username-validation-feedback.component.tsx | 58 ++++++++
package.json | 2 +-
15 files changed, 488 insertions(+), 151 deletions(-)
create mode 100644 apps/api/libs/shared/src/constants/username.ts
create mode 100644 apps/web/features/lobby/components/create-lobby-form.component.tsx
create mode 100644 apps/web/features/lobby/components/index.ts
create mode 100644 apps/web/features/lobby/components/join-lobby-form.component.tsx
create mode 100644 apps/web/features/lobby/components/lobby-divider.component.tsx
create mode 100644 apps/web/features/settings/components/username-validation-feedback.component.tsx
diff --git a/AGENTS.md b/AGENTS.md
index c7a1a1d..2b0bac4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1195,12 +1195,118 @@ None / [Description si applicable]
- [ ] Mettre à jour `package.json` (version)
- [ ] Mettre à jour `README.md` (badge version)
- [ ] Ajouter une entrée dans `VERSIONS.md`
+- [ ] Créer le fichier de release dans `release-notes/vX.X.X.md`
- [ ] Mettre à jour footer de `AGENTS.md` (date + version + status)
- [ ] Tester en local (dev + build)
- [ ] Tester en Docker (si applicable)
---
+## 📦 GitHub Release Notes Guidelines
+
+### Format Standardisé
+
+Chaque release doit avoir un fichier `release-notes/vX.X.X.md` suivant ce format :
+
+**Titre de Release** : `X.X.X (Month DD, YYYY)`
+
+**Titre de Description** : `[Emoji] LolTimeFlash vX.X.X - [Feature Name]`
+
+**Structure** :
+
+```markdown
+X.X.X (Month DD, YYYY)
+
+[Emoji] LolTimeFlash vX.X.X - [Feature Name]
+
+### 🔄 Overview
+
+[1-2 phrases résumant la release]
+
+### ✨ What's New (ou 🐛 Bug Fixes / ♻️ Refactoring selon le type)
+
+- **Feature/Fix Name**: Description
+- **Feature/Fix Name**: Description
+
+### 📝 Documentation (si applicable)
+
+- Doc change 1
+- Doc change 2
+
+### 📦 Technical Changes
+
+**Backend** (si applicable):
+
+- Change 1
+- Change 2
+
+**Frontend** (si applicable):
+
+- Change 1
+- Change 2
+
+**Modified Files**:
+| File | Changes |
+|------|---------|
+| `path/to/file` | Description |
+
+### 🎯 Impact
+
+[Description de l'impact utilisateur/développeur]
+
+---
+
+**Full Changelog**: https://github.com/yourusername/LolTimeFlash/compare/vX.X.X...vY.Y.Y
+```
+
+### Exemples par Type de Release
+
+**MAJOR (X.0.0)** : Breaking changes, architecture majeure
+
+- Emoji titre: 🚀 ou 📦
+- Sections: Overview, Breaking Changes, New Features, Migration Guide, Technical Changes, Impact
+- Exemple: `🚀 LolTimeFlash v2.0.0 - NestJS Monorepo Architecture`
+
+**MINOR (X.Y.0)** : Nouvelles features, améliorations
+
+- Emoji titre: ✨
+- Sections: Overview, What's New, Documentation, Technical Changes, Impact
+- Exemple: `✨ LolTimeFlash v2.3.0 - Timer Calibration Controls & UX Polish`
+
+**PATCH (X.Y.Z)** : Bug fixes, hotfixes, optimizations
+
+- Emoji titre: 🐛 (fixes) ou ⚙️ (refactor) ou 📝 (docs)
+- Sections: Overview, Bug Fixes/Refactoring, Documentation, Technical Changes, Impact
+- Exemple: `⚙️ LolTimeFlash v2.3.1 - Username Storage Refactor`
+
+### Publication sur GitHub
+
+1. Créer le tag Git :
+
+```bash
+git tag -a vX.X.X -m "Release vX.X.X"
+git push origin vX.X.X
+```
+
+2. Créer la release sur GitHub :
+ - Aller sur Releases → Draft a new release
+ - Choisir le tag `vX.X.X`
+ - Titre : `X.X.X (Month DD, YYYY)`
+ - Description : Copier le contenu de `release-notes/vX.X.X.md`
+ - Publier
+
+### Fichiers à Mettre à Jour
+
+Pour chaque release :
+
+1. `package.json` - Version number
+2. `README.md` - Badge version
+3. `VERSIONS.md` - Entrée historique détaillée
+4. `release-notes/vX.X.X.md` - Notes de release GitHub
+5. `AGENTS.md` - Footer (date + version + status)
+
+---
+
## 📖 Additional Resources
### External Documentation
@@ -1697,5 +1803,5 @@ For questions, issues, or contributions:
---
**Last Updated**: November 24, 2025
-**Version**: 2.3.1 - Username Storage Refactor
+**Version**: 2.3.2 - Username Validation & Lobby Refactor
**Status**: ✅ Production Ready (API + Web + Docker + Timer Sync + Calibration)
diff --git a/README.md b/README.md
index 4957154..8f5311a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
**League of Legends Website Tool: Easily Time and Communicate Summoner Spells - THE FLASH! 🌟**
-[](https://github.com/yourusername/LolTimeFlash/releases)
+[](https://github.com/yourusername/LolTimeFlash/releases)
[](https://nextjs.org/)
[](https://www.typescriptlang.org/)
[](https://socket.io/)
diff --git a/VERSIONS.md b/VERSIONS.md
index a9c805a..124f60d 100644
--- a/VERSIONS.md
+++ b/VERSIONS.md
@@ -7,6 +7,57 @@
## 📚 Version History
+### Version 2.3.2 - November 2025 (Username Validation & Lobby Refactor)
+
+**New Features** :
+
+- ✨ **Username Length Validation** : Validation stricte 3-12 caractères (backend + frontend)
+- 🎨 **Visual Validation Feedback** : Indicateurs check/croix en temps réel avec feedback couleur
+- 🏗️ **Lobby Architecture Refactor** : Réécriture complète en composants atomiques mémorisés
+- ⚡ **Performance Improvements** : Mémorisation des composants réduit les re-renders
+
+**Security** :
+
+- 🔐 **Backend Validation** : Protection contre la manipulation localStorage (username > 12 chars rejeté)
+
+**Technical Changes** :
+
+**Backend** :
+- Nouveau fichier : `apps/api/libs/shared/src/constants/username.ts` (MIN/MAX constants)
+- Mise à jour : `JoinRoomDto` validation (3-12 caractères avec constantes locales)
+
+**Frontend** :
+- Nouveau composant : `UsernameValidationFeedback.component.tsx` (feedback réutilisable)
+- Nouveaux composants lobby : `CreateLobbyForm`, `JoinLobbyForm`, `LobbyDivider`
+- Refactoring : `app/lobby/page.tsx` (134 → 28 lignes, -78%)
+- Architecture : Pattern `features/` avec barrel exports
+- Mémorisation : `React.memo()` sur tous les nouveaux composants
+
+**Fichiers Modifiés** :
+
+| Fichier | Changements |
+| ---------------------------------------------------------- | -------------------------------------------- |
+| `apps/api/libs/shared/src/constants/username.ts` | **Created** - Constantes longueur username |
+| `apps/api/src/game/dto/join-room.dto.ts` | Validation 3-12 caractères |
+| `apps/web/features/settings/components/username-validation-feedback.component.tsx` | **Created** - Composant feedback |
+| `apps/web/features/lobby/components/create-lobby-form.component.tsx` | **Created** - Form création |
+| `apps/web/features/lobby/components/join-lobby-form.component.tsx` | **Created** - Form join |
+| `apps/web/features/lobby/components/lobby-divider.component.tsx` | **Created** - Divider mémorisé |
+| `apps/web/features/lobby/components/index.ts` | **Created** - Barrel exports |
+| `apps/web/app/lobby/page.tsx` | **Refactored** - Réduction de 78% |
+| `features/settings/components/username-input-modal.component.tsx` | Intégration feedback validation |
+| `app/settings/page.tsx` | Intégration feedback validation |
+
+**Impact** :
+
+- ✅ Sécurité renforcée (validation backend)
+- ✅ UX améliorée (feedback visuel instantané)
+- ✅ Architecture propre (composants atomiques)
+- ✅ Performance optimisée (mémorisation)
+- ✅ Maintenabilité accrue (code -78% sur lobby)
+
+---
+
### Version 2.3.1 - November 2025 (Username Storage Refactor)
**Refactoring & Optimization** :
diff --git a/apps/api/libs/shared/src/constants/index.ts b/apps/api/libs/shared/src/constants/index.ts
index 7834883..eff3174 100644
--- a/apps/api/libs/shared/src/constants/index.ts
+++ b/apps/api/libs/shared/src/constants/index.ts
@@ -1,2 +1,3 @@
export * from './cooldowns';
export * from './roles';
+export * from './username';
diff --git a/apps/api/libs/shared/src/constants/username.ts b/apps/api/libs/shared/src/constants/username.ts
new file mode 100644
index 0000000..64795ea
--- /dev/null
+++ b/apps/api/libs/shared/src/constants/username.ts
@@ -0,0 +1,2 @@
+export const MAX_USERNAME_LENGTH: number = 12;
+export const MIN_USERNAME_LENGTH: number = 3;
diff --git a/apps/api/src/game/dto/join-room.dto.ts b/apps/api/src/game/dto/join-room.dto.ts
index 7f8e7b7..20f9841 100644
--- a/apps/api/src/game/dto/join-room.dto.ts
+++ b/apps/api/src/game/dto/join-room.dto.ts
@@ -1,4 +1,7 @@
-import { IsString, IsNotEmpty, Length, Matches } from 'class-validator';
+import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
+
+const MIN_USERNAME_LENGTH = 3;
+const MAX_USERNAME_LENGTH = 12;
export class JoinRoomDto {
@IsString()
@@ -11,6 +14,8 @@ export class JoinRoomDto {
@IsString()
@IsNotEmpty()
- @Length(3, 20, { message: 'Username must be between 3 and 20 characters' })
+ @Length(MIN_USERNAME_LENGTH, MAX_USERNAME_LENGTH, {
+ message: `Username must be between ${MIN_USERNAME_LENGTH} and ${MAX_USERNAME_LENGTH} characters`,
+ })
username: string;
}
diff --git a/apps/web/app/lobby/page.tsx b/apps/web/app/lobby/page.tsx
index d6f8e5b..92ecf3b 100644
--- a/apps/web/app/lobby/page.tsx
+++ b/apps/web/app/lobby/page.tsx
@@ -1,32 +1,17 @@
'use client'
-import { useState } from 'react'
-
-import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { useToast } from '@/hooks/use-toast.hook'
+import {
+ CreateLobbyForm,
+ JoinLobbyForm,
+ LobbyDivider,
+} from '@/features/lobby/components'
import { RxTrackPrevious } from 'react-icons/rx'
-import { GrFormNextLink } from 'react-icons/gr'
-import { FaCopy } from 'react-icons/fa'
-import { cn, generateLobbyCodeId } from '@/lib/utils'
-
-export default function Home() {
- const router = useRouter()
- const { toast } = useToast()
-
- const [joinLobbyCode, setJoinLobbyCode] = useState('')
- const [lobbyCode, setLobbyCode] = useState('')
- const [isError, setIsError] = useState(false)
-
- function createLobbyCode() {
- const code: string = generateLobbyCodeId(10)
- setLobbyCode(code)
- }
+export default function LobbyPage() {
return (
@@ -34,100 +19,9 @@ export default function Home() {
- {/* CREATE LOBBY */}
-
-
Create a Lobby
- {!lobbyCode && (
-
- )}
- {lobbyCode && (
-
-
Your lobby code is :
-
-
-
-
-
-
- )}
-
- {/* BORDER */}
-
- {/* JOIN LOBBY */}
-
-
Join a Lobby
-
- {isError && (
-
- The lobby code must be 10 characters, {joinLobbyCode.length} actual.
-
- )}
-
+
+
+
)
}
diff --git a/apps/web/app/settings/page.tsx b/apps/web/app/settings/page.tsx
index 571f803..b557bab 100644
--- a/apps/web/app/settings/page.tsx
+++ b/apps/web/app/settings/page.tsx
@@ -6,12 +6,16 @@ import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
+import { UsernameValidationFeedback } from '@/features/settings/components/username-validation-feedback.component'
+import { cn } from '@/lib/utils'
+import { MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH } from '@loltimeflash/shared'
import { GrFormNextLink } from 'react-icons/gr'
import { RxTrackPrevious } from 'react-icons/rx'
export default function Home() {
const [inputValue, setInputValue] = useState('')
+ const [error, setError] = useState('')
const [username, setUsername] = useState(
typeof window !== 'undefined' ? localStorage.getItem('username') : null
@@ -19,8 +23,34 @@ export default function Home() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
- localStorage.setItem('username', inputValue)
- setUsername(inputValue)
+
+ const trimmedValue = inputValue.trim()
+
+ if (!trimmedValue) {
+ setError('Username cannot be empty')
+ return
+ }
+
+ if (trimmedValue.length < MIN_USERNAME_LENGTH) {
+ setError(`Username must be at least ${MIN_USERNAME_LENGTH} characters`)
+ return
+ }
+
+ if (trimmedValue.length > MAX_USERNAME_LENGTH) {
+ setError(`Username must be at most ${MAX_USERNAME_LENGTH} characters`)
+ return
+ }
+
+ localStorage.setItem('username', trimmedValue)
+ setUsername(trimmedValue)
+ setInputValue('')
+ setError('')
+ }
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const value = e.target.value
+ setInputValue(value)
+ setError('')
}
return (
@@ -32,7 +62,7 @@ export default function Home() {
)}
-
-
setInputValue(e.target.value)}
- />
+
+
+
+
+
+
-
+ {error &&
{error}
}
+
+
0,
+ })}
+ />
diff --git a/apps/web/features/lobby/components/create-lobby-form.component.tsx b/apps/web/features/lobby/components/create-lobby-form.component.tsx
new file mode 100644
index 0000000..05c1074
--- /dev/null
+++ b/apps/web/features/lobby/components/create-lobby-form.component.tsx
@@ -0,0 +1,68 @@
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { useToast } from '@/hooks/use-toast.hook'
+import { generateLobbyCodeId } from '@/lib/utils'
+import { useRouter } from 'next/navigation'
+import { memo, useState } from 'react'
+import { FaCopy } from 'react-icons/fa'
+import { GrFormNextLink } from 'react-icons/gr'
+
+const CreateLobbyFormComponent = () => {
+ const router = useRouter()
+ const { toast } = useToast()
+ const [lobbyCode, setLobbyCode] = useState('')
+
+ const handleCreateLobby = () => {
+ const code: string = generateLobbyCodeId(10)
+ setLobbyCode(code)
+ }
+
+ const handleCopyCode = () => {
+ if (lobbyCode) {
+ navigator.clipboard.writeText(lobbyCode)
+ toast({
+ title: 'Your lobby code has been copied to your clipboard!',
+ })
+ }
+ }
+
+ const handleJoinLobby = () => {
+ router.push(`/game/${lobbyCode}`)
+ }
+
+ return (
+
+
Create a Lobby
+
+ {!lobbyCode && (
+
+ )}
+
+ {lobbyCode && (
+
+
Your lobby code is :
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export const CreateLobbyForm = memo(CreateLobbyFormComponent)
+
diff --git a/apps/web/features/lobby/components/index.ts b/apps/web/features/lobby/components/index.ts
new file mode 100644
index 0000000..7a0886e
--- /dev/null
+++ b/apps/web/features/lobby/components/index.ts
@@ -0,0 +1,4 @@
+export { CreateLobbyForm } from './create-lobby-form.component'
+export { JoinLobbyForm } from './join-lobby-form.component'
+export { LobbyDivider } from './lobby-divider.component'
+
diff --git a/apps/web/features/lobby/components/join-lobby-form.component.tsx b/apps/web/features/lobby/components/join-lobby-form.component.tsx
new file mode 100644
index 0000000..c2f9b3c
--- /dev/null
+++ b/apps/web/features/lobby/components/join-lobby-form.component.tsx
@@ -0,0 +1,62 @@
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { cn } from '@/lib/utils'
+import { useRouter } from 'next/navigation'
+import { FormEvent, memo, useState } from 'react'
+import { GrFormNextLink } from 'react-icons/gr'
+
+const LOBBY_CODE_LENGTH = 10
+
+const JoinLobbyFormComponent = () => {
+ const router = useRouter()
+ const [joinLobbyCode, setJoinLobbyCode] = useState('')
+ const [isError, setIsError] = useState(false)
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault()
+ if (joinLobbyCode.length === LOBBY_CODE_LENGTH) {
+ router.push(`/game/${joinLobbyCode}`)
+ } else {
+ setIsError(true)
+ }
+ }
+
+ const handleChange = (value: string) => {
+ setJoinLobbyCode(value)
+ }
+
+ return (
+
+
Join a Lobby
+
+
+
+ {isError && (
+
+ The lobby code must be {LOBBY_CODE_LENGTH} characters,{' '}
+ {joinLobbyCode.length} actual.
+
+ )}
+
+ )
+}
+
+export const JoinLobbyForm = memo(JoinLobbyFormComponent)
diff --git a/apps/web/features/lobby/components/lobby-divider.component.tsx b/apps/web/features/lobby/components/lobby-divider.component.tsx
new file mode 100644
index 0000000..a859435
--- /dev/null
+++ b/apps/web/features/lobby/components/lobby-divider.component.tsx
@@ -0,0 +1,10 @@
+import { memo } from 'react'
+
+const LobbyDividerComponent = () => {
+ return (
+
+ )
+}
+
+export const LobbyDivider = memo(LobbyDividerComponent)
+
diff --git a/apps/web/features/settings/components/username-input-modal.component.tsx b/apps/web/features/settings/components/username-input-modal.component.tsx
index 9e2640d..a49ec13 100644
--- a/apps/web/features/settings/components/username-input-modal.component.tsx
+++ b/apps/web/features/settings/components/username-input-modal.component.tsx
@@ -2,21 +2,46 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
+import { cn } from '@/lib/utils'
+import { MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH } from '@loltimeflash/shared'
import { useParams } from 'next/navigation'
import { useState } from 'react'
import { GrFormNextLink } from 'react-icons/gr'
+import { UsernameValidationFeedback } from './username-validation-feedback.component'
export const UsernameInputModal = () => {
const [inputValue, setInputValue] = useState('')
+ const [error, setError] = useState('')
const params = useParams()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
-
- if (inputValue.trim()) {
- localStorage.setItem('username', inputValue.trim())
- location.reload()
+
+ const trimmedValue = inputValue.trim()
+
+ if (!trimmedValue) {
+ setError('Username cannot be empty')
+ return
+ }
+
+ if (trimmedValue.length < MIN_USERNAME_LENGTH) {
+ setError(`Username must be at least ${MIN_USERNAME_LENGTH} characters`)
+ return
}
+
+ if (trimmedValue.length > MAX_USERNAME_LENGTH) {
+ setError(`Username must be at most ${MAX_USERNAME_LENGTH} characters`)
+ return
+ }
+
+ localStorage.setItem('username', trimmedValue)
+ location.reload()
+ }
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const value = e.target.value
+ setInputValue(value)
+ setError('')
}
return (
@@ -29,22 +54,32 @@ export const UsernameInputModal = () => {
You are joining lobby{' '}
{params.roomId}
-
-
-
setInputValue(e.target.value)}
- />
-
+
+
+
+
+
+
+
+ {error &&
{error}
}
+
+
0,
+ })}
+ />
)
}
-
diff --git a/apps/web/features/settings/components/username-validation-feedback.component.tsx b/apps/web/features/settings/components/username-validation-feedback.component.tsx
new file mode 100644
index 0000000..9ebed3d
--- /dev/null
+++ b/apps/web/features/settings/components/username-validation-feedback.component.tsx
@@ -0,0 +1,58 @@
+import { cn } from '@/lib/utils'
+import { MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH } from '@loltimeflash/shared'
+import { memo } from 'react'
+import { FaCheck, FaTimes } from 'react-icons/fa'
+
+interface IUsernameValidationFeedbackProps {
+ usernameLength: number
+ className?: string
+}
+
+const UsernameValidationFeedbackComponent = (
+ props: IUsernameValidationFeedbackProps
+) => {
+ const { usernameLength, className } = props
+
+ const isMinLengthValid = usernameLength >= MIN_USERNAME_LENGTH
+ const isMaxLengthValid = usernameLength <= MAX_USERNAME_LENGTH
+
+ return (
+
+
+ {isMinLengthValid ? (
+
+ ) : (
+
+ )}
+
+ Must be at least {MIN_USERNAME_LENGTH} characters
+
+
+
+
+ {isMaxLengthValid ? (
+
+ ) : (
+
+ )}
+
+ Must not exceed {MAX_USERNAME_LENGTH} characters
+
+
+
+ )
+}
+
+export const UsernameValidationFeedback = memo(
+ UsernameValidationFeedbackComponent
+)
diff --git a/package.json b/package.json
index 5d4bfa1..94773fb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "loltimeflash-monorepo",
- "version": "2.3.1",
+ "version": "2.3.2",
"private": true,
"description": "LolTimeFlash - Real-time Flash timer tracking for League of Legends",
"author": "LolTimeFlash Team",