mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 13:38:34 +00:00
feat: complete categories feature
This commit is contained in:
+78
-77
@@ -1,79 +1,80 @@
|
||||
{
|
||||
"name": "multitenant-ecommerce",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"generate:types": "payload generate:types",
|
||||
"db:fresh": "payload migrate:fresh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@payloadcms/db-mongodb": "^3.36.0",
|
||||
"@payloadcms/next": "^3.36.0",
|
||||
"@payloadcms/payload-cloud": "^3.36.0",
|
||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.8",
|
||||
"@radix-ui/react-context-menu": "^2.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.11",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-menubar": "^1.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-popover": "^1.1.11",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.4",
|
||||
"@radix-ui/react-scroll-area": "^1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slider": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.9",
|
||||
"@radix-ui/react-toggle": "^1.1.6",
|
||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"graphql": "^16.8.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"payload": "^3.36.0",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"recharts": "^2.15.3",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tw-animate-css": "1.2.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.2.4",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
"name": "multitenant-ecommerce",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"generate:types": "payload generate:types",
|
||||
"db:fresh": "payload migrate:fresh",
|
||||
"db:seed": "bun run src/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@payloadcms/db-mongodb": "^3.36.0",
|
||||
"@payloadcms/next": "^3.36.0",
|
||||
"@payloadcms/payload-cloud": "^3.36.0",
|
||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.4",
|
||||
"@radix-ui/react-avatar": "^1.1.7",
|
||||
"@radix-ui/react-checkbox": "^1.2.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.8",
|
||||
"@radix-ui/react-context-menu": "^2.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
"@radix-ui/react-hover-card": "^1.1.11",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-menubar": "^1.1.12",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
||||
"@radix-ui/react-popover": "^1.1.11",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.4",
|
||||
"@radix-ui/react-scroll-area": "^1.2.6",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slider": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.9",
|
||||
"@radix-ui/react-toggle": "^1.1.6",
|
||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"graphql": "^16.8.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"payload": "^3.36.0",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"recharts": "^2.15.3",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tw-animate-css": "1.2.5",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.2.4",
|
||||
"@eslint/eslintrc": "^3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Navbar from './navbar';
|
||||
import Footer from './footer';
|
||||
import SearchFilters from './search-filters';
|
||||
import { Category } from '@/payload-types';
|
||||
import { CustomCategory } from './type';
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -24,9 +25,10 @@ const Layout = async ({ children }: LayoutProps) => {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
sort: 'name',
|
||||
});
|
||||
|
||||
const formattedData = data.docs.map((category) => ({
|
||||
const formattedData: CustomCategory[] = data.docs.map((category) => ({
|
||||
...category,
|
||||
subcategories: (category.subcategories?.docs ?? []).map((doc) => ({
|
||||
...(doc as Category),
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { CustomCategory } from '../type';
|
||||
import { useState } from 'react';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface CategoriesSidebarProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
data: CustomCategory[];
|
||||
}
|
||||
|
||||
const CategoriesSidebar = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
data,
|
||||
}: CategoriesSidebarProps) => {
|
||||
const router = useRouter();
|
||||
const [parentCategories, setParentCategories] = useState<
|
||||
CustomCategory[] | null
|
||||
>(null);
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<CustomCategory | null>(null);
|
||||
|
||||
// If we have parent categories, show those, otherwise show root categories
|
||||
const currentCategories = parentCategories ?? data ?? [];
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setSelectedCategory(null);
|
||||
setParentCategories(null);
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
const handleCategoryClick = (category: CustomCategory) => {
|
||||
if (category.subcategories && category.subcategories.length > 0) {
|
||||
setParentCategories(category.subcategories as CustomCategory[]);
|
||||
setSelectedCategory(category);
|
||||
} else {
|
||||
// This is a leaf category (no subcategories)
|
||||
if (parentCategories && selectedCategory) {
|
||||
router.push(`/${selectedCategory.slug}/${category.slug}`);
|
||||
} else {
|
||||
if (category.slug === 'all') {
|
||||
router.push('/');
|
||||
} else {
|
||||
router.push(`/${category.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (parentCategories) {
|
||||
setParentCategories(null);
|
||||
setSelectedCategory(null);
|
||||
}
|
||||
};
|
||||
|
||||
const backgroundColor = selectedCategory?.color || 'white';
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={handleOpenChange}>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="p-0 transition-none"
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<SheetHeader className="p-4 border-b">
|
||||
<SheetTitle>Categories</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="flex flex-col overflow-y-auto h-full pb-2">
|
||||
{parentCategories && (
|
||||
<button
|
||||
onClick={handleBackClick}
|
||||
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" />
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
|
||||
{currentCategories.map((category) => (
|
||||
<button
|
||||
key={category.slug}
|
||||
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"
|
||||
>
|
||||
{category.name}
|
||||
{category.subcategories && category.subcategories.length > 0 && (
|
||||
<ChevronRightIcon className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesSidebar;
|
||||
@@ -1,24 +1,126 @@
|
||||
import { Category } from '@/payload-types';
|
||||
'use client';
|
||||
|
||||
import { ListFilterIcon } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import CategoryDropdown from './category-dropdown';
|
||||
|
||||
import { CustomCategory } from '../type';
|
||||
import CategoriesSidebar from './categories-sidebar';
|
||||
|
||||
interface CategoriesProps {
|
||||
data: any;
|
||||
data: CustomCategory[];
|
||||
}
|
||||
|
||||
const Categories = ({ data }: CategoriesProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const viewAllRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [visibleCount, setVisibleCount] = useState(data.length);
|
||||
const [isAnyHovered, setIsAnyHovered] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const activeCategory = 'all';
|
||||
|
||||
const activeCategoryIndex = data.findIndex(
|
||||
(cat) => cat.slug === activeCategory
|
||||
);
|
||||
const isActiveCategoryHidden =
|
||||
activeCategoryIndex >= visibleCount && activeCategoryIndex !== -1;
|
||||
|
||||
useEffect(() => {
|
||||
const calculateVisible = () => {
|
||||
if (!containerRef.current || !measureRef.current || !viewAllRef.current)
|
||||
return;
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const viewAllWidth = viewAllRef.current.offsetWidth;
|
||||
const availableWidth = containerWidth - viewAllWidth;
|
||||
|
||||
const items = Array.from(measureRef.current.children);
|
||||
let totalWidth = 0;
|
||||
let visible = 0;
|
||||
|
||||
for (const item of items) {
|
||||
const width = item.getBoundingClientRect().width;
|
||||
|
||||
if (totalWidth + width > availableWidth) break;
|
||||
|
||||
totalWidth += width;
|
||||
visible++;
|
||||
}
|
||||
|
||||
setVisibleCount(visible);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(calculateVisible);
|
||||
|
||||
resizeObserver.observe(containerRef.current!);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [data.length]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div className="flex flex-nowrap items-center">
|
||||
{data.map((category: Category) => (
|
||||
{/* Categories sidebar */}
|
||||
<CategoriesSidebar
|
||||
open={isSidebarOpen}
|
||||
onOpenChange={setIsSidebarOpen}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
{/* Hidden div to measure all items */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="absolute opacity-0 pointer-events-none flex"
|
||||
style={{ position: 'fixed', top: -9999, left: -9999 }}
|
||||
>
|
||||
{data.map((category) => (
|
||||
<div key={category.id}>
|
||||
<CategoryDropdown
|
||||
category={category}
|
||||
isActive={false}
|
||||
isActive={activeCategory === category.slug}
|
||||
isNavigationHovered={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Visible items */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-nowrap items-center"
|
||||
onMouseEnter={() => setIsAnyHovered(true)}
|
||||
onMouseLeave={() => setIsAnyHovered(false)}
|
||||
>
|
||||
{data.slice(0, visibleCount).map((category) => (
|
||||
<div key={category.id}>
|
||||
<CategoryDropdown
|
||||
category={category}
|
||||
isActive={activeCategory === category.slug}
|
||||
isNavigationHovered={isAnyHovered}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div ref={viewAllRef} className="shrink-0">
|
||||
<Button
|
||||
className={cn(
|
||||
'h-11 px-4 bg-transparent border-transparent rounded-full hover:bg-white hover:border-primary text-black',
|
||||
isActiveCategoryHidden &&
|
||||
!isAnyHovered &&
|
||||
'bg-white border-primary'
|
||||
)}
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
View All
|
||||
<ListFilterIcon className="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Category } from '@/payload-types';
|
||||
import { useRef, useState } from 'react';
|
||||
import useDropdownPosition from './use-dropdown-position';
|
||||
import SubCategoryMenu from './subcategory-menu';
|
||||
|
||||
import { CustomCategory } from '../type';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CategoryDropdownProps {
|
||||
category: Category;
|
||||
category: CustomCategory;
|
||||
isActive?: boolean;
|
||||
isNavigationHovered?: boolean;
|
||||
}
|
||||
@@ -21,7 +24,14 @@ const CategoryDropdown = ({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { getDropdownPosition } = useDropdownPosition(dropdownRef);
|
||||
const dropdownPosition = getDropdownPosition();
|
||||
const dropdownPosition = getDropdownPosition();
|
||||
|
||||
// TODO: Will improve in mobile screen
|
||||
// const toggleDropdown = () => {
|
||||
// if (category.subcategories?.docs?.length) {
|
||||
// setIsOpen(!isOpen);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (category.subcategories) {
|
||||
@@ -37,16 +47,21 @@ const CategoryDropdown = ({
|
||||
ref={dropdownRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
// onClick={toggleDropdown}
|
||||
>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="elevated"
|
||||
className={cn(
|
||||
'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 &&
|
||||
'bg-white border-primary shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] -translate-x-[4px] -translate-y-[4px]'
|
||||
)}
|
||||
>
|
||||
{category.name}
|
||||
<Link href={`/${category.slug === 'all' ? '' : category.slug}`}>
|
||||
{category.name}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{category.subcategories && category.subcategories.length > 0 && (
|
||||
@@ -59,11 +74,11 @@ const CategoryDropdown = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SubCategoryMenu
|
||||
category={category}
|
||||
isOpen={isOpen}
|
||||
position={dropdownPosition}
|
||||
/>
|
||||
<SubCategoryMenu
|
||||
category={category}
|
||||
isOpen={isOpen}
|
||||
position={dropdownPosition}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { CustomCategory } from '../type';
|
||||
import Categories from './categories';
|
||||
import SearchInput from './search-input';
|
||||
|
||||
interface SearchFiltersProps {
|
||||
data: any;
|
||||
data: CustomCategory[];
|
||||
}
|
||||
|
||||
const SearchFilters = ({ data }: SearchFiltersProps) => {
|
||||
return (
|
||||
<div className="px-4 lg:px-12 py-8 border-b flex flex-col gap-4 w-full">
|
||||
<SearchInput />
|
||||
<SearchInput data={data} />
|
||||
|
||||
<Categories data={data} />
|
||||
<div className="hidden lg:block">
|
||||
<Categories data={data} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ListFilterIcon, SearchIcon } from 'lucide-react';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SearchIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import CategoriesSidebar from './categories-sidebar';
|
||||
|
||||
import { CustomCategory } from '../type';
|
||||
|
||||
interface SearchInputProps {
|
||||
disabled?: boolean;
|
||||
data: CustomCategory[];
|
||||
}
|
||||
|
||||
const SearchInput = ({ disabled }: SearchInputProps) => {
|
||||
const SearchInput = ({ disabled, data }: SearchInputProps) => {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className='relative w-full'>
|
||||
<SearchIcon className='absolute left-3 top-1/2 -translate-y-1/2 size-4 text-neutral-500' />
|
||||
<CategoriesSidebar
|
||||
data={data}
|
||||
open={isSidebarOpen}
|
||||
onOpenChange={setIsSidebarOpen}
|
||||
/>
|
||||
|
||||
<Input className='pl-8' placeholder='Search products' disabled={disabled} />
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-neutral-500" />
|
||||
|
||||
<Input
|
||||
className="pl-8"
|
||||
placeholder="Search products"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="elevated"
|
||||
className="size-12 shrink-0 flex lg:hidden"
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<ListFilterIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Category } from '@/payload-types';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { CustomCategory } from '../type';
|
||||
|
||||
interface SubCategoryMenuProps {
|
||||
category: Category;
|
||||
category: CustomCategory;
|
||||
isOpen: boolean;
|
||||
position: {
|
||||
top: number;
|
||||
@@ -43,7 +45,7 @@ const SubCategoryMenu = ({
|
||||
{category.subcategories.map((subcategory: Category) => (
|
||||
<Link
|
||||
key={subcategory.id}
|
||||
href="/"
|
||||
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"
|
||||
>
|
||||
{subcategory.name}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RefObject } from "react";
|
||||
|
||||
const useDropdownPosition = (ref: RefObject<HTMLDivElement | null> | RefObject<HTMLDivElement>) => {
|
||||
const getDropdownPosition = () => {
|
||||
if (!ref.current) return;
|
||||
if (!ref.current) return { top: 0, left: 0 };
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const dropdownWidth = 240; // Width of dropdown (w-60 = 15rem = 240px)
|
||||
@@ -26,6 +26,8 @@ const useDropdownPosition = (ref: RefObject<HTMLDivElement | null> | RefObject<H
|
||||
|
||||
return { top, left };
|
||||
}
|
||||
|
||||
return { top, left };
|
||||
}
|
||||
|
||||
return { getDropdownPosition }
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Category } from "@/payload-types";
|
||||
|
||||
export type CustomCategory = Category & {
|
||||
subcategories: Category[];
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from '@payload-config'
|
||||
|
||||
const categories = [
|
||||
{
|
||||
name: "All",
|
||||
slug: "all",
|
||||
},
|
||||
{
|
||||
name: "Business & Money",
|
||||
color: "#FFB347",
|
||||
slug: "business-money",
|
||||
subcategories: [
|
||||
{ name: "Accounting", slug: "accounting" },
|
||||
{
|
||||
name: "Entrepreneurship",
|
||||
slug: "entrepreneurship",
|
||||
},
|
||||
{ name: "Gigs & Side Projects", slug: "gigs-side-projects" },
|
||||
{ name: "Investing", slug: "investing" },
|
||||
{ name: "Management & Leadership", slug: "management-leadership" },
|
||||
{
|
||||
name: "Marketing & Sales",
|
||||
slug: "marketing-sales",
|
||||
},
|
||||
{ name: "Networking, Careers & Jobs", slug: "networking-careers-jobs" },
|
||||
{ name: "Personal Finance", slug: "personal-finance" },
|
||||
{ name: "Real Estate", slug: "real-estate" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Software Development",
|
||||
color: "#7EC8E3",
|
||||
slug: "software-development",
|
||||
subcategories: [
|
||||
{ name: "Web Development", slug: "web-development" },
|
||||
{ name: "Mobile Development", slug: "mobile-development" },
|
||||
{ name: "Game Development", slug: "game-development" },
|
||||
{ name: "Programming Languages", slug: "programming-languages" },
|
||||
{ name: "DevOps", slug: "devops" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Writing & Publishing",
|
||||
color: "#D8B5FF",
|
||||
slug: "writing-publishing",
|
||||
subcategories: [
|
||||
{ name: "Fiction", slug: "fiction" },
|
||||
{ name: "Non-Fiction", slug: "non-fiction" },
|
||||
{ name: "Blogging", slug: "blogging" },
|
||||
{ name: "Copywriting", slug: "copywriting" },
|
||||
{ name: "Self-Publishing", slug: "self-publishing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
slug: "other",
|
||||
},
|
||||
{
|
||||
name: "Education",
|
||||
color: "#FFE066",
|
||||
slug: "education",
|
||||
subcategories: [
|
||||
{ name: "Online Courses", slug: "online-courses" },
|
||||
{ name: "Tutoring", slug: "tutoring" },
|
||||
{ name: "Test Preparation", slug: "test-preparation" },
|
||||
{ name: "Language Learning", slug: "language-learning" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Self Improvement",
|
||||
color: "#96E6B3",
|
||||
slug: "self-improvement",
|
||||
subcategories: [
|
||||
{ name: "Productivity", slug: "productivity" },
|
||||
{ name: "Personal Development", slug: "personal-development" },
|
||||
{ name: "Mindfulness", slug: "mindfulness" },
|
||||
{ name: "Career Growth", slug: "career-growth" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Fitness & Health",
|
||||
color: "#FF9AA2",
|
||||
slug: "fitness-health",
|
||||
subcategories: [
|
||||
{ name: "Workout Plans", slug: "workout-plans" },
|
||||
{ name: "Nutrition", slug: "nutrition" },
|
||||
{ name: "Mental Health", slug: "mental-health" },
|
||||
{ name: "Yoga", slug: "yoga" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Design",
|
||||
color: "#B5B9FF",
|
||||
slug: "design",
|
||||
subcategories: [
|
||||
{ name: "UI/UX", slug: "ui-ux" },
|
||||
{ name: "Graphic Design", slug: "graphic-design" },
|
||||
{ name: "3D Modeling", slug: "3d-modeling" },
|
||||
{ name: "Typography", slug: "typography" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Drawing & Painting",
|
||||
color: "#FFCAB0",
|
||||
slug: "drawing-painting",
|
||||
subcategories: [
|
||||
{ name: "Watercolor", slug: "watercolor" },
|
||||
{ name: "Acrylic", slug: "acrylic" },
|
||||
{ name: "Oil", slug: "oil" },
|
||||
{ name: "Pastel", slug: "pastel" },
|
||||
{ name: "Charcoal", slug: "charcoal" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Music",
|
||||
color: "#FFD700",
|
||||
slug: "music",
|
||||
subcategories: [
|
||||
{ name: "Songwriting", slug: "songwriting" },
|
||||
{ name: "Music Production", slug: "music-production" },
|
||||
{ name: "Music Theory", slug: "music-theory" },
|
||||
{ name: "Music History", slug: "music-history" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Photography",
|
||||
color: "#FF6B6B",
|
||||
slug: "photography",
|
||||
subcategories: [
|
||||
{ name: "Portrait", slug: "portrait" },
|
||||
{ name: "Landscape", slug: "landscape" },
|
||||
{ name: "Street Photography", slug: "street-photography" },
|
||||
{ name: "Nature", slug: "nature" },
|
||||
{ name: "Macro", slug: "macro" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const seed = async () => {
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
for (const category of categories) {
|
||||
const { name, slug, color } = category;
|
||||
const categoryDoc = await payload.create({
|
||||
collection: "categories",
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
color,
|
||||
parent: null,
|
||||
},
|
||||
});
|
||||
|
||||
for (const subCategory of category.subcategories || []) {
|
||||
await payload.create({
|
||||
collection: "categories",
|
||||
data: {
|
||||
name: subCategory.name,
|
||||
slug: subCategory.slug,
|
||||
parent: categoryDoc.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await seed();
|
||||
console.log("Seed completed successfully.");
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error seeding the database:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user