@@ -15,6 +15,8 @@ import { toast } from "sonner";
1515import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar" ;
1616import { DateTooltip } from "@/components/shared/date-tooltip" ;
1717import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input" ;
18+ import { TagBadge } from "@/components/shared/tag-badge" ;
19+ import { TagFilter } from "@/components/shared/tag-filter" ;
1820import {
1921 AlertDialog ,
2022 AlertDialogAction ,
@@ -63,6 +65,7 @@ export const ShowProjects = () => {
6365 const { data : auth } = api . user . get . useQuery ( ) ;
6466 const { data : permissions } = api . user . getPermissions . useQuery ( ) ;
6567 const { mutateAsync } = api . project . remove . useMutation ( ) ;
68+ const { data : availableTags } = api . tag . all . useQuery ( ) ;
6669
6770 const [ searchQuery , setSearchQuery ] = useState (
6871 router . isReady && typeof router . query . q === "string" ? router . query . q : "" ,
@@ -76,10 +79,31 @@ export const ShowProjects = () => {
7679 return "createdAt-desc" ;
7780 } ) ;
7881
82+ const [ selectedTagIds , setSelectedTagIds ] = useState < string [ ] > ( ( ) => {
83+ if ( typeof window !== "undefined" ) {
84+ const saved = localStorage . getItem ( "projectsTagFilter" ) ;
85+ return saved ? JSON . parse ( saved ) : [ ] ;
86+ }
87+ return [ ] ;
88+ } ) ;
89+
7990 useEffect ( ( ) => {
8091 localStorage . setItem ( "projectsSort" , sortBy ) ;
8192 } , [ sortBy ] ) ;
8293
94+ useEffect ( ( ) => {
95+ localStorage . setItem ( "projectsTagFilter" , JSON . stringify ( selectedTagIds ) ) ;
96+ } , [ selectedTagIds ] ) ;
97+
98+ useEffect ( ( ) => {
99+ if ( ! availableTags ) return ;
100+ const validIds = new Set ( availableTags . map ( ( t ) => t . tagId ) ) ;
101+ setSelectedTagIds ( ( prev ) => {
102+ const filtered = prev . filter ( ( id ) => validIds . has ( id ) ) ;
103+ return filtered . length === prev . length ? prev : filtered ;
104+ } ) ;
105+ } , [ availableTags ] ) ;
106+
83107 useEffect ( ( ) => {
84108 if ( ! router . isReady ) return ;
85109 const urlQuery = typeof router . query . q === "string" ? router . query . q : "" ;
@@ -107,7 +131,7 @@ export const ShowProjects = () => {
107131 const filteredProjects = useMemo ( ( ) => {
108132 if ( ! data ) return [ ] ;
109133
110- const filtered = data . filter (
134+ let filtered = data . filter (
111135 ( project ) =>
112136 project . name
113137 . toLowerCase ( )
@@ -117,6 +141,15 @@ export const ShowProjects = () => {
117141 . includes ( debouncedSearchQuery . toLowerCase ( ) ) ,
118142 ) ;
119143
144+ // Filter by selected tags (OR logic: show projects with ANY selected tag)
145+ if ( selectedTagIds . length > 0 ) {
146+ filtered = filtered . filter ( ( project ) =>
147+ project . projectTags ?. some ( ( pt ) =>
148+ selectedTagIds . includes ( pt . tag . tagId ) ,
149+ ) ,
150+ ) ;
151+ }
152+
120153 // Then sort the filtered results
121154 const [ field , direction ] = sortBy . split ( "-" ) ;
122155 return [ ...filtered ] . sort ( ( a , b ) => {
@@ -162,7 +195,7 @@ export const ShowProjects = () => {
162195 }
163196 return direction === "asc" ? comparison : - comparison ;
164197 } ) ;
165- } , [ data , debouncedSearchQuery , sortBy ] ) ;
198+ } , [ data , debouncedSearchQuery , sortBy , selectedTagIds ] ) ;
166199
167200 return (
168201 < >
@@ -213,29 +246,44 @@ export const ShowProjects = () => {
213246
214247 < Search className = "absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
215248 </ div >
216- < div className = "flex items-center gap-2 min-w-48 max-sm:w-full" >
217- < ArrowUpDown className = "size-4 text-muted-foreground" />
218- < Select value = { sortBy } onValueChange = { setSortBy } >
219- < SelectTrigger className = "w-full" >
220- < SelectValue placeholder = "Sort by..." />
221- </ SelectTrigger >
222- < SelectContent >
223- < SelectItem value = "name-asc" > Name (A-Z)</ SelectItem >
224- < SelectItem value = "name-desc" > Name (Z-A)</ SelectItem >
225- < SelectItem value = "createdAt-desc" >
226- Newest first
227- </ SelectItem >
228- < SelectItem value = "createdAt-asc" >
229- Oldest first
230- </ SelectItem >
231- < SelectItem value = "services-desc" >
232- Most services
233- </ SelectItem >
234- < SelectItem value = "services-asc" >
235- Least services
236- </ SelectItem >
237- </ SelectContent >
238- </ Select >
249+ < div className = "flex items-center gap-2" >
250+ < TagFilter
251+ tags = {
252+ availableTags ?. map ( ( tag ) => ( {
253+ id : tag . tagId ,
254+ name : tag . name ,
255+ color : tag . color || undefined ,
256+ } ) ) || [ ]
257+ }
258+ selectedTags = { selectedTagIds }
259+ onTagsChange = { setSelectedTagIds }
260+ />
261+ < div className = "flex items-center gap-2 min-w-48 max-sm:w-full" >
262+ < ArrowUpDown className = "size-4 text-muted-foreground" />
263+ < Select value = { sortBy } onValueChange = { setSortBy } >
264+ < SelectTrigger className = "w-full" >
265+ < SelectValue placeholder = "Sort by..." />
266+ </ SelectTrigger >
267+ < SelectContent >
268+ < SelectItem value = "name-asc" > Name (A-Z)</ SelectItem >
269+ < SelectItem value = "name-desc" >
270+ Name (Z-A)
271+ </ SelectItem >
272+ < SelectItem value = "createdAt-desc" >
273+ Newest first
274+ </ SelectItem >
275+ < SelectItem value = "createdAt-asc" >
276+ Oldest first
277+ </ SelectItem >
278+ < SelectItem value = "services-desc" >
279+ Most services
280+ </ SelectItem >
281+ < SelectItem value = "services-asc" >
282+ Least services
283+ </ SelectItem >
284+ </ SelectContent >
285+ </ Select >
286+ </ div >
239287 </ div >
240288 </ div >
241289 { filteredProjects ?. length === 0 && (
@@ -314,6 +362,19 @@ export const ShowProjects = () => {
314362 { project . description }
315363 </ span >
316364
365+ { project . projectTags &&
366+ project . projectTags . length > 0 && (
367+ < div className = "flex flex-wrap gap-1.5 mt-2" >
368+ { project . projectTags . map ( ( pt ) => (
369+ < TagBadge
370+ key = { pt . tag . tagId }
371+ name = { pt . tag . name }
372+ color = { pt . tag . color }
373+ />
374+ ) ) }
375+ </ div >
376+ ) }
377+
317378 { hasNoEnvironments && (
318379 < div className = "flex flex-row gap-2 items-center rounded-lg bg-yellow-50 p-2 mt-2 dark:bg-yellow-950" >
319380 < AlertTriangle className = "size-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
0 commit comments