11import { useState , useMemo , useEffect , useRef } from 'react' ;
22import { items } from './constants/items' ;
33
4- const ITEMS_PER_PAGE = 20 ;
4+ const ITEMS_PER_PAGE = 24 ;
55
66export default function App ( ) {
77 const [ search , setSearch ] = useState ( '' ) ;
88 const [ activeCategory , setActiveCategory ] = useState ( 'all' ) ;
99 const [ page , setPage ] = useState ( 1 ) ;
10+ const [ isSidebarOpen , setIsSidebarOpen ] = useState ( false ) ; // État pour ouvrir/fermer
1011 const loaderRef = useRef ( null ) ;
1112
12- // Fonctions de modification (On reset la page ici, pas dans un effect)
1313 const handleSearchChange = ( e ) => {
1414 setSearch ( e . target . value ) ;
1515 setPage ( 1 ) ;
@@ -18,15 +18,14 @@ export default function App() {
1818 const handleCategoryChange = ( cat ) => {
1919 setActiveCategory ( cat ) ;
2020 setPage ( 1 ) ;
21+ setIsSidebarOpen ( false ) ; // Ferme la sidebar sur mobile après sélection
2122 } ;
2223
23- // Extraction des catégories
2424 const categories = useMemo ( ( ) => {
2525 const cats = items . map ( item => item . category ) ;
2626 return [ 'all' , ...new Set ( cats ) ] ;
2727 } , [ ] ) ;
2828
29- // Filtrage des items
3029 const allFilteredItems = useMemo ( ( ) => {
3130 return items . filter ( item => {
3231 const matchesSearch = item . name . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) ||
@@ -36,17 +35,8 @@ export default function App() {
3635 } ) ;
3736 } , [ search , activeCategory ] ) ;
3837
39- // Calcul des items visibles en fonction de la page
4038 const visibleItems = allFilteredItems . slice ( 0 , page * ITEMS_PER_PAGE ) ;
4139
42- // Intersection Observer (Seul effect autorisé pour l'API externe Browser)
43- useEffect ( ( ) => {
44- window . scrollTo ( 0 , 0 ) ;
45- if ( 'scrollRestoration' in window . history ) {
46- window . history . scrollRestoration = 'manual' ;
47- }
48- } , [ ] ) ;
49-
5040 // Scroll infini
5141 useEffect ( ( ) => {
5242 const observer = new IntersectionObserver ( ( entries ) => {
@@ -63,7 +53,7 @@ export default function App() {
6353 navigator . clipboard . writeText ( id ) ;
6454 const notification = document . createElement ( 'div' ) ;
6555 notification . innerText = `Copié: ${ id } ` ;
66- notification . className = "fixed bottom-5 right-5 bg-[#c3a05b] text-black px-4 py-2 rounded-lg shadow-lg z-50 font-bold border border-white/20" ;
56+ notification . className = "fixed bottom-5 right-5 bg-[#c3a05b] text-black px-4 py-2 rounded-lg shadow-lg z-[100] font-bold border border-white/20" ;
6757 document . body . appendChild ( notification ) ;
6858 setTimeout ( ( ) => notification . remove ( ) , 2000 ) ;
6959 } ;
@@ -74,12 +64,36 @@ export default function App() {
7464 ::-webkit-scrollbar { width: 6px; }
7565 ::-webkit-scrollbar-track { background: #0a0a0f; }
7666 ::-webkit-scrollbar-thumb { background: #c3a05b; border-radius: 10px; }
77- * { scrollbar-width: thin; scrollbar-color: #c3a05b #0a0a0f ; }
67+ .sidebar-scroll { overscroll-behavior: contain ; }
7868 ` } } />
7969
80- { /* Sidebar */ }
81- < aside className = "w-64 border-r border-white/5 bg-[#0d0d14]/80 backdrop-blur-md sticky top-0 h-screen hidden md:flex flex-col" >
82- < div className = "p-6 flex-1 overflow-y-auto" >
70+ { /* --- MENU BURGER (Mobile) --- */ }
71+ < button
72+ onClick = { ( ) => setIsSidebarOpen ( ! isSidebarOpen ) }
73+ className = "fixed top-6 left-6 z-[70] md:hidden bg-[#c3a05b] p-3 rounded-xl text-black shadow-lg active:scale-90 transition-transform"
74+ >
75+ { isSidebarOpen ? (
76+ < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" > </ line > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" > </ line > </ svg >
77+ ) : (
78+ < svg width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2.5" > < line x1 = "3" y1 = "12" x2 = "21" y2 = "12" > </ line > < line x1 = "3" y1 = "6" x2 = "21" y2 = "6" > </ line > < line x1 = "3" y1 = "18" x2 = "21" y2 = "18" > </ line > </ svg >
79+ ) }
80+ </ button >
81+
82+ { /* --- OVERLAY (Mobile) --- */ }
83+ { isSidebarOpen && (
84+ < div
85+ className = "fixed inset-0 bg-black/80 backdrop-blur-sm z-[60] md:hidden transition-opacity duration-300"
86+ onClick = { ( ) => setIsSidebarOpen ( false ) }
87+ />
88+ ) }
89+
90+ { /* --- SIDEBAR RESPONSIVE --- */ }
91+ < aside className = { `
92+ fixed md:sticky top-0 left-0 z-[65] w-72 md:w-64 h-screen border-r border-white/5 bg-[#0d0d14]/95 backdrop-blur-xl
93+ transition-transform duration-300 ease-out flex flex-col
94+ ${ isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0' }
95+ ` } >
96+ < div className = "p-6 pt-20 md:pt-6 flex-1 overflow-y-auto sidebar-scroll" >
8397 < h2 className = "text-[#c3a05b] font-black uppercase tracking-widest text-[10px] mb-8 opacity-50" > Navigation</ h2 >
8498 < nav className = "space-y-1" >
8599 { categories . map ( ( cat ) => (
@@ -106,42 +120,51 @@ export default function App() {
106120 </ div >
107121 </ aside >
108122
109- { /* Main Content */ }
110- < main className = "flex-1 p-6 lg :p-10" >
111- < header className = "flex flex-col lg:flex-row lg:items-center justify-between gap-6 mb-12" >
123+ { /* --- MAIN CONTENT --- */ }
124+ < main className = "flex-1 p-4 md :p-10 transition-all duration-300 " >
125+ < header className = "flex flex-col lg:flex-row lg:items-center justify-between gap-6 mb-12 mt-16 md:mt-0 " >
112126 < div >
113- < h1 className = "text-4xl font-black tracking-tighter uppercase italic" >
127+ < h1 className = "text-3xl md:text- 4xl font-black tracking-tighter uppercase italic" >
114128 < span className = "text-[#c3a05b]" > Lunatic</ span > Catalogue
115129 </ h1 >
116130 < p className = "text-gray-500 text-[10px] font-mono uppercase tracking-[0.2em] mt-1" > Items Lunatic RP</ p >
117131 </ div >
118132
119- < input
120- type = "text"
121- placeholder = "Rechercher..."
122- className = "w-full lg:w-96 bg-[#11111a] border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-[#c3a05b]/50 transition-all shadow-2xl"
123- onChange = { handleSearchChange }
124- />
133+ < div className = "relative group" >
134+ < input
135+ type = "text"
136+ placeholder = "Rechercher un item..."
137+ className = "w-full lg:w-96 bg-[#11111a] border border-white/10 rounded-2xl px-6 py-4 outline-none focus:border-[#c3a05b]/50 transition-all shadow-2xl"
138+ onChange = { handleSearchChange }
139+ />
140+ </ div >
125141 </ header >
126142
127- < div className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8 gap-4" >
143+ { /* --- GRID ITEMS --- */ }
144+ < div className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8 gap-3 md:gap-4" >
128145 { visibleItems . map ( ( item ) => (
129146 < div
130147 key = { item . id }
131148 onClick = { ( ) => copyToClipboard ( item . id ) }
132- className = "group relative bg-[#11111a] border border-white/5 rounded-2xl p-4 flex flex-col items-center justify-between aspect-square hover:border-[#c3a05b]/40 hover:bg-[#c3a05b]/[0.02] transition-all duration-300 cursor-pointer"
149+ className = "group relative bg-[#11111a] border border-white/5 rounded-2xl p-3 md:p- 4 flex flex-col items-center justify-between aspect-square hover:border-[#c3a05b]/40 hover:bg-[#c3a05b]/[0.02] transition-all duration-300 cursor-pointer"
133150 >
134- < div className = "flex-1 flex items-center justify-center p-2" >
135- < img src = { item . image . startsWith ( './' ) ? item . image : `./${ item . image } ` } alt = { item . name } className = "max-w-[80%] max-h-[80%] object-contain group-hover:scale-110 transition-transform duration-500" />
151+ < div className = "flex-1 flex items-center justify-center p-2 w-full overflow-hidden" >
152+ < img
153+ src = { item . image . startsWith ( './' ) ? item . image : `./${ item . image } ` }
154+ alt = { item . name }
155+ loading = "lazy"
156+ className = "max-w-[85%] max-h-[85%] object-contain group-hover:scale-110 transition-transform duration-500"
157+ />
136158 </ div >
137159 < div className = "w-full text-center mt-2" >
138- < p className = "text-[10px] font-bold text-gray-400 group-hover:text-[#c3a05b] truncate uppercase" > { item . name } </ p >
139- < p className = "text-[8px] font-mono text-gray-700 mt-0.5" > { item . category } </ p >
160+ < p className = "text-[9px] md:text-[ 10px] font-bold text-gray-400 group-hover:text-[#c3a05b] truncate uppercase" > { item . name } </ p >
161+ < p className = "text-[7px] md:text-[ 8px] font-mono text-gray-700 mt-0.5" > { item . category } </ p >
140162 </ div >
141163 </ div >
142164 ) ) }
143165 </ div >
144166
167+ { /* --- LOADER --- */ }
145168 < div ref = { loaderRef } className = "h-20 w-full flex items-center justify-center mt-10" >
146169 { visibleItems . length < allFilteredItems . length && (
147170 < div className = "w-6 h-6 border-2 border-[#c3a05b]/20 border-b-[#c3a05b] rounded-full animate-spin" />
0 commit comments