mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 05:31:56 +00:00
feat: add category pages
This commit is contained in:
@@ -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;
|
||||||
@@ -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,9 +3,9 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import { getQueryClient, trpc } from '@/trpc/server';
|
import { getQueryClient, trpc } from '@/trpc/server';
|
||||||
|
|
||||||
import Navbar from './navbar';
|
import Navbar from '@/modules/home/ui/components/navbar';
|
||||||
import Footer from './footer';
|
import Footer from '@/modules/home/ui/components/footer';
|
||||||
import SearchFilters, { SearchFiltersSkeleton } from './search-filters';
|
import SearchFilters, { SearchFiltersSkeleton } from '@/modules/home/ui/components/search-filters';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: ReactNode;
|
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';
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return (
|
return (
|
||||||
<footer className="flex border-t justify-between font-medium p-6">
|
<footer className="flex border-t justify-between font-medium p-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p>funroad, Inc.</p>
|
<p>funroad, Inc.</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Footer
|
export default Footer
|
||||||
+65
-65
@@ -1,65 +1,65 @@
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface NavbarItem {
|
interface NavbarItem {
|
||||||
href: string;
|
href: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: NavbarItem[];
|
items: NavbarItem[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavbarSidebar = ({ items, open, onOpenChange }: Props) => {
|
const NavbarSidebar = ({ items, open, onOpenChange }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent side="left" className="p-0 transition-none">
|
<SheetContent side="left" className="p-0 transition-none">
|
||||||
<SheetHeader className="p-4 border-b">
|
<SheetHeader className="p-4 border-b">
|
||||||
<SheetTitle>Menu</SheetTitle>
|
<SheetTitle>Menu</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<ScrollArea className="flex flex-col overflow-y-auto h-full pb-2">
|
<ScrollArea className="flex flex-col overflow-y-auto h-full pb-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
{item.children}
|
{item.children}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="border-t">
|
<div className="border-t">
|
||||||
<Link
|
<Link
|
||||||
href="/sign-in"
|
href="/sign-in"
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/sign-up"
|
href="/sign-up"
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
Start Selling
|
Start Selling
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NavbarSidebar;
|
export default NavbarSidebar;
|
||||||
@@ -1,147 +1,147 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, useState } from 'react';
|
import { ReactNode, useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Poppins } from 'next/font/google';
|
import { Poppins } from 'next/font/google';
|
||||||
import { MenuIcon } from 'lucide-react';
|
import { MenuIcon } from 'lucide-react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useTRPC } from '@/trpc/client';
|
import { useTRPC } from '@/trpc/client';
|
||||||
|
|
||||||
import NavbarSidebar from './navbar-sidebar';
|
import NavbarSidebar from './navbar-sidebar';
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['700'],
|
weight: ['700'],
|
||||||
});
|
});
|
||||||
|
|
||||||
interface NavbarItemProps {
|
interface NavbarItemProps {
|
||||||
href: string;
|
href: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navbarItems = [
|
const navbarItems = [
|
||||||
{
|
{
|
||||||
href: '/',
|
href: '/',
|
||||||
children: 'Home',
|
children: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/about',
|
href: '/about',
|
||||||
children: 'About',
|
children: 'About',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/features',
|
href: '/features',
|
||||||
children: 'Features',
|
children: 'Features',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/pricing',
|
href: '/pricing',
|
||||||
children: 'Pricing',
|
children: 'Pricing',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/contact',
|
href: '/contact',
|
||||||
children: 'Contact',
|
children: 'Contact',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => {
|
const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
||||||
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={href}>{children}</Link>
|
<Link href={href}>{children}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const session = useQuery(trpc.auth.session.queryOptions());
|
const session = useQuery(trpc.auth.session.queryOptions());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
||||||
<Link href="/" className="pl-6 flex items-center">
|
<Link href="/" className="pl-6 flex items-center">
|
||||||
<span className={cn('text-5xl font-semibold', poppins.className)}>
|
<span className={cn('text-5xl font-semibold', poppins.className)}>
|
||||||
funroad
|
funroad
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<NavbarSidebar
|
<NavbarSidebar
|
||||||
items={navbarItems}
|
items={navbarItems}
|
||||||
open={isSidebarOpen}
|
open={isSidebarOpen}
|
||||||
onOpenChange={setIsSidebarOpen}
|
onOpenChange={setIsSidebarOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="items-center gap-4 hidden lg:flex">
|
<div className="items-center gap-4 hidden lg:flex">
|
||||||
{navbarItems.map((item) => (
|
{navbarItems.map((item) => (
|
||||||
<NavbarItem
|
<NavbarItem
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
isActive={pathname === item.href}
|
isActive={pathname === item.href}
|
||||||
>
|
>
|
||||||
{item.children}
|
{item.children}
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{session.data?.user ? (
|
{session.data?.user ? (
|
||||||
<div className='hidden lg:flex'>
|
<div className='hidden lg:flex'>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
||||||
>
|
>
|
||||||
<Link href="/admin">
|
<Link href="/admin">
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
||||||
>
|
>
|
||||||
<Link prefetch href="/sign-in">
|
<Link prefetch href="/sign-in">
|
||||||
Log in
|
Log in
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
||||||
>
|
>
|
||||||
<Link prefetch href="/sign-up">
|
<Link prefetch href="/sign-up">
|
||||||
Start Selling
|
Start Selling
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex lg:hidden items-center justify-center">
|
<div className="flex lg:hidden items-center justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="size-12 border-transparent bg-white"
|
className="size-12 border-transparent bg-white"
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Navbar;
|
export default Navbar;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+113
-113
@@ -1,113 +1,113 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useTRPC } from '@/trpc/client';
|
import { useTRPC } from '@/trpc/client';
|
||||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||||
|
|
||||||
interface CategoriesSidebarProps {
|
interface CategoriesSidebarProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategoriesSidebar = ({
|
const CategoriesSidebar = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: CategoriesSidebarProps) => {
|
}: CategoriesSidebarProps) => {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions());
|
const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions());
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [parentCategories, setParentCategories] = useState<
|
const [parentCategories, setParentCategories] = useState<
|
||||||
CategoriesGetManyOutput | null
|
CategoriesGetManyOutput | null
|
||||||
>(null);
|
>(null);
|
||||||
const [selectedCategory, setSelectedCategory] =
|
const [selectedCategory, setSelectedCategory] =
|
||||||
useState<CategoriesGetManyOutput[1] | null>(null);
|
useState<CategoriesGetManyOutput[1] | null>(null);
|
||||||
|
|
||||||
// If we have parent categories, show those, otherwise show root categories
|
// If we have parent categories, show those, otherwise show root categories
|
||||||
const currentCategories = parentCategories ?? data ?? [];
|
const currentCategories = parentCategories ?? data ?? [];
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
setSelectedCategory(null);
|
setSelectedCategory(null);
|
||||||
setParentCategories(null);
|
setParentCategories(null);
|
||||||
onOpenChange(open);
|
onOpenChange(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryClick = (category: CategoriesGetManyOutput[1]) => {
|
const handleCategoryClick = (category: CategoriesGetManyOutput[1]) => {
|
||||||
if (category.subcategories && category.subcategories.length > 0) {
|
if (category.subcategories && category.subcategories.length > 0) {
|
||||||
setParentCategories(category.subcategories as CategoriesGetManyOutput);
|
setParentCategories(category.subcategories as CategoriesGetManyOutput);
|
||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
} else {
|
} else {
|
||||||
// This is a leaf category (no subcategories)
|
// This is a leaf category (no subcategories)
|
||||||
if (parentCategories && selectedCategory) {
|
if (parentCategories && selectedCategory) {
|
||||||
router.push(`/${selectedCategory.slug}/${category.slug}`);
|
router.push(`/${selectedCategory.slug}/${category.slug}`);
|
||||||
} else {
|
} else {
|
||||||
if (category.slug === 'all') {
|
if (category.slug === 'all') {
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else {
|
} else {
|
||||||
router.push(`/${category.slug}`);
|
router.push(`/${category.slug}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
if (parentCategories) {
|
if (parentCategories) {
|
||||||
setParentCategories(null);
|
setParentCategories(null);
|
||||||
setSelectedCategory(null);
|
setSelectedCategory(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundColor = selectedCategory?.color || 'white';
|
const backgroundColor = selectedCategory?.color || 'white';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="left"
|
side="left"
|
||||||
className="p-0 transition-none"
|
className="p-0 transition-none"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
<SheetHeader className="p-4 border-b">
|
<SheetHeader className="p-4 border-b">
|
||||||
<SheetTitle>Categories</SheetTitle>
|
<SheetTitle>Categories</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<ScrollArea className="flex flex-col overflow-y-auto h-full pb-2">
|
<ScrollArea className="flex flex-col overflow-y-auto h-full pb-2">
|
||||||
{parentCategories && (
|
{parentCategories && (
|
||||||
<button
|
<button
|
||||||
onClick={handleBackClick}
|
onClick={handleBackClick}
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium cursor-pointer"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex items-center text-base font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="size-4 mr-2" />
|
<ChevronLeftIcon className="size-4 mr-2" />
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentCategories.map((category) => (
|
{currentCategories.map((category) => (
|
||||||
<button
|
<button
|
||||||
key={category.slug}
|
key={category.slug}
|
||||||
onClick={() => handleCategoryClick(category)}
|
onClick={() => handleCategoryClick(category)}
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex justify-between items-center text-base font-medium cursor-pointer"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex justify-between items-center text-base font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
{category.name}
|
{category.name}
|
||||||
{category.subcategories && category.subcategories.length > 0 && (
|
{category.subcategories && category.subcategories.length > 0 && (
|
||||||
<ChevronRightIcon className="size-4" />
|
<ChevronRightIcon className="size-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CategoriesSidebar;
|
export default CategoriesSidebar;
|
||||||
+132
-127
@@ -1,127 +1,132 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ListFilterIcon } from 'lucide-react';
|
import { useParams } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { ListFilterIcon } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||||
import CategoryDropdown from './category-dropdown';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import CategoriesSidebar from './categories-sidebar';
|
|
||||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
import CategoryDropdown from './category-dropdown';
|
||||||
|
import CategoriesSidebar from './categories-sidebar';
|
||||||
interface CategoriesProps {
|
|
||||||
data: CategoriesGetManyOutput;
|
interface CategoriesProps {
|
||||||
}
|
data: CategoriesGetManyOutput;
|
||||||
|
}
|
||||||
const Categories = ({ data }: CategoriesProps) => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const Categories = ({ data }: CategoriesProps) => {
|
||||||
const measureRef = useRef<HTMLDivElement>(null);
|
const params = useParams();
|
||||||
const viewAllRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [visibleCount, setVisibleCount] = useState(data.length);
|
const measureRef = useRef<HTMLDivElement>(null);
|
||||||
const [isAnyHovered, setIsAnyHovered] = useState(false);
|
const viewAllRef = useRef<HTMLDivElement>(null);
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
||||||
|
const [visibleCount, setVisibleCount] = useState(data.length);
|
||||||
const activeCategory = 'all';
|
const [isAnyHovered, setIsAnyHovered] = useState(false);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const activeCategoryIndex = data.findIndex(
|
|
||||||
(cat) => cat.slug === activeCategory
|
const categoryParam = params.category as string | undefined;
|
||||||
);
|
const activeCategory = categoryParam || 'all';
|
||||||
const isActiveCategoryHidden =
|
|
||||||
activeCategoryIndex >= visibleCount && activeCategoryIndex !== -1;
|
const activeCategoryIndex = data.findIndex(
|
||||||
|
(cat) => cat.slug === activeCategory
|
||||||
useEffect(() => {
|
);
|
||||||
const calculateVisible = () => {
|
const isActiveCategoryHidden =
|
||||||
if (!containerRef.current || !measureRef.current || !viewAllRef.current)
|
activeCategoryIndex >= visibleCount && activeCategoryIndex !== -1;
|
||||||
return;
|
|
||||||
|
useEffect(() => {
|
||||||
const containerWidth = containerRef.current.offsetWidth;
|
const calculateVisible = () => {
|
||||||
const viewAllWidth = viewAllRef.current.offsetWidth;
|
if (!containerRef.current || !measureRef.current || !viewAllRef.current)
|
||||||
const availableWidth = containerWidth - viewAllWidth;
|
return;
|
||||||
|
|
||||||
const items = Array.from(measureRef.current.children);
|
const containerWidth = containerRef.current.offsetWidth;
|
||||||
let totalWidth = 0;
|
const viewAllWidth = viewAllRef.current.offsetWidth;
|
||||||
let visible = 0;
|
const availableWidth = containerWidth - viewAllWidth;
|
||||||
|
|
||||||
for (const item of items) {
|
const items = Array.from(measureRef.current.children);
|
||||||
const width = item.getBoundingClientRect().width;
|
let totalWidth = 0;
|
||||||
|
let visible = 0;
|
||||||
if (totalWidth + width > availableWidth) break;
|
|
||||||
|
for (const item of items) {
|
||||||
totalWidth += width;
|
const width = item.getBoundingClientRect().width;
|
||||||
visible++;
|
|
||||||
}
|
if (totalWidth + width > availableWidth) break;
|
||||||
|
|
||||||
setVisibleCount(visible);
|
totalWidth += width;
|
||||||
};
|
visible++;
|
||||||
|
}
|
||||||
const resizeObserver = new ResizeObserver(calculateVisible);
|
|
||||||
|
setVisibleCount(visible);
|
||||||
resizeObserver.observe(containerRef.current!);
|
};
|
||||||
|
|
||||||
return () => resizeObserver.disconnect();
|
const resizeObserver = new ResizeObserver(calculateVisible);
|
||||||
}, [data.length]);
|
|
||||||
|
resizeObserver.observe(containerRef.current!);
|
||||||
return (
|
|
||||||
<div className="relative w-full">
|
return () => resizeObserver.disconnect();
|
||||||
{/* Categories sidebar */}
|
}, [data.length]);
|
||||||
<CategoriesSidebar
|
|
||||||
open={isSidebarOpen}
|
return (
|
||||||
onOpenChange={setIsSidebarOpen}
|
<div className="relative w-full">
|
||||||
/>
|
{/* Categories sidebar */}
|
||||||
|
<CategoriesSidebar
|
||||||
{/* Hidden div to measure all items */}
|
open={isSidebarOpen}
|
||||||
<div
|
onOpenChange={setIsSidebarOpen}
|
||||||
ref={measureRef}
|
/>
|
||||||
className="absolute opacity-0 pointer-events-none flex"
|
|
||||||
style={{ position: 'fixed', top: -9999, left: -9999 }}
|
{/* Hidden div to measure all items */}
|
||||||
>
|
<div
|
||||||
{data.map((category) => (
|
ref={measureRef}
|
||||||
<div key={category.id}>
|
className="absolute opacity-0 pointer-events-none flex"
|
||||||
<CategoryDropdown
|
style={{ position: 'fixed', top: -9999, left: -9999 }}
|
||||||
category={category}
|
>
|
||||||
isActive={activeCategory === category.slug}
|
{data.map((category) => (
|
||||||
isNavigationHovered={false}
|
<div key={category.id}>
|
||||||
/>
|
<CategoryDropdown
|
||||||
</div>
|
category={category}
|
||||||
))}
|
isActive={activeCategory === category.slug}
|
||||||
</div>
|
isNavigationHovered={false}
|
||||||
|
/>
|
||||||
{/* Visible items */}
|
</div>
|
||||||
<div
|
))}
|
||||||
ref={containerRef}
|
</div>
|
||||||
className="flex flex-nowrap items-center"
|
|
||||||
onMouseEnter={() => setIsAnyHovered(true)}
|
{/* Visible items */}
|
||||||
onMouseLeave={() => setIsAnyHovered(false)}
|
<div
|
||||||
>
|
ref={containerRef}
|
||||||
{data.slice(0, visibleCount).map((category) => (
|
className="flex flex-nowrap items-center"
|
||||||
<div key={category.id}>
|
onMouseEnter={() => setIsAnyHovered(true)}
|
||||||
<CategoryDropdown
|
onMouseLeave={() => setIsAnyHovered(false)}
|
||||||
category={category}
|
>
|
||||||
isActive={activeCategory === category.slug}
|
{data.slice(0, visibleCount).map((category) => (
|
||||||
isNavigationHovered={isAnyHovered}
|
<div key={category.id}>
|
||||||
/>
|
<CategoryDropdown
|
||||||
</div>
|
category={category}
|
||||||
))}
|
isActive={activeCategory === category.slug}
|
||||||
|
isNavigationHovered={isAnyHovered}
|
||||||
<div ref={viewAllRef} className="shrink-0">
|
/>
|
||||||
<Button
|
</div>
|
||||||
className={cn(
|
))}
|
||||||
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
|
|
||||||
isActiveCategoryHidden &&
|
<div ref={viewAllRef} className="shrink-0">
|
||||||
!isAnyHovered &&
|
<Button
|
||||||
'bg-white border-primary'
|
variant="elevated"
|
||||||
)}
|
className={cn(
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
|
||||||
>
|
isActiveCategoryHidden &&
|
||||||
View All
|
!isAnyHovered &&
|
||||||
<ListFilterIcon className="ml-2" />
|
'bg-white border-primary'
|
||||||
</Button>
|
)}
|
||||||
</div>
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
</div>
|
>
|
||||||
</div>
|
View All
|
||||||
);
|
<ListFilterIcon className="ml-2" />
|
||||||
};
|
</Button>
|
||||||
|
</div>
|
||||||
export default Categories;
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Categories;
|
||||||
+86
-86
@@ -1,86 +1,86 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import useDropdownPosition from './use-dropdown-position';
|
import useDropdownPosition from './use-dropdown-position';
|
||||||
import SubCategoryMenu from './subcategory-menu';
|
import SubCategoryMenu from './subcategory-menu';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||||
|
|
||||||
interface CategoryDropdownProps {
|
interface CategoryDropdownProps {
|
||||||
category: CategoriesGetManyOutput[1];
|
category: CategoriesGetManyOutput[1];
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isNavigationHovered?: boolean;
|
isNavigationHovered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategoryDropdown = ({
|
const CategoryDropdown = ({
|
||||||
category,
|
category,
|
||||||
isActive,
|
isActive,
|
||||||
isNavigationHovered,
|
isNavigationHovered,
|
||||||
}: CategoryDropdownProps) => {
|
}: CategoryDropdownProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const { getDropdownPosition } = useDropdownPosition(dropdownRef);
|
const { getDropdownPosition } = useDropdownPosition(dropdownRef);
|
||||||
const dropdownPosition = getDropdownPosition();
|
const dropdownPosition = getDropdownPosition();
|
||||||
|
|
||||||
// TODO: Will improve in mobile screen
|
// TODO: Will improve in mobile screen
|
||||||
// const toggleDropdown = () => {
|
// const toggleDropdown = () => {
|
||||||
// if (category.subcategories?.docs?.length) {
|
// if (category.subcategories?.docs?.length) {
|
||||||
// setIsOpen(!isOpen);
|
// setIsOpen(!isOpen);
|
||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (category.subcategories) {
|
if (category.subcategories) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => setIsOpen(false);
|
const handleMouseLeave = () => setIsOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
// onClick={toggleDropdown}
|
// onClick={toggleDropdown}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Button
|
<Button
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
|
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
|
||||||
isActive && !isNavigationHovered && 'bg-white border-primary',
|
isActive && !isNavigationHovered && 'bg-white border-primary',
|
||||||
isOpen &&
|
isOpen &&
|
||||||
'bg-white border-primary shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-[4px] -translate-y-[4px]'
|
'bg-white border-primary shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-[4px] -translate-y-[4px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={`/${category.slug === 'all' ? '' : category.slug}`}>
|
<Link href={`/${category.slug === 'all' ? '' : category.slug}`}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{category.subcategories && category.subcategories.length > 0 && (
|
{category.subcategories && category.subcategories.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'opacity-0 absolute -bottom-3 w-0 h-0 border-l-[10px] border-r-[10px] border-b-[10px] border-l-transparent border-r-transparent border-b-black left-1/2 -translate-x-1/2',
|
'opacity-0 absolute -bottom-3 w-0 h-0 border-l-[10px] border-r-[10px] border-b-[10px] border-l-transparent border-r-transparent border-b-black left-1/2 -translate-x-1/2',
|
||||||
isOpen && 'opacity-100'
|
isOpen && 'opacity-100'
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubCategoryMenu
|
<SubCategoryMenu
|
||||||
category={category}
|
category={category}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
position={dropdownPosition}
|
position={dropdownPosition}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CategoryDropdown;
|
export default CategoryDropdown;
|
||||||
@@ -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;
|
||||||
+63
-63
@@ -1,63 +1,63 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { BookmarkCheckIcon, ListFilterIcon, SearchIcon } from 'lucide-react';
|
import { BookmarkCheckIcon, ListFilterIcon, SearchIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useTRPC } from '@/trpc/client';
|
import { useTRPC } from '@/trpc/client';
|
||||||
|
|
||||||
import CategoriesSidebar from './categories-sidebar';
|
import CategoriesSidebar from './categories-sidebar';
|
||||||
|
|
||||||
interface SearchInputProps {
|
interface SearchInputProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchInput = ({ disabled }: SearchInputProps) => {
|
const SearchInput = ({ disabled }: SearchInputProps) => {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
const session = useQuery(trpc.auth.session.queryOptions());
|
const session = useQuery(trpc.auth.session.queryOptions());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<CategoriesSidebar
|
<CategoriesSidebar
|
||||||
open={isSidebarOpen}
|
open={isSidebarOpen}
|
||||||
onOpenChange={setIsSidebarOpen}
|
onOpenChange={setIsSidebarOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-neutral-500" />
|
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-neutral-500" />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
placeholder="Search products"
|
placeholder="Search products"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
className="size-12 shrink-0 flex lg:hidden"
|
className="size-12 shrink-0 flex lg:hidden"
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
>
|
>
|
||||||
<ListFilterIcon />
|
<ListFilterIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{session.data?.user && (
|
{session.data?.user && (
|
||||||
<Button asChild variant="elevated">
|
<Button asChild variant="elevated">
|
||||||
<Link href="/library">
|
<Link href="/library">
|
||||||
<BookmarkCheckIcon />
|
<BookmarkCheckIcon />
|
||||||
Library
|
Library
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchInput;
|
export default SearchInput;
|
||||||
+60
-60
@@ -1,60 +1,60 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Category } from '@/payload-types';
|
import { Category } from '@/payload-types';
|
||||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||||
|
|
||||||
interface SubCategoryMenuProps {
|
interface SubCategoryMenuProps {
|
||||||
category: CategoriesGetManyOutput[1];
|
category: CategoriesGetManyOutput[1];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
position: {
|
position: {
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SubCategoryMenu = ({
|
const SubCategoryMenu = ({
|
||||||
category,
|
category,
|
||||||
isOpen,
|
isOpen,
|
||||||
position,
|
position,
|
||||||
}: SubCategoryMenuProps) => {
|
}: SubCategoryMenuProps) => {
|
||||||
if (
|
if (
|
||||||
!isOpen ||
|
!isOpen ||
|
||||||
!category.subcategories ||
|
!category.subcategories ||
|
||||||
category.subcategories.length === 0
|
category.subcategories.length === 0
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const backgroundColor = category.color || '#F5F5F5';
|
const backgroundColor = category.color || '#F5F5F5';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed z-100"
|
className="fixed z-100"
|
||||||
style={{
|
style={{
|
||||||
top: position?.top,
|
top: position?.top,
|
||||||
left: position?.left,
|
left: position?.left,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Invisible bridge to maintain hover */}
|
{/* Invisible bridge to maintain hover */}
|
||||||
<div className="h-3 w-60" />
|
<div className="h-3 w-60" />
|
||||||
<div
|
<div
|
||||||
className="w-60 text-black rounded-md overflow-hidden border shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-[2px] -translate-y-[2px]"
|
className="w-60 text-black rounded-md overflow-hidden border shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-[2px] -translate-y-[2px]"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{category.subcategories.map((subcategory: Category) => (
|
{category.subcategories.map((subcategory: Category) => (
|
||||||
<Link
|
<Link
|
||||||
key={subcategory.id}
|
key={subcategory.id}
|
||||||
href={`/${category.slug}/${subcategory.slug}`}
|
href={`/${category.slug}/${subcategory.slug}`}
|
||||||
className="w-full text-left p-4 hover:bg-black hover:text-white flex justify-between items-center underline font-medium"
|
className="w-full text-left p-4 hover:bg-black hover:text-white flex justify-between items-center underline font-medium"
|
||||||
>
|
>
|
||||||
{subcategory.name}
|
{subcategory.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SubCategoryMenu;
|
export default SubCategoryMenu;
|
||||||
+35
-35
@@ -1,36 +1,36 @@
|
|||||||
import { RefObject } from "react";
|
import { RefObject } from "react";
|
||||||
|
|
||||||
const useDropdownPosition = (ref: RefObject<HTMLDivElement | null> | RefObject<HTMLDivElement>) => {
|
const useDropdownPosition = (ref: RefObject<HTMLDivElement | null> | RefObject<HTMLDivElement>) => {
|
||||||
const getDropdownPosition = () => {
|
const getDropdownPosition = () => {
|
||||||
if (!ref.current) return { top: 0, left: 0 };
|
if (!ref.current) return { top: 0, left: 0 };
|
||||||
|
|
||||||
const rect = ref.current.getBoundingClientRect();
|
const rect = ref.current.getBoundingClientRect();
|
||||||
const dropdownWidth = 240; // Width of dropdown (w-60 = 15rem = 240px)
|
const dropdownWidth = 240; // Width of dropdown (w-60 = 15rem = 240px)
|
||||||
|
|
||||||
// Calculate the initial position
|
// Calculate the initial position
|
||||||
let left = rect.left + window.scrollX;
|
let left = rect.left + window.scrollX;
|
||||||
const top = rect.bottom + window.scrollY;
|
const top = rect.bottom + window.scrollY;
|
||||||
|
|
||||||
// Check if dropdown would go off the right edge of the viewport
|
// Check if dropdown would go off the right edge of the viewport
|
||||||
if (left + dropdownWidth > window.innerWidth) {
|
if (left + dropdownWidth > window.innerWidth) {
|
||||||
left = rect.right + window.scrollX - dropdownWidth;
|
left = rect.right + window.scrollX - dropdownWidth;
|
||||||
|
|
||||||
// If still off-screen, align to the left edge of the viewport with some padding
|
// If still off-screen, align to the left edge of the viewport with some padding
|
||||||
if (left < 0) {
|
if (left < 0) {
|
||||||
left = window.innerWidth - dropdownWidth - 16; // 16px padding
|
left = window.innerWidth - dropdownWidth - 16; // 16px padding
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left < 0) {
|
if (left < 0) {
|
||||||
left = 16
|
left = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
return { top, left };
|
return { top, left };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { top, left };
|
return { top, left };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { getDropdownPosition }
|
return { getDropdownPosition }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useDropdownPosition;
|
export default useDropdownPosition;
|
||||||
Reference in New Issue
Block a user