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! 🌟** -[![Version](https://img.shields.io/badge/version-2.3.1-brightgreen?style=flat)](https://github.com/yourusername/LolTimeFlash/releases) +[![Version](https://img.shields.io/badge/version-2.3.2-brightgreen?style=flat)](https://github.com/yourusername/LolTimeFlash/releases) [![Next.js](https://img.shields.io/badge/Next.js-16.0.1-black?style=flat&logo=next.js)](https://nextjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7.2-blue?style=flat&logo=typescript)](https://www.typescriptlang.org/) [![Socket.IO](https://img.shields.io/badge/Socket.IO-4.8.1-010101?style=flat&logo=socket.io)](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

-
{ - e.preventDefault() - if (joinLobbyCode.length === 10) { - router.push(`/game/${joinLobbyCode}`) - } else { - setIsError(true) - } - }} - > - setJoinLobbyCode(e.target.value)} - /> - -
- {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() {

Change or set your username @@ -46,18 +76,29 @@ 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

+ +
+ handleChange(e.target.value)} + /> + +
+ + {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",