index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { type ChangeEvent, useState } from 'react'
  2. import { getRouteApi } from '@tanstack/react-router'
  3. import { SlidersHorizontal, ArrowUpAZ, ArrowDownAZ } from 'lucide-react'
  4. import { Button } from '@/components/ui/button'
  5. import { Input } from '@/components/ui/input'
  6. import {
  7. Select,
  8. SelectContent,
  9. SelectItem,
  10. SelectTrigger,
  11. SelectValue,
  12. } from '@/components/ui/select'
  13. import { Separator } from '@/components/ui/separator'
  14. import { Header } from '@/components/layout/header'
  15. import { Main } from '@/components/layout/main'
  16. import { ProfileDropdown } from '@/components/profile-dropdown'
  17. import { Search } from '@/components/search'
  18. import { ThemeSwitch } from '@/components/theme-switch'
  19. import { apps } from './data/apps'
  20. const route = getRouteApi('/_authenticated/apps/')
  21. type AppType = 'all' | 'connected' | 'notConnected'
  22. const appText = new Map<AppType, string>([
  23. ['all', 'All Apps'],
  24. ['connected', 'Connected'],
  25. ['notConnected', 'Not Connected'],
  26. ])
  27. export function Apps() {
  28. const {
  29. filter = '',
  30. type = 'all',
  31. sort: initSort = 'asc',
  32. } = route.useSearch()
  33. const navigate = route.useNavigate()
  34. const [sort, setSort] = useState(initSort)
  35. const [appType, setAppType] = useState(type)
  36. const [searchTerm, setSearchTerm] = useState(filter)
  37. const filteredApps = apps
  38. .sort((a, b) =>
  39. sort === 'asc'
  40. ? a.name.localeCompare(b.name)
  41. : b.name.localeCompare(a.name)
  42. )
  43. .filter((app) =>
  44. appType === 'connected'
  45. ? app.connected
  46. : appType === 'notConnected'
  47. ? !app.connected
  48. : true
  49. )
  50. .filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()))
  51. const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
  52. setSearchTerm(e.target.value)
  53. navigate({
  54. search: (prev) => ({
  55. ...prev,
  56. filter: e.target.value || undefined,
  57. }),
  58. })
  59. }
  60. const handleTypeChange = (value: AppType) => {
  61. setAppType(value)
  62. navigate({
  63. search: (prev) => ({
  64. ...prev,
  65. type: value === 'all' ? undefined : value,
  66. }),
  67. })
  68. }
  69. const handleSortChange = (sort: 'asc' | 'desc') => {
  70. setSort(sort)
  71. navigate({ search: (prev) => ({ ...prev, sort }) })
  72. }
  73. return (
  74. <>
  75. {/* ===== Top Heading ===== */}
  76. <Header>
  77. <Search />
  78. <div className='ms-auto flex items-center gap-4'>
  79. <ThemeSwitch />
  80. <ProfileDropdown />
  81. </div>
  82. </Header>
  83. {/* ===== Content ===== */}
  84. <Main fixed>
  85. <div>
  86. <h1 className='text-2xl font-bold tracking-tight'>
  87. App Integrations
  88. </h1>
  89. <p className='text-muted-foreground'>
  90. Here&apos;s a list of your apps for the integration!
  91. </p>
  92. </div>
  93. <div className='my-4 flex items-end justify-between sm:my-0 sm:items-center'>
  94. <div className='flex flex-col gap-4 sm:my-4 sm:flex-row'>
  95. <Input
  96. placeholder='Filter apps...'
  97. className='h-9 w-40 lg:w-[250px]'
  98. value={searchTerm}
  99. onChange={handleSearch}
  100. />
  101. <Select value={appType} onValueChange={handleTypeChange}>
  102. <SelectTrigger className='w-36'>
  103. <SelectValue>{appText.get(appType)}</SelectValue>
  104. </SelectTrigger>
  105. <SelectContent>
  106. <SelectItem value='all'>All Apps</SelectItem>
  107. <SelectItem value='connected'>Connected</SelectItem>
  108. <SelectItem value='notConnected'>Not Connected</SelectItem>
  109. </SelectContent>
  110. </Select>
  111. </div>
  112. <Select value={sort} onValueChange={handleSortChange}>
  113. <SelectTrigger className='w-16'>
  114. <SelectValue>
  115. <SlidersHorizontal size={18} />
  116. </SelectValue>
  117. </SelectTrigger>
  118. <SelectContent align='end'>
  119. <SelectItem value='asc'>
  120. <div className='flex items-center gap-4'>
  121. <ArrowUpAZ size={16} />
  122. <span>Ascending</span>
  123. </div>
  124. </SelectItem>
  125. <SelectItem value='desc'>
  126. <div className='flex items-center gap-4'>
  127. <ArrowDownAZ size={16} />
  128. <span>Descending</span>
  129. </div>
  130. </SelectItem>
  131. </SelectContent>
  132. </Select>
  133. </div>
  134. <Separator className='shadow-sm' />
  135. <ul className='faded-bottom no-scrollbar grid gap-4 overflow-auto pt-4 pb-16 md:grid-cols-2 lg:grid-cols-3'>
  136. {filteredApps.map((app) => (
  137. <li
  138. key={app.name}
  139. className='rounded-lg border p-4 hover:shadow-md'
  140. >
  141. <div className='mb-8 flex items-center justify-between'>
  142. <div
  143. className={`bg-muted flex size-10 items-center justify-center rounded-lg p-2`}
  144. >
  145. {app.logo}
  146. </div>
  147. <Button
  148. variant='outline'
  149. size='sm'
  150. className={`${app.connected ? 'border border-blue-300 bg-blue-50 hover:bg-blue-100 dark:border-blue-700 dark:bg-blue-950 dark:hover:bg-blue-900' : ''}`}
  151. >
  152. {app.connected ? 'Connected' : 'Connect'}
  153. </Button>
  154. </div>
  155. <div>
  156. <h2 className='mb-1 font-semibold'>{app.name}</h2>
  157. <p className='line-clamp-2 text-gray-500'>{app.desc}</p>
  158. </div>
  159. </li>
  160. ))}
  161. </ul>
  162. </Main>
  163. </>
  164. )
  165. }