feat: add category pages

This commit is contained in:
2025-07-19 21:37:09 +07:00
parent 7ce5f8d4aa
commit 2522dd6584
16 changed files with 862 additions and 746 deletions
@@ -0,0 +1,14 @@
interface Props {
params: Promise<{
category: string;
subcategory: string;
}>;
}
const Page = async ({ params }: Props) => {
const { category, subcategory } = await params;
return <div>Subcategory Page: {subcategory} in Category: {category}</div>;
};
export default Page;
+13
View File
@@ -0,0 +1,13 @@
interface Props {
params: Promise<{
category: string;
}>;
}
const Page = async ({ params }: Props) => {
const { category } = await params;
return <div>Category Page: {category}</div>;
};
export default Page;
+3 -3
View File
@@ -3,9 +3,9 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient, trpc } from '@/trpc/server';
import Navbar from './navbar';
import Footer from './footer';
import SearchFilters, { SearchFiltersSkeleton } from './search-filters';
import Navbar from '@/modules/home/ui/components/navbar';
import Footer from '@/modules/home/ui/components/footer';
import SearchFilters, { SearchFiltersSkeleton } from '@/modules/home/ui/components/search-filters';
interface LayoutProps {
children: ReactNode;
@@ -1,37 +0,0 @@
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '@/trpc/client';
import Categories from './categories';
import SearchInput from './search-input';
const SearchFilters = () => {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions());
return (
<div className="px-4 lg:px-12 py-8 border-b flex flex-col gap-4 w-full bg-[#F5F5F5]">
<SearchInput />
<div className="hidden lg:block">
<Categories data={data} />
</div>
</div>
);
};
export const SearchFiltersSkeleton = () => {
return (
<div className="px-4 lg:px-12 py-8 border-b flex flex-col gap-4 w-full bg-[#F5F5F5]">
<SearchInput disabled />
<div className="hidden lg:block">
<div className="h-11" />
</div>
</div>
);
};
export default SearchFilters;
@@ -0,0 +1 @@
export const DEFAULT_BG_COLOR = '#F5F5F5';
@@ -0,0 +1,56 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import Link from 'next/link';
interface Props {
activeCategoryName?: string | null;
activeCategory?: string | null;
activeSubCategoryName?: string | null;
}
export const BreadcrumbNavigation = ({
activeCategory,
activeCategoryName,
activeSubCategoryName,
}: Props) => {
if (!activeCategoryName || activeCategory === 'all') return null;
return (
<Breadcrumb>
<BreadcrumbList>
{activeSubCategoryName ? (
<>
<BreadcrumbItem>
<BreadcrumbLink
asChild
className="text-xl font-medium underline text-primary"
>
<Link href={`/${activeCategory}`}>{activeCategoryName}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="text-primary font-medium text-lg">
/
</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage className="text-xl font-medium">
{activeSubCategoryName}
</BreadcrumbPage>
</BreadcrumbItem>
</>
) : (
<BreadcrumbItem>
<BreadcrumbPage className="text-xl font-medium">
{activeCategoryName}
</BreadcrumbPage>
</BreadcrumbItem>
)}
</BreadcrumbList>
</Breadcrumb>
);
};
@@ -1,20 +1,23 @@
'use client';
import { useParams } from 'next/navigation';
import { ListFilterIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { CategoriesGetManyOutput } from '@/modules/categories/type';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import CategoryDropdown from './category-dropdown';
import CategoriesSidebar from './categories-sidebar';
import { CategoriesGetManyOutput } from '@/modules/categories/type';
interface CategoriesProps {
data: CategoriesGetManyOutput;
}
const Categories = ({ data }: CategoriesProps) => {
const params = useParams();
const containerRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);
const viewAllRef = useRef<HTMLDivElement>(null);
@@ -23,7 +26,8 @@ const Categories = ({ data }: CategoriesProps) => {
const [isAnyHovered, setIsAnyHovered] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const activeCategory = 'all';
const categoryParam = params.category as string | undefined;
const activeCategory = categoryParam || 'all';
const activeCategoryIndex = data.findIndex(
(cat) => cat.slug === activeCategory
@@ -107,6 +111,7 @@ const Categories = ({ data }: CategoriesProps) => {
<div ref={viewAllRef} className="shrink-0">
<Button
variant="elevated"
className={cn(
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
isActiveCategoryHidden &&
@@ -0,0 +1,64 @@
'use client';
import { useSuspenseQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { useTRPC } from '@/trpc/client';
import Categories from './categories';
import SearchInput from './search-input';
import { DEFAULT_BG_COLOR } from '../constants';
import { BreadcrumbNavigation } from './breadcrumb-navigation';
const SearchFilters = () => {
const trpc = useTRPC();
const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions());
const params = useParams();
const categoryParam = params.category as string | undefined;
const activeCategory = categoryParam || 'all';
const activeCategoryData = data.find(
(category) => category.slug === activeCategory
);
const activeCategoryColor = activeCategoryData?.color || DEFAULT_BG_COLOR;
const activeCategoryName = activeCategoryData?.name || null;
const activeSubCategory = params.subcategory as string | undefined;
const activeSubCategoryName =
activeCategoryData?.subcategories.find(
(sub) => sub.slug === activeSubCategory
)?.name || null;
return (
<div
className="px-4 lg:px-12 py-8 border-b flex flex-col gap-4 w-full bg-[#F5F5F5]"
style={{
backgroundColor: activeCategoryColor,
}}
>
<SearchInput />
<div className="hidden lg:block">
<Categories data={data} />
</div>
<BreadcrumbNavigation
activeCategory={activeCategory}
activeCategoryName={activeCategoryName}
activeSubCategoryName={activeSubCategoryName}
/>
</div>
);
};
export const SearchFiltersSkeleton = () => {
return (
<div className="px-4 lg:px-12 py-8 border-b flex flex-col gap-4 w-full bg-[#F5F5F5]">
<SearchInput disabled />
<div className="hidden lg:block">
<div className="h-11" />
</div>
</div>
);
};
export default SearchFilters;