diff --git a/package.json b/package.json
index c360508..2a266d0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "generate:types": "payload generate:types"
+ "generate:types": "payload generate:types",
+ "db:fresh": "payload migrate:fresh"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
diff --git a/src/app/(app)/(home)/layout.tsx b/src/app/(app)/(home)/layout.tsx
index 6fa6f14..f815a20 100644
--- a/src/app/(app)/(home)/layout.tsx
+++ b/src/app/(app)/(home)/layout.tsx
@@ -1,17 +1,45 @@
import { ReactNode } from 'react';
+import configPromise from '@payload-config';
+import { getPayload } from 'payload';
import Navbar from './navbar';
import Footer from './footer';
+import SearchFilters from './search-filters';
+import { Category } from '@/payload-types';
interface LayoutProps {
children: ReactNode;
}
-const Layout = ({ children }: LayoutProps) => {
+const Layout = async ({ children }: LayoutProps) => {
+ const payload = await getPayload({
+ config: configPromise,
+ });
+
+ const data = await payload.find({
+ collection: 'categories',
+ depth: 1,
+ where: {
+ parent: {
+ exists: false,
+ },
+ },
+ });
+
+ const formattedData = data.docs.map((category) => ({
+ ...category,
+ subcategories: (category.subcategories?.docs ?? []).map((doc) => ({
+ ...(doc as Category),
+ subcategories: undefined,
+ })),
+ }));
+
return (
+
+
{children}
diff --git a/src/app/(app)/(home)/page.tsx b/src/app/(app)/(home)/page.tsx
index 23977fd..d2fdc12 100644
--- a/src/app/(app)/(home)/page.tsx
+++ b/src/app/(app)/(home)/page.tsx
@@ -1,12 +1,3 @@
-import configPromise from '@payload-config';
-import { getPayload } from 'payload';
-
-export default async function Home() {
- const payload = await getPayload({
- config: configPromise,
- });
-
- const data = await payload.find({ collection: 'categories' });
-
- return
{JSON.stringify(data, null, 2)}
;
+export default function Home() {
+ return
Home page
;
}
diff --git a/src/app/(app)/(home)/search-filters/categories.tsx b/src/app/(app)/(home)/search-filters/categories.tsx
new file mode 100644
index 0000000..041f9c3
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/categories.tsx
@@ -0,0 +1,26 @@
+import { Category } from '@/payload-types';
+import CategoryDropdown from './category-dropdown';
+
+interface CategoriesProps {
+ data: any;
+}
+
+const Categories = ({ data }: CategoriesProps) => {
+ return (
+
+
+ {data.map((category: Category) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+export default Categories;
diff --git a/src/app/(app)/(home)/search-filters/category-dropdown.tsx b/src/app/(app)/(home)/search-filters/category-dropdown.tsx
new file mode 100644
index 0000000..a1cd9d4
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/category-dropdown.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+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';
+
+interface CategoryDropdownProps {
+ category: Category;
+ isActive?: boolean;
+ isNavigationHovered?: boolean;
+}
+
+const CategoryDropdown = ({
+ category,
+ isActive,
+ isNavigationHovered,
+}: CategoryDropdownProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef
(null);
+ const { getDropdownPosition } = useDropdownPosition(dropdownRef);
+ const dropdownPosition = getDropdownPosition();
+
+ const handleMouseEnter = () => {
+ if (category.subcategories) {
+ setIsOpen(true);
+ }
+ };
+
+ const handleMouseLeave = () => setIsOpen(false);
+
+ return (
+
+
+
+
+ {category.subcategories && category.subcategories.length > 0 && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default CategoryDropdown;
diff --git a/src/app/(app)/(home)/search-filters/index.tsx b/src/app/(app)/(home)/search-filters/index.tsx
new file mode 100644
index 0000000..d01bd88
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/index.tsx
@@ -0,0 +1,18 @@
+import Categories from './categories';
+import SearchInput from './search-input';
+
+interface SearchFiltersProps {
+ data: any;
+}
+
+const SearchFilters = ({ data }: SearchFiltersProps) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default SearchFilters;
diff --git a/src/app/(app)/(home)/search-filters/search-input.tsx b/src/app/(app)/(home)/search-filters/search-input.tsx
new file mode 100644
index 0000000..66f8c77
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/search-input.tsx
@@ -0,0 +1,22 @@
+import { Input } from '@/components/ui/input';
+import { SearchIcon } from 'lucide-react';
+
+interface SearchInputProps {
+ disabled?: boolean;
+}
+
+const SearchInput = ({ disabled }: SearchInputProps) => {
+ return (
+
+ );
+};
+
+export default SearchInput;
diff --git a/src/app/(app)/(home)/search-filters/subcategory-menu.tsx b/src/app/(app)/(home)/search-filters/subcategory-menu.tsx
new file mode 100644
index 0000000..c6bccb8
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/subcategory-menu.tsx
@@ -0,0 +1,58 @@
+import { Category } from '@/payload-types';
+import Link from 'next/link';
+
+interface SubCategoryMenuProps {
+ category: Category;
+ isOpen: boolean;
+ position: {
+ top: number;
+ left: number;
+ };
+}
+
+const SubCategoryMenu = ({
+ category,
+ isOpen,
+ position,
+}: SubCategoryMenuProps) => {
+ if (
+ !isOpen ||
+ !category.subcategories ||
+ category.subcategories.length === 0
+ ) {
+ return null;
+ }
+
+ const backgroundColor = category.color || '#F5F5F5';
+
+ return (
+
+ {/* Invisible bridge to maintain hover */}
+
+
+
+ {category.subcategories.map((subcategory: Category) => (
+
+ {subcategory.name}
+
+ ))}
+
+
+
+ );
+};
+
+export default SubCategoryMenu;
diff --git a/src/app/(app)/(home)/search-filters/use-dropdown-position.ts b/src/app/(app)/(home)/search-filters/use-dropdown-position.ts
new file mode 100644
index 0000000..9746ae7
--- /dev/null
+++ b/src/app/(app)/(home)/search-filters/use-dropdown-position.ts
@@ -0,0 +1,34 @@
+import { RefObject } from "react";
+
+const useDropdownPosition = (ref: RefObject | RefObject) => {
+ const getDropdownPosition = () => {
+ if (!ref.current) return;
+
+ const rect = ref.current.getBoundingClientRect();
+ const dropdownWidth = 240; // Width of dropdown (w-60 = 15rem = 240px)
+
+ // Calculate the initial position
+ let left = rect.left + window.scrollX;
+ const top = rect.bottom + window.scrollY;
+
+ // Check if dropdown would go off the right edge of the viewport
+ if (left + dropdownWidth > window.innerWidth) {
+ left = rect.right + window.scrollX - dropdownWidth;
+
+ // If still off-screen, align to the left edge of the viewport with some padding
+ if (left < 0) {
+ left = window.innerWidth - dropdownWidth - 16; // 16px padding
+ }
+
+ if (left < 0) {
+ left = 16
+ }
+
+ return { top, left };
+ }
+ }
+
+ return { getDropdownPosition }
+}
+
+export default useDropdownPosition;
\ No newline at end of file
diff --git a/src/collections/Categories.ts b/src/collections/Categories.ts
index 1d89284..92a8839 100644
--- a/src/collections/Categories.ts
+++ b/src/collections/Categories.ts
@@ -7,6 +7,30 @@ export const Categories: CollectionConfig = {
name: 'name',
type: 'text',
required: true,
+ },
+ {
+ name: 'slug',
+ type: 'text',
+ required: true,
+ unique: true,
+ index: true,
+ },
+ {
+ name: 'color',
+ type: 'text',
+ },
+ {
+ name: 'parent',
+ type: 'relationship',
+ relationTo: 'categories',
+ hasMany: false,
+ },
+ {
+ name: 'subcategories',
+ type: 'join',
+ collection: 'categories',
+ on: 'parent',
+ hasMany: true,
}
]
}
\ No newline at end of file
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 362dd71..2af02a9 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -74,7 +74,11 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
- collectionsJoins: {};
+ collectionsJoins: {
+ categories: {
+ subcategories: 'categories';
+ };
+ };
collectionsSelect: {
users: UsersSelect | UsersSelect;
media: MediaSelect | MediaSelect;
@@ -158,6 +162,14 @@ export interface Media {
export interface Category {
id: string;
name: string;
+ slug: string;
+ color?: string | null;
+ parent?: (string | null) | Category;
+ subcategories?: {
+ docs?: (string | Category)[];
+ hasNextPage?: boolean;
+ totalDocs?: number;
+ };
updatedAt: string;
createdAt: string;
}
@@ -261,6 +273,10 @@ export interface MediaSelect {
*/
export interface CategoriesSelect {
name?: T;
+ slug?: T;
+ color?: T;
+ parent?: T;
+ subcategories?: T;
updatedAt?: T;
createdAt?: T;
}