HomePageActions.tsx 5.2 KB

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