|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; |
| 4 | +import { fetchDatasets } from '@/fetch'; |
| 5 | +import { graphql } from '@/gql'; |
| 6 | +import { useQuery } from '@tanstack/react-query'; |
| 7 | +import Image from 'next/image'; |
| 8 | +import { useRouter } from 'next/navigation'; |
| 9 | +import { Pill, SearchInput, Select, Text } from 'opub-ui'; |
| 10 | +import { useEffect, useReducer, useState } from 'react'; |
| 11 | + |
| 12 | +import BreadCrumbs from '@/components/BreadCrumbs'; |
| 13 | +import { ErrorPage } from '@/components/error'; |
| 14 | +import { Loading } from '@/components/loading'; |
| 15 | +import { GraphQL } from '@/lib/api'; |
| 16 | +import Card from '../../datasets/components/Card'; |
| 17 | +import Filter from '../../datasets/components/FIlter/Filter'; |
| 18 | + |
| 19 | +const categoryQueryDoc: any = graphql(` |
| 20 | + query CategoryDetails($filters: CategoryFilter) { |
| 21 | + categories(filters: $filters) { |
| 22 | + id |
| 23 | + name |
| 24 | + description |
| 25 | + datasetCount |
| 26 | + } |
| 27 | + } |
| 28 | +`); |
| 29 | + |
| 30 | +interface Bucket { |
| 31 | + key: string; |
| 32 | + doc_count: number; |
| 33 | +} |
| 34 | + |
| 35 | +interface Aggregation { |
| 36 | + buckets: Bucket[]; |
| 37 | +} |
| 38 | + |
| 39 | +interface Aggregations { |
| 40 | + [key: string]: Aggregation; |
| 41 | +} |
| 42 | + |
| 43 | +interface FilterOptions { |
| 44 | + [key: string]: string[]; |
| 45 | +} |
| 46 | + |
| 47 | +interface QueryParams { |
| 48 | + pageSize: number; |
| 49 | + currentPage: number; |
| 50 | + filters: FilterOptions; |
| 51 | + query?: string; |
| 52 | +} |
| 53 | + |
| 54 | +type Action = |
| 55 | + | { type: 'SET_PAGE_SIZE'; payload: number } |
| 56 | + | { type: 'SET_CURRENT_PAGE'; payload: number } |
| 57 | + | { type: 'SET_FILTERS'; payload: { category: string; values: string[] } } |
| 58 | + | { type: 'REMOVE_FILTER'; payload: { category: string; value: string } } |
| 59 | + | { type: 'SET_QUERY'; payload: string } |
| 60 | + | { type: 'INITIALIZE'; payload: QueryParams }; |
| 61 | + |
| 62 | +const initialState: QueryParams = { |
| 63 | + pageSize: 5, |
| 64 | + currentPage: 1, |
| 65 | + filters: {}, |
| 66 | + query: '', |
| 67 | +}; |
| 68 | + |
| 69 | +const queryReducer = (state: QueryParams, action: Action): QueryParams => { |
| 70 | + switch (action.type) { |
| 71 | + case 'SET_PAGE_SIZE': { |
| 72 | + return { ...state, pageSize: action.payload, currentPage: 1 }; |
| 73 | + } |
| 74 | + case 'SET_CURRENT_PAGE': { |
| 75 | + return { ...state, currentPage: action.payload }; |
| 76 | + } |
| 77 | + case 'SET_FILTERS': { |
| 78 | + return { |
| 79 | + ...state, |
| 80 | + filters: { |
| 81 | + ...state.filters, |
| 82 | + [action.payload.category]: action.payload.values, |
| 83 | + }, |
| 84 | + currentPage: 1, |
| 85 | + }; |
| 86 | + } |
| 87 | + case 'REMOVE_FILTER': { |
| 88 | + const newFilters = { ...state.filters }; |
| 89 | + newFilters[action.payload.category] = newFilters[ |
| 90 | + action.payload.category |
| 91 | + ].filter((v) => v !== action.payload.value); |
| 92 | + return { ...state, filters: newFilters, currentPage: 1 }; |
| 93 | + } |
| 94 | + case 'SET_QUERY': { |
| 95 | + return { ...state, query: action.payload }; |
| 96 | + } |
| 97 | + case 'INITIALIZE': { |
| 98 | + return { ...state, ...action.payload }; |
| 99 | + } |
| 100 | + default: |
| 101 | + return state; |
| 102 | + } |
| 103 | +}; |
| 104 | + |
| 105 | +const useUrlParams = ( |
| 106 | + queryParams: QueryParams, |
| 107 | + setQueryParams: React.Dispatch<Action>, |
| 108 | + setVariables: (vars: string) => void |
| 109 | +) => { |
| 110 | + const router = useRouter(); |
| 111 | + |
| 112 | + useEffect(() => { |
| 113 | + const urlParams = new URLSearchParams(window.location.search); |
| 114 | + const sizeParam = urlParams.get('size'); |
| 115 | + const pageParam = urlParams.get('page'); |
| 116 | + const filters: FilterOptions = {}; |
| 117 | + |
| 118 | + urlParams.forEach((value, key) => { |
| 119 | + if (!['size', 'page', 'query'].includes(key)) { |
| 120 | + filters[key] = value.split(','); |
| 121 | + } |
| 122 | + }); |
| 123 | + |
| 124 | + const initialParams: QueryParams = { |
| 125 | + pageSize: sizeParam ? Number(sizeParam) : 5, |
| 126 | + currentPage: pageParam ? Number(pageParam) : 1, |
| 127 | + filters, |
| 128 | + query: urlParams.get('query') || '', |
| 129 | + }; |
| 130 | + |
| 131 | + setQueryParams({ type: 'INITIALIZE', payload: initialParams }); |
| 132 | + }, [setQueryParams]); |
| 133 | + |
| 134 | + useEffect(() => { |
| 135 | + const filtersString = Object.entries(queryParams.filters) |
| 136 | + .filter(([_, values]) => values.length > 0) |
| 137 | + .map(([key, values]) => `${key}=${values.join(',')}`) |
| 138 | + .join('&'); |
| 139 | + |
| 140 | + const searchParam = queryParams.query |
| 141 | + ? `&query=${encodeURIComponent(queryParams.query)}` |
| 142 | + : ''; |
| 143 | + const variablesString = `?${filtersString}&size=${queryParams.pageSize}&page=${queryParams.currentPage}${searchParam}`; |
| 144 | + setVariables(variablesString); |
| 145 | + |
| 146 | + const currentUrl = new URL(window.location.href); |
| 147 | + currentUrl.searchParams.set('size', queryParams.pageSize.toString()); |
| 148 | + currentUrl.searchParams.set('page', queryParams.currentPage.toString()); |
| 149 | + |
| 150 | + Object.entries(queryParams.filters).forEach(([key, values]) => { |
| 151 | + if (values.length > 0) { |
| 152 | + currentUrl.searchParams.set(key, values.join(',')); |
| 153 | + } else { |
| 154 | + currentUrl.searchParams.delete(key); |
| 155 | + } |
| 156 | + }); |
| 157 | + |
| 158 | + if (queryParams.query) { |
| 159 | + currentUrl.searchParams.set('query', queryParams.query); |
| 160 | + } else { |
| 161 | + currentUrl.searchParams.delete('query'); |
| 162 | + } |
| 163 | + |
| 164 | + router.push(currentUrl.toString()); |
| 165 | + }, [queryParams, setVariables, router]); |
| 166 | +}; |
| 167 | + |
| 168 | +const CategoryDetailsPage = ({ params }: { params: { categorySlug: any } }) => { |
| 169 | + const getCategoryDetails: { |
| 170 | + data: any; |
| 171 | + isLoading: boolean; |
| 172 | + isError: boolean; |
| 173 | + } = useQuery([`get_category_details_${params.categorySlug}`], () => |
| 174 | + GraphQL(categoryQueryDoc, { filters: { slug: params.categorySlug } }) |
| 175 | + ); |
| 176 | + |
| 177 | + const [facets, setFacets] = useState<{ |
| 178 | + results: any[]; |
| 179 | + total: number; |
| 180 | + aggregations: Aggregations; |
| 181 | + } | null>(null); |
| 182 | + const [variables, setVariables] = useState(''); |
| 183 | + const [open, setOpen] = useState(false); |
| 184 | + const count = facets?.total ?? 0; |
| 185 | + const datasetDetails = facets?.results ?? []; |
| 186 | + const [queryParams, setQueryParams] = useReducer(queryReducer, initialState); |
| 187 | + |
| 188 | + useEffect(() => { |
| 189 | + if (variables) { |
| 190 | + fetchDatasets(variables) |
| 191 | + .then((res) => { |
| 192 | + setFacets(res); |
| 193 | + }) |
| 194 | + .catch((err) => { |
| 195 | + console.error(err); |
| 196 | + }); |
| 197 | + } |
| 198 | + }, [variables]); |
| 199 | + |
| 200 | + useUrlParams(queryParams, setQueryParams, setVariables); |
| 201 | + |
| 202 | + const handlePageChange = (newPage: number) => { |
| 203 | + setQueryParams({ type: 'SET_CURRENT_PAGE', payload: newPage }); |
| 204 | + }; |
| 205 | + |
| 206 | + const handlePageSizeChange = (newSize: number) => { |
| 207 | + setQueryParams({ type: 'SET_PAGE_SIZE', payload: newSize }); |
| 208 | + }; |
| 209 | + |
| 210 | + const handleFilterChange = (category: string, values: string[]) => { |
| 211 | + setQueryParams({ type: 'SET_FILTERS', payload: { category, values } }); |
| 212 | + }; |
| 213 | + |
| 214 | + const handleRemoveFilter = (category: string, value: string) => { |
| 215 | + setQueryParams({ type: 'REMOVE_FILTER', payload: { category, value } }); |
| 216 | + }; |
| 217 | + |
| 218 | + const handleSearch = (searchTerm: string) => { |
| 219 | + setQueryParams({ type: 'SET_QUERY', payload: searchTerm }); |
| 220 | + }; |
| 221 | + |
| 222 | + const aggregations: Aggregations = facets?.aggregations || {}; |
| 223 | + |
| 224 | + const filterOptions = Object.entries(aggregations).reduce( |
| 225 | + (acc: Record<string, { label: string; value: string }[]>, [key, value]) => { |
| 226 | + acc[key.replace('.raw', '')] = value.buckets.map((bucket: Bucket) => ({ |
| 227 | + label: bucket.key, |
| 228 | + value: bucket.key, |
| 229 | + })); |
| 230 | + return acc; |
| 231 | + }, |
| 232 | + {} |
| 233 | + ); |
| 234 | + |
| 235 | + return ( |
| 236 | + <div className="bg-basePureWhite"> |
| 237 | + <BreadCrumbs |
| 238 | + data={[ |
| 239 | + { href: '/', label: 'Home' }, |
| 240 | + { href: '/categories', label: 'Categories' }, |
| 241 | + { |
| 242 | + href: '#', |
| 243 | + label: |
| 244 | + getCategoryDetails.data?.categories[0].name || |
| 245 | + params.categorySlug, |
| 246 | + }, |
| 247 | + ]} |
| 248 | + /> |
| 249 | + |
| 250 | + {getCategoryDetails.isError ? ( |
| 251 | + <ErrorPage /> |
| 252 | + ) : getCategoryDetails.isLoading ? ( |
| 253 | + <Loading /> |
| 254 | + ) : ( |
| 255 | + <div className="min-h-screen"> |
| 256 | + <div className="flex flex-col items-center gap-8 py-3 lg:flex-row lg:px-28 lg:py-10"> |
| 257 | + <div className="flex flex-col items-center justify-center rounded-2 bg-baseGraySlateSolid2 p-2"> |
| 258 | + <Image |
| 259 | + src={'/obi.jpg'} |
| 260 | + width={164} |
| 261 | + height={164} |
| 262 | + alt={`${params.categorySlug} Logo`} |
| 263 | + /> |
| 264 | + </div> |
| 265 | + <div className="flex flex-col gap-4 p-2"> |
| 266 | + <Text |
| 267 | + variant="heading3xl" |
| 268 | + as="h1" |
| 269 | + // className="text-baseIndigoAlpha4" |
| 270 | + fontWeight="bold" |
| 271 | + > |
| 272 | + {getCategoryDetails.data?.categories[0].name || |
| 273 | + params.categorySlug} |
| 274 | + </Text> |
| 275 | + <Text variant="bodyLg"> |
| 276 | + {getCategoryDetails.data?.categories[0].datasetCount} Datasets |
| 277 | + </Text> |
| 278 | + <Text variant="bodyMd"> |
| 279 | + {getCategoryDetails.data?.categories[0].description || |
| 280 | + 'No description available.'} |
| 281 | + </Text> |
| 282 | + </div> |
| 283 | + </div> |
| 284 | + |
| 285 | + <div> |
| 286 | + <div className="mx-10 my-4 flex flex-wrap items-center justify-between gap-6 rounded-2 bg-baseBlueSolid4 px-4 py-2"> |
| 287 | + <div> |
| 288 | + <Text>Showing 10 of 30 Datasets</Text> |
| 289 | + </div> |
| 290 | + <div className=" w-full max-w-[550px] md:block"> |
| 291 | + <SearchInput |
| 292 | + label="Search" |
| 293 | + name="Search" |
| 294 | + // className={cn(Styles.Search)} |
| 295 | + placeholder="Search datasets" |
| 296 | + onSubmit={(value: any) => console.log(value)} |
| 297 | + onClear={(value: any) => console.log(value)} |
| 298 | + /> |
| 299 | + </div> |
| 300 | + <div className="flex flex-wrap items-center justify-between gap-4"> |
| 301 | + <div className="flex items-center gap-2"> |
| 302 | + <Text |
| 303 | + variant="bodyLg" |
| 304 | + className="font-bold text-baseBlueSolid8" |
| 305 | + > |
| 306 | + Sort by: |
| 307 | + </Text> |
| 308 | + <Select |
| 309 | + label="" |
| 310 | + labelInline |
| 311 | + name="select" |
| 312 | + options={[ |
| 313 | + { |
| 314 | + label: 'Newest', |
| 315 | + value: 'newestUpdate', |
| 316 | + }, |
| 317 | + { |
| 318 | + label: 'Oldest', |
| 319 | + value: 'oldestUpdate', |
| 320 | + }, |
| 321 | + ]} |
| 322 | + /> |
| 323 | + </div> |
| 324 | + <div className="flex items-center gap-2"> |
| 325 | + <Text |
| 326 | + variant="bodyLg" |
| 327 | + className="font-bold text-baseBlueSolid8" |
| 328 | + > |
| 329 | + Rows: |
| 330 | + </Text> |
| 331 | + <Select |
| 332 | + label="" |
| 333 | + labelInline |
| 334 | + name="select" |
| 335 | + options={[ |
| 336 | + { |
| 337 | + label: '10', |
| 338 | + value: '10', |
| 339 | + }, |
| 340 | + { |
| 341 | + label: '20', |
| 342 | + value: '20', |
| 343 | + }, |
| 344 | + ]} |
| 345 | + /> |
| 346 | + </div> |
| 347 | + </div> |
| 348 | + </div> |
| 349 | + </div> |
| 350 | + |
| 351 | + <div className="row mx-10 mb-16 flex gap-5"> |
| 352 | + <div className="hidden min-w-64 max-w-64 lg:block"> |
| 353 | + <Filter |
| 354 | + options={filterOptions} |
| 355 | + setSelectedOptions={handleFilterChange} |
| 356 | + selectedOptions={queryParams.filters} |
| 357 | + /> |
| 358 | + </div> |
| 359 | + <div className="flex h-full w-full flex-col px-2"> |
| 360 | + <div className="flex gap-2 border-b-2 border-solid border-baseGraySlateSolid4 pb-4"> |
| 361 | + {Object.entries(queryParams.filters).map(([category, values]) => |
| 362 | + values.map((value) => ( |
| 363 | + <Pill |
| 364 | + key={`${category}-${value}`} |
| 365 | + onRemove={() => handleRemoveFilter(category, value)} |
| 366 | + > |
| 367 | + {value} |
| 368 | + </Pill> |
| 369 | + )) |
| 370 | + )} |
| 371 | + </div> |
| 372 | + |
| 373 | + <div className="flex flex-col gap-6"> |
| 374 | + {facets && datasetDetails?.length > 0 && ( |
| 375 | + <GraphqlPagination |
| 376 | + totalRows={count} |
| 377 | + pageSize={queryParams.pageSize} |
| 378 | + currentPage={queryParams.currentPage} |
| 379 | + onPageChange={handlePageChange} |
| 380 | + onPageSizeChange={handlePageSizeChange} |
| 381 | + > |
| 382 | + {datasetDetails.map((item: any, index: any) => ( |
| 383 | + <Card key={index} data={item} /> |
| 384 | + ))} |
| 385 | + </GraphqlPagination> |
| 386 | + )} |
| 387 | + </div> |
| 388 | + </div> |
| 389 | + </div> |
| 390 | + </div> |
| 391 | + )} |
| 392 | + </div> |
| 393 | + ); |
| 394 | +}; |
| 395 | + |
| 396 | +export default CategoryDetailsPage; |
0 commit comments