22
33import { ActionCard } from "@/components/cards/ActionCard"
44import type { ActionItem } from "@/lib/cms"
5+ import { cn } from "@/lib/utils"
56import { TextInput } from "@code0-tech/pictor"
67import { IconSearch } from "@tabler/icons-react"
78import type { ChangeEvent } from "react"
89import { useMemo , useState } from "react"
910
10- interface ActionsPageContent {
11+ interface ActionSectionContent {
1112 heading : string
1213 description : string
1314 searchPlaceholder : string
1415 noActionsFoundLabel : string
1516 referencesLabel : string
1617}
1718
18- interface ActionsPageClientProps {
19+ interface ActionSectionProps {
1920 actions : ActionItem [ ]
2021 locale : string
21- content ?: Partial < ActionsPageContent > | null
22+ content ?: Partial < ActionSectionContent > | null
2223}
2324
24- const defaultContent : ActionsPageContent = {
25+ const defaultContent : ActionSectionContent = {
2526 heading : "Actions" ,
2627 description : "Browse available actions and integrations." ,
2728 searchPlaceholder : "Search actions" ,
2829 noActionsFoundLabel : "No actions found for your search." ,
2930 referencesLabel : "References" ,
3031}
3132
32- export function ActionsPageClient ( { actions, locale, content } : ActionsPageClientProps ) {
33+ export function ActionSection ( { actions, locale, content } : ActionSectionProps ) {
3334 const labels = { ...defaultContent , ...content }
3435 const [ search , setSearch ] = useState ( "" )
36+ const [ selectedTags , setSelectedTags ] = useState < string [ ] > ( [ ] )
37+
38+ const tags = useMemo ( ( ) => {
39+ return Array . from ( new Set ( actions . flatMap ( ( action ) => action . tags ?. filter ( ( tag ) : tag is string => Boolean ( tag ?. trim ( ) ) ) . map ( ( tag ) => tag . trim ( ) ) ?? [ ] ) ) ) . sort ( ( a , b ) => a . localeCompare ( b ) )
40+ } , [ actions ] )
3541
3642 const filteredActions = useMemo ( ( ) => {
3743 const searchTerm = search . trim ( ) . toLowerCase ( )
38- if ( ! searchTerm ) return actions
3944
4045 return actions . filter ( ( action ) => {
4146 const item = [ action . title , action . shortDescription , action . description , ...( action . tags ?? [ ] ) ]
4247 . filter ( ( value ) => Boolean ( value ) )
4348 . join ( " " )
4449 . toLowerCase ( )
50+ const matchesSearch = ! searchTerm || item . includes ( searchTerm )
51+ const matchesTags = selectedTags . length === 0 || selectedTags . some ( ( selectedTag ) => action . tags ?. includes ( selectedTag ) )
4552
46- return item . includes ( searchTerm )
53+ return matchesSearch && matchesTags
4754 } )
48- } , [ actions , search ] )
55+ } , [ actions , search , selectedTags ] )
56+
57+ const toggleTag = ( tag : string ) => {
58+ setSelectedTags ( ( currentTags ) => ( currentTags . includes ( tag ) ? currentTags . filter ( ( currentTag ) => currentTag !== tag ) : [ ...currentTags , tag ] ) )
59+ }
4960
5061 return (
5162 < div className = "mx-auto flex w-full max-w-4xl flex-col gap-8" >
@@ -61,6 +72,27 @@ export function ActionsPageClient({ actions, locale, content }: ActionsPageClien
6172 clearable
6273 className = "text-white!"
6374 />
75+ { tags . length > 0 && (
76+ < div className = "flex flex-wrap gap-2" >
77+ { tags . map ( ( tag ) => {
78+ const selected = selectedTags . includes ( tag )
79+
80+ return (
81+ < button
82+ key = { tag }
83+ type = "button"
84+ onClick = { ( ) => toggleTag ( tag ) }
85+ className = { cn (
86+ "rounded-full border px-3 py-1 text-xs font-medium transition-colors" ,
87+ selected ? "border-brand/40 bg-brand/15 text-brand" : "border-white/10 bg-white/3 text-tertiary hover:border-white/20 hover:text-white"
88+ ) }
89+ >
90+ { tag }
91+ </ button >
92+ )
93+ } ) }
94+ </ div >
95+ ) }
6496 < div className = "flex flex-col gap-4" >
6597 { filteredActions . map ( ( action ) => (
6698 < ActionCard key = { action . id } action = { action } locale = { locale } />
0 commit comments