11'use client' ;
22
3- import { useState , useEffect , useCallback } from 'react' ;
3+ import { useState , useEffect , useCallback , useRef } from 'react' ;
44import { HugeiconsIcon } from '@hugeicons/react' ;
55import {
66 PaintBrush01Icon ,
77 SortingDownIcon ,
88 FilterIcon ,
99 Loading02Icon ,
10+ FavouriteIcon ,
1011} from '@hugeicons/core-free-icons' ;
1112import { cn } from '@/lib/utils' ;
1213import { Button } from '@/components/ui/button' ;
1314import { GalleryGrid , type GalleryItem } from '@/components/gallery/GalleryGrid' ;
1415import { GalleryDetail } from '@/components/gallery/GalleryDetail' ;
15- import { TagManager , useTags , type Tag } from '@/components/gallery/TagManager' ;
1616import { useTranslation } from '@/hooks/useTranslation' ;
1717import type { TranslationKey } from '@/i18n' ;
1818
@@ -22,19 +22,18 @@ type SortOrder = 'newest' | 'oldest';
2222
2323export default function GalleryPage ( ) {
2424 const { t } = useTranslation ( ) ;
25- const { tags, loading : tagsLoading , addTag, removeTag, fetchTags } = useTags ( ) ;
2625
2726 const [ items , setItems ] = useState < GalleryItem [ ] > ( [ ] ) ;
2827 const [ total , setTotal ] = useState ( 0 ) ;
2928 const [ loading , setLoading ] = useState ( true ) ;
3029 const [ offset , setOffset ] = useState ( 0 ) ;
3130
3231 // Filters
33- const [ selectedTags , setSelectedTags ] = useState < string [ ] > ( [ ] ) ;
3432 const [ dateFrom , setDateFrom ] = useState ( '' ) ;
3533 const [ dateTo , setDateTo ] = useState ( '' ) ;
3634 const [ sort , setSort ] = useState < SortOrder > ( 'newest' ) ;
3735 const [ showFilters , setShowFilters ] = useState ( false ) ;
36+ const [ favoritesOnly , setFavoritesOnly ] = useState ( false ) ;
3837
3938 // Detail
4039 const [ selectedItem , setSelectedItem ] = useState < GalleryItem | null > ( null ) ;
@@ -44,9 +43,9 @@ export default function GalleryPage() {
4443 setLoading ( true ) ;
4544 try {
4645 const params = new URLSearchParams ( ) ;
47- if ( selectedTags . length > 0 ) params . set ( 'tags' , selectedTags . join ( ',' ) ) ;
4846 if ( dateFrom ) params . set ( 'dateFrom' , dateFrom ) ;
4947 if ( dateTo ) params . set ( 'dateTo' , dateTo ) ;
48+ if ( favoritesOnly ) params . set ( 'favoritesOnly' , '1' ) ;
5049 params . set ( 'sort' , sort ) ;
5150 params . set ( 'limit' , String ( PAGE_SIZE ) ) ;
5251 params . set ( 'offset' , reset ? '0' : String ( offset ) ) ;
@@ -68,22 +67,14 @@ export default function GalleryPage() {
6867 } finally {
6968 setLoading ( false ) ;
7069 }
71- } , [ selectedTags , dateFrom , dateTo , sort , offset ] ) ;
70+ } , [ dateFrom , dateTo , sort , offset , favoritesOnly ] ) ;
7271
7372 // Initial load and reload on filter changes
7473 useEffect ( ( ) => {
7574 setOffset ( 0 ) ;
7675 fetchItems ( true ) ;
7776 // eslint-disable-next-line react-hooks/exhaustive-deps
78- } , [ selectedTags , dateFrom , dateTo , sort ] ) ;
79-
80- const handleToggleTag = useCallback ( ( tagId : string ) => {
81- setSelectedTags ( ( prev ) =>
82- prev . includes ( tagId )
83- ? prev . filter ( ( t ) => t !== tagId )
84- : [ ...prev , tagId ]
85- ) ;
86- } , [ ] ) ;
77+ } , [ dateFrom , dateTo , sort , favoritesOnly ] ) ;
8778
8879 const handleSelect = useCallback ( ( item : GalleryItem ) => {
8980 setSelectedItem ( item ) ;
@@ -102,22 +93,19 @@ export default function GalleryPage() {
10293 }
10394 } , [ ] ) ;
10495
105- const handleTagsChange = useCallback ( async ( id : string , newTags : string [ ] ) => {
96+ const handleToggleFavorite = useCallback ( async ( id : string ) => {
10697 try {
107- const res = await fetch ( `/api/media/${ id } /tags` , {
108- method : 'PUT' ,
109- headers : { 'Content-Type' : 'application/json' } ,
110- body : JSON . stringify ( { tags : newTags } ) ,
111- } ) ;
98+ const res = await fetch ( `/api/media/${ id } /favorite` , { method : 'PUT' } ) ;
11299 if ( res . ok ) {
100+ const data = await res . json ( ) ;
101+ const favorited = ! ! data . favorited ;
113102 setItems ( ( prev ) =>
114103 prev . map ( ( item ) =>
115- item . id === id ? { ...item , tags : newTags } : item
104+ item . id === id ? { ...item , favorited } : item
116105 )
117106 ) ;
118- // Update selected item too
119107 setSelectedItem ( ( prev ) =>
120- prev && prev . id === id ? { ...prev , tags : newTags } : prev
108+ prev && prev . id === id ? { ...prev , favorited } : prev
121109 ) ;
122110 }
123111 } catch {
@@ -126,6 +114,29 @@ export default function GalleryPage() {
126114 } , [ ] ) ;
127115
128116 const hasMore = items . length < total ;
117+ const sentinelRef = useRef < HTMLDivElement > ( null ) ;
118+ const loadingRef = useRef ( false ) ;
119+
120+ // Infinite scroll via IntersectionObserver
121+ useEffect ( ( ) => {
122+ const sentinel = sentinelRef . current ;
123+ if ( ! sentinel ) return ;
124+
125+ const observer = new IntersectionObserver (
126+ ( entries ) => {
127+ if ( entries [ 0 ] . isIntersecting && hasMore && ! loadingRef . current ) {
128+ loadingRef . current = true ;
129+ fetchItems ( false ) . finally ( ( ) => {
130+ loadingRef . current = false ;
131+ } ) ;
132+ }
133+ } ,
134+ { rootMargin : '200px' } ,
135+ ) ;
136+
137+ observer . observe ( sentinel ) ;
138+ return ( ) => observer . disconnect ( ) ;
139+ } , [ hasMore , fetchItems ] ) ;
129140
130141 return (
131142 < div className = "flex h-full flex-col overflow-hidden" >
@@ -136,6 +147,20 @@ export default function GalleryPage() {
136147 { t ( 'gallery.title' as TranslationKey ) }
137148 </ h1 >
138149 < div className = "flex items-center gap-2" >
150+ { /* Favorites toggle */ }
151+ < Button
152+ variant = { favoritesOnly ? 'secondary' : 'ghost' }
153+ size = "sm"
154+ onClick = { ( ) => setFavoritesOnly ( ( v ) => ! v ) }
155+ >
156+ < HugeiconsIcon
157+ icon = { FavouriteIcon }
158+ className = { cn ( 'h-3.5 w-3.5' , favoritesOnly && 'text-red-500' ) }
159+ fill = { favoritesOnly ? 'currentColor' : 'none' }
160+ />
161+ { t ( 'gallery.favoritesOnly' as TranslationKey ) }
162+ </ Button >
163+
139164 { /* Filter toggle */ }
140165 < Button
141166 variant = { showFilters ? 'secondary' : 'ghost' }
@@ -163,18 +188,6 @@ export default function GalleryPage() {
163188 { /* Filter bar */ }
164189 { showFilters && (
165190 < div className = "mt-3 space-y-2.5" >
166- { /* Tag filter */ }
167- { ! tagsLoading && tags . length > 0 && (
168- < div >
169- < TagManager
170- tags = { tags }
171- selectedTags = { selectedTags }
172- onToggleTag = { handleToggleTag }
173- compact
174- />
175- </ div >
176- ) }
177-
178191 { /* Date range */ }
179192 < div className = "flex items-center gap-2" >
180193 < label className = "text-xs text-muted-foreground" >
@@ -195,14 +208,13 @@ export default function GalleryPage() {
195208 onChange = { ( e ) => setDateTo ( e . target . value ) }
196209 className = "h-7 rounded-md border border-input bg-transparent px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
197210 />
198- { ( dateFrom || dateTo || selectedTags . length > 0 ) && (
211+ { ( dateFrom || dateTo ) && (
199212 < Button
200213 variant = "ghost"
201214 size = "xs"
202215 onClick = { ( ) => {
203216 setDateFrom ( '' ) ;
204217 setDateTo ( '' ) ;
205- setSelectedTags ( [ ] ) ;
206218 } }
207219 >
208220 { t ( 'gallery.clearFilters' as TranslationKey ) }
@@ -226,40 +238,28 @@ export default function GalleryPage() {
226238 < p className = "text-xs opacity-70" > { t ( 'gallery.emptyHint' as TranslationKey ) } </ p >
227239 </ div >
228240 ) : (
229- < div className = "space-y-4" >
241+ < div >
230242 < GalleryGrid
231243 items = { items }
232- tags = { tags }
233244 onSelect = { handleSelect }
234245 />
235- { hasMore && (
236- < div className = "flex justify-center py-4" >
237- < Button
238- variant = "outline"
239- size = "sm"
240- onClick = { ( ) => fetchItems ( false ) }
241- disabled = { loading }
242- >
243- { loading ? (
244- < HugeiconsIcon icon = { Loading02Icon } className = "h-3.5 w-3.5 animate-spin" />
245- ) : (
246- t ( 'gallery.loadMore' as TranslationKey )
247- ) }
248- </ Button >
249- </ div >
250- ) }
246+ { /* Sentinel for infinite scroll */ }
247+ < div ref = { sentinelRef } className = "flex justify-center py-4" >
248+ { loading && (
249+ < HugeiconsIcon icon = { Loading02Icon } className = "h-4 w-4 animate-spin text-muted-foreground" />
250+ ) }
251+ </ div >
251252 </ div >
252253 ) }
253254 </ div >
254255
255256 { /* Detail dialog */ }
256257 < GalleryDetail
257258 item = { selectedItem }
258- tags = { tags }
259259 open = { detailOpen }
260260 onOpenChange = { setDetailOpen }
261261 onDelete = { handleDelete }
262- onTagsChange = { handleTagsChange }
262+ onToggleFavorite = { handleToggleFavorite }
263263 />
264264 </ div >
265265 ) ;
0 commit comments