Skip to content

Commit d90a297

Browse files
author
bhavabhuthi
committed
feat: add category details page with listing
1 parent d50305e commit d90a297

1 file changed

Lines changed: 397 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)