HomePageActions.tsx144 lines · main
1import { keepPreviousData } from '@tanstack/react-query'
2import { useDebounce } from '@uidotdev/usehooks'
3import { LOCAL_STORAGE_KEYS, useParams } from 'common'
4import { Grid, List, Loader2, Plus, Search, X } from 'lucide-react'
5import Link from 'next/link'
6import { parseAsArrayOf, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
7import { useEffect } from 'react'
8import { Button, ToggleGroup, ToggleGroupItem } from 'ui'
9import { Input } from 'ui-patterns/DataInputs/Input'
10
11import { FilterPopover } from '../ui/FilterPopover'
12import { SortDropdown } from '../ui/SortDropdown'
13import {
14 PROJECT_LIST_SORT_VALUES,
15 type ProjectListSort,
16} from '@/components/interfaces/Home/ProjectList/ProjectListSort.utils'
17import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
18import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
19import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
20import { PROJECT_STATUS } from '@/lib/constants'
21
22interface HomePageActionsProps {
23 slug?: string
24 hideNewProject?: boolean
25}
26
27export const HomePageActions = ({ slug: _slug, hideNewProject = false }: HomePageActionsProps) => {
28 const { slug: urlSlug } = useParams()
29 const projectCreationEnabled = useIsFeatureEnabled('projects:create')
30
31 const slug = _slug ?? urlSlug
32 const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
33 const debouncedSearch = useDebounce(search, 500)
34 const [filterStatus, setFilterStatus] = useQueryState(
35 'status',
36 parseAsArrayOf(parseAsString, ',').withDefault([])
37 )
38 const [sort, setSort] = useQueryState(
39 'sort',
40 parseAsStringLiteral(PROJECT_LIST_SORT_VALUES).withDefault('name_asc')
41 )
42 const [viewMode, setViewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')
43
44 const [filterStatusStorage, setFilterStatusStorage, { isSuccess: isSuccessFilterStatusStorage }] =
45 useLocalStorageQuery<string[]>(LOCAL_STORAGE_KEYS.PROJECTS_FILTER, [])
46
47 const [sortStorage, setSortStorage, { isSuccess: isSuccessSortStorage }] =
48 useLocalStorageQuery<ProjectListSort>(LOCAL_STORAGE_KEYS.PROJECTS_SORT, 'name_asc')
49
50 const { isFetching: isFetchingProjects } = useOrgProjectsInfiniteQuery(
51 {
52 slug,
53 sort,
54 search: search.length === 0 ? search : debouncedSearch,
55 statuses: filterStatus,
56 },
57 { placeholderData: keepPreviousData }
58 )
59
60 useEffect(() => {
61 if (isSuccessFilterStatusStorage && !!slug) setFilterStatus(filterStatusStorage)
62 }, [filterStatusStorage, isSuccessFilterStatusStorage, setFilterStatus, slug])
63
64 useEffect(() => {
65 if (isSuccessSortStorage && slug) setSort(sortStorage)
66 }, [sortStorage, isSuccessSortStorage, setSort, slug])
67
68 return (
69 <div className="flex flex-wrap items-center justify-between gap-2 w-full">
70 <div className="flex flex-col gap-2 min-w-0 flex-1 basis-full md:basis-auto sm:flex-row sm:flex-wrap sm:items-center">
71 <Input
72 placeholder="Search for a project"
73 icon={<Search />}
74 size="tiny"
75 className="w-full sm:w-32 md:w-64"
76 value={search}
77 onChange={(event) => setSearch(event.target.value)}
78 actions={[
79 search && (
80 <Button
81 key="clear"
82 size="tiny"
83 type="text"
84 icon={<X />}
85 onClick={() => setSearch('')}
86 className="p-0 h-5 w-5"
87 />
88 ),
89 ]}
90 />
91
92 <div className="flex items-center gap-2">
93 <FilterPopover
94 name="Status"
95 title="Filter projects by status"
96 options={[
97 { key: PROJECT_STATUS.ACTIVE_HEALTHY, label: 'Active' },
98 { key: PROJECT_STATUS.INACTIVE, label: 'Paused' },
99 ]}
100 activeOptions={filterStatus}
101 valueKey="key"
102 labelKey="label"
103 onSaveFilters={(options) => setFilterStatusStorage(options)}
104 />
105
106 <SortDropdown
107 options={[
108 { label: 'name', value: 'name' },
109 { label: 'creation date', value: 'created' },
110 ]}
111 value={sort}
112 setValue={(val) => setSortStorage(val as ProjectListSort)}
113 />
114
115 {isFetchingProjects && <Loader2 className="animate-spin" size={14} />}
116 </div>
117 </div>
118
119 <div className="flex items-center gap-2 shrink-0">
120 {viewMode && setViewMode && (
121 <ToggleGroup
122 type="single"
123 size="sm"
124 value={viewMode}
125 onValueChange={(value) => value && setViewMode(value as 'grid' | 'table')}
126 >
127 <ToggleGroupItem value="grid" size="sm" className="h-[26px] w-[26px] p-0">
128 <Grid size={14} strokeWidth={1.5} />
129 </ToggleGroupItem>
130 <ToggleGroupItem value="table" size="sm" className="h-[26px] w-[26px] p-0">
131 <List size={14} strokeWidth={1.5} />
132 </ToggleGroupItem>
133 </ToggleGroup>
134 )}
135
136 {projectCreationEnabled && !hideNewProject && (
137 <Button asChild icon={<Plus />} type="primary" size="tiny">
138 <Link href={`/new/${slug}`}>New project</Link>
139 </Button>
140 )}
141 </div>
142 </div>
143 )
144}