From 2522dd65842952d61dd9e70cb5dc64aa64542722 Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Sat, 19 Jul 2025 21:37:09 +0700 Subject: [PATCH] feat: add category pages --- .../(home)/[category]/[subcategory]/page.tsx | 14 + src/app/(app)/(home)/[category]/page.tsx | 13 + src/app/(app)/(home)/layout.tsx | 6 +- src/app/(app)/(home)/search-filters/index.tsx | 37 --- src/modules/home/ui/components/constants.ts | 1 + .../home/ui/components}/footer.tsx | 20 +- .../home/ui/components}/navbar-sidebar.tsx | 130 ++++---- .../home/ui/components}/navbar.tsx | 294 +++++++++--------- .../search-filters/breadcrumb-navigation.tsx | 56 ++++ .../search-filters/categories-sidebar.tsx | 226 +++++++------- .../components}/search-filters/categories.tsx | 259 +++++++-------- .../search-filters/category-dropdown.tsx | 172 +++++----- .../ui/components/search-filters/index.tsx | 64 ++++ .../search-filters/search-input.tsx | 126 ++++---- .../search-filters/subcategory-menu.tsx | 120 +++---- .../search-filters/use-dropdown-position.ts | 70 ++--- 16 files changed, 862 insertions(+), 746 deletions(-) create mode 100644 src/app/(app)/(home)/[category]/[subcategory]/page.tsx create mode 100644 src/app/(app)/(home)/[category]/page.tsx delete mode 100644 src/app/(app)/(home)/search-filters/index.tsx create mode 100644 src/modules/home/ui/components/constants.ts rename src/{app/(app)/(home) => modules/home/ui/components}/footer.tsx (95%) rename src/{app/(app)/(home) => modules/home/ui/components}/navbar-sidebar.tsx (96%) rename src/{app/(app)/(home) => modules/home/ui/components}/navbar.tsx (96%) create mode 100644 src/modules/home/ui/components/search-filters/breadcrumb-navigation.tsx rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/categories-sidebar.tsx (96%) rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/categories.tsx (94%) rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/category-dropdown.tsx (96%) create mode 100644 src/modules/home/ui/components/search-filters/index.tsx rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/search-input.tsx (95%) rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/subcategory-menu.tsx (95%) rename src/{app/(app)/(home) => modules/home/ui/components}/search-filters/use-dropdown-position.ts (96%) diff --git a/src/app/(app)/(home)/[category]/[subcategory]/page.tsx b/src/app/(app)/(home)/[category]/[subcategory]/page.tsx new file mode 100644 index 0000000..d9b637f --- /dev/null +++ b/src/app/(app)/(home)/[category]/[subcategory]/page.tsx @@ -0,0 +1,14 @@ +interface Props { + params: Promise<{ + category: string; + subcategory: string; + }>; +} + +const Page = async ({ params }: Props) => { + const { category, subcategory } = await params; + + return
Subcategory Page: {subcategory} in Category: {category}
; +}; + +export default Page; diff --git a/src/app/(app)/(home)/[category]/page.tsx b/src/app/(app)/(home)/[category]/page.tsx new file mode 100644 index 0000000..ce6694a --- /dev/null +++ b/src/app/(app)/(home)/[category]/page.tsx @@ -0,0 +1,13 @@ +interface Props { + params: Promise<{ + category: string; + }>; +} + +const Page = async ({ params }: Props) => { + const { category } = await params; + + return
Category Page: {category}
; +}; + +export default Page; diff --git a/src/app/(app)/(home)/layout.tsx b/src/app/(app)/(home)/layout.tsx index 4f7f1c6..6f37dda 100644 --- a/src/app/(app)/(home)/layout.tsx +++ b/src/app/(app)/(home)/layout.tsx @@ -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; diff --git a/src/app/(app)/(home)/search-filters/index.tsx b/src/app/(app)/(home)/search-filters/index.tsx deleted file mode 100644 index 7a52ba1..0000000 --- a/src/app/(app)/(home)/search-filters/index.tsx +++ /dev/null @@ -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 ( -
- - -
- -
-
- ); -}; - -export const SearchFiltersSkeleton = () => { - return ( -
- - -
-
-
-
- ); -}; - -export default SearchFilters; diff --git a/src/modules/home/ui/components/constants.ts b/src/modules/home/ui/components/constants.ts new file mode 100644 index 0000000..bf990ce --- /dev/null +++ b/src/modules/home/ui/components/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_BG_COLOR = '#F5F5F5'; \ No newline at end of file diff --git a/src/app/(app)/(home)/footer.tsx b/src/modules/home/ui/components/footer.tsx similarity index 95% rename from src/app/(app)/(home)/footer.tsx rename to src/modules/home/ui/components/footer.tsx index 28c4202..ac38fe8 100644 --- a/src/app/(app)/(home)/footer.tsx +++ b/src/modules/home/ui/components/footer.tsx @@ -1,11 +1,11 @@ -const Footer = () => { - return ( -
-
-

funroad, Inc.

-
-
- ) -} - +const Footer = () => { + return ( +
+
+

funroad, Inc.

+
+
+ ) +} + export default Footer \ No newline at end of file diff --git a/src/app/(app)/(home)/navbar-sidebar.tsx b/src/modules/home/ui/components/navbar-sidebar.tsx similarity index 96% rename from src/app/(app)/(home)/navbar-sidebar.tsx rename to src/modules/home/ui/components/navbar-sidebar.tsx index 2654b22..b6e07de 100644 --- a/src/app/(app)/(home)/navbar-sidebar.tsx +++ b/src/modules/home/ui/components/navbar-sidebar.tsx @@ -1,65 +1,65 @@ -import { ScrollArea } from '@/components/ui/scroll-area'; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet'; -import Link from 'next/link'; -import { ReactNode } from 'react'; - -interface NavbarItem { - href: string; - children: ReactNode; -} - -interface Props { - items: NavbarItem[]; - open: boolean; - onOpenChange: (open: boolean) => void; -} - -const NavbarSidebar = ({ items, open, onOpenChange }: Props) => { - return ( - - - - Menu - - - - {items.map((item) => ( - onOpenChange(false)} - > - {item.children} - - ))} - -
- onOpenChange(false)} - > - Log in - - - onOpenChange(false)} - > - Start Selling - -
-
-
-
- ); -}; - -export default NavbarSidebar; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import Link from 'next/link'; +import { ReactNode } from 'react'; + +interface NavbarItem { + href: string; + children: ReactNode; +} + +interface Props { + items: NavbarItem[]; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const NavbarSidebar = ({ items, open, onOpenChange }: Props) => { + return ( + + + + Menu + + + + {items.map((item) => ( + onOpenChange(false)} + > + {item.children} + + ))} + +
+ onOpenChange(false)} + > + Log in + + + onOpenChange(false)} + > + Start Selling + +
+
+
+
+ ); +}; + +export default NavbarSidebar; diff --git a/src/app/(app)/(home)/navbar.tsx b/src/modules/home/ui/components/navbar.tsx similarity index 96% rename from src/app/(app)/(home)/navbar.tsx rename to src/modules/home/ui/components/navbar.tsx index 0ac73eb..77eb820 100644 --- a/src/app/(app)/(home)/navbar.tsx +++ b/src/modules/home/ui/components/navbar.tsx @@ -1,147 +1,147 @@ -'use client'; - -import { ReactNode, useState } from 'react'; -import Link from 'next/link'; -import { Poppins } from 'next/font/google'; -import { MenuIcon } from 'lucide-react'; -import { usePathname } from 'next/navigation'; -import { useQuery } from '@tanstack/react-query'; - -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { useTRPC } from '@/trpc/client'; - -import NavbarSidebar from './navbar-sidebar'; - -const poppins = Poppins({ - subsets: ['latin'], - weight: ['700'], -}); - -interface NavbarItemProps { - href: string; - children: ReactNode; - isActive?: boolean; -} - -const navbarItems = [ - { - href: '/', - children: 'Home', - }, - { - href: '/about', - children: 'About', - }, - { - href: '/features', - children: 'Features', - }, - { - href: '/pricing', - children: 'Pricing', - }, - { - href: '/contact', - children: 'Contact', - }, -]; - -const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => { - return ( - - ); -}; - -const Navbar = () => { - const pathname = usePathname(); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - const trpc = useTRPC(); - const session = useQuery(trpc.auth.session.queryOptions()); - - return ( - - ); -}; - -export default Navbar; +'use client'; + +import { ReactNode, useState } from 'react'; +import Link from 'next/link'; +import { Poppins } from 'next/font/google'; +import { MenuIcon } from 'lucide-react'; +import { usePathname } from 'next/navigation'; +import { useQuery } from '@tanstack/react-query'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { useTRPC } from '@/trpc/client'; + +import NavbarSidebar from './navbar-sidebar'; + +const poppins = Poppins({ + subsets: ['latin'], + weight: ['700'], +}); + +interface NavbarItemProps { + href: string; + children: ReactNode; + isActive?: boolean; +} + +const navbarItems = [ + { + href: '/', + children: 'Home', + }, + { + href: '/about', + children: 'About', + }, + { + href: '/features', + children: 'Features', + }, + { + href: '/pricing', + children: 'Pricing', + }, + { + href: '/contact', + children: 'Contact', + }, +]; + +const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => { + return ( + + ); +}; + +const Navbar = () => { + const pathname = usePathname(); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const trpc = useTRPC(); + const session = useQuery(trpc.auth.session.queryOptions()); + + return ( + + ); +}; + +export default Navbar; diff --git a/src/modules/home/ui/components/search-filters/breadcrumb-navigation.tsx b/src/modules/home/ui/components/search-filters/breadcrumb-navigation.tsx new file mode 100644 index 0000000..e2ec71e --- /dev/null +++ b/src/modules/home/ui/components/search-filters/breadcrumb-navigation.tsx @@ -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 ( + + + {activeSubCategoryName ? ( + <> + + + {activeCategoryName} + + + + / + + + + {activeSubCategoryName} + + + + ) : ( + + + {activeCategoryName} + + + )} + + + ); +}; diff --git a/src/app/(app)/(home)/search-filters/categories-sidebar.tsx b/src/modules/home/ui/components/search-filters/categories-sidebar.tsx similarity index 96% rename from src/app/(app)/(home)/search-filters/categories-sidebar.tsx rename to src/modules/home/ui/components/search-filters/categories-sidebar.tsx index 408d013..70a7d57 100644 --- a/src/app/(app)/(home)/search-filters/categories-sidebar.tsx +++ b/src/modules/home/ui/components/search-filters/categories-sidebar.tsx @@ -1,113 +1,113 @@ -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; - -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { useTRPC } from '@/trpc/client'; -import { CategoriesGetManyOutput } from '@/modules/categories/type'; - -interface CategoriesSidebarProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -const CategoriesSidebar = ({ - open, - onOpenChange, -}: CategoriesSidebarProps) => { - const trpc = useTRPC(); - const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions()); - - const router = useRouter(); - const [parentCategories, setParentCategories] = useState< - CategoriesGetManyOutput | null - >(null); - const [selectedCategory, setSelectedCategory] = - useState(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: CategoriesGetManyOutput[1]) => { - if (category.subcategories && category.subcategories.length > 0) { - setParentCategories(category.subcategories as CategoriesGetManyOutput); - 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 ( - - - - Categories - - - - {parentCategories && ( - - )} - - {currentCategories.map((category) => ( - - ))} - - - - ); -}; - -export default CategoriesSidebar; +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; + +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { useTRPC } from '@/trpc/client'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; + +interface CategoriesSidebarProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const CategoriesSidebar = ({ + open, + onOpenChange, +}: CategoriesSidebarProps) => { + const trpc = useTRPC(); + const { data } = useSuspenseQuery(trpc.categories.getMany.queryOptions()); + + const router = useRouter(); + const [parentCategories, setParentCategories] = useState< + CategoriesGetManyOutput | null + >(null); + const [selectedCategory, setSelectedCategory] = + useState(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: CategoriesGetManyOutput[1]) => { + if (category.subcategories && category.subcategories.length > 0) { + setParentCategories(category.subcategories as CategoriesGetManyOutput); + 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 ( + + + + Categories + + + + {parentCategories && ( + + )} + + {currentCategories.map((category) => ( + + ))} + + + + ); +}; + +export default CategoriesSidebar; diff --git a/src/app/(app)/(home)/search-filters/categories.tsx b/src/modules/home/ui/components/search-filters/categories.tsx similarity index 94% rename from src/app/(app)/(home)/search-filters/categories.tsx rename to src/modules/home/ui/components/search-filters/categories.tsx index a2277a7..90c6d3a 100644 --- a/src/app/(app)/(home)/search-filters/categories.tsx +++ b/src/modules/home/ui/components/search-filters/categories.tsx @@ -1,127 +1,132 @@ -'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 CategoriesSidebar from './categories-sidebar'; -import { CategoriesGetManyOutput } from '@/modules/categories/type'; - -interface CategoriesProps { - data: CategoriesGetManyOutput; -} - -const Categories = ({ data }: CategoriesProps) => { - const containerRef = useRef(null); - const measureRef = useRef(null); - const viewAllRef = useRef(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 ( -
- {/* Categories sidebar */} - - - {/* Hidden div to measure all items */} -
- {data.map((category) => ( -
- -
- ))} -
- - {/* Visible items */} -
setIsAnyHovered(true)} - onMouseLeave={() => setIsAnyHovered(false)} - > - {data.slice(0, visibleCount).map((category) => ( -
- -
- ))} - -
- -
-
-
- ); -}; - -export default Categories; +'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'; + +interface CategoriesProps { + data: CategoriesGetManyOutput; +} + +const Categories = ({ data }: CategoriesProps) => { + const params = useParams(); + + const containerRef = useRef(null); + const measureRef = useRef(null); + const viewAllRef = useRef(null); + + const [visibleCount, setVisibleCount] = useState(data.length); + const [isAnyHovered, setIsAnyHovered] = useState(false); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const categoryParam = params.category as string | undefined; + const activeCategory = categoryParam || '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 ( +
+ {/* Categories sidebar */} + + + {/* Hidden div to measure all items */} +
+ {data.map((category) => ( +
+ +
+ ))} +
+ + {/* Visible items */} +
setIsAnyHovered(true)} + onMouseLeave={() => setIsAnyHovered(false)} + > + {data.slice(0, visibleCount).map((category) => ( +
+ +
+ ))} + +
+ +
+
+
+ ); +}; + +export default Categories; diff --git a/src/app/(app)/(home)/search-filters/category-dropdown.tsx b/src/modules/home/ui/components/search-filters/category-dropdown.tsx similarity index 96% rename from src/app/(app)/(home)/search-filters/category-dropdown.tsx rename to src/modules/home/ui/components/search-filters/category-dropdown.tsx index edbf25b..0c4477a 100644 --- a/src/app/(app)/(home)/search-filters/category-dropdown.tsx +++ b/src/modules/home/ui/components/search-filters/category-dropdown.tsx @@ -1,86 +1,86 @@ -'use client'; - -import { useRef, useState } from 'react'; - -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; -import useDropdownPosition from './use-dropdown-position'; -import SubCategoryMenu from './subcategory-menu'; - -import Link from 'next/link'; -import { CategoriesGetManyOutput } from '@/modules/categories/type'; - -interface CategoryDropdownProps { - category: CategoriesGetManyOutput[1]; - 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(); - - // TODO: Will improve in mobile screen - // const toggleDropdown = () => { - // if (category.subcategories?.docs?.length) { - // setIsOpen(!isOpen); - // } - // }; - - const handleMouseEnter = () => { - if (category.subcategories) { - setIsOpen(true); - } - }; - - const handleMouseLeave = () => setIsOpen(false); - - return ( -
-
- - - {category.subcategories && category.subcategories.length > 0 && ( -
- )} -
- - -
- ); -}; - -export default CategoryDropdown; +'use client'; + +import { useRef, useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import useDropdownPosition from './use-dropdown-position'; +import SubCategoryMenu from './subcategory-menu'; + +import Link from 'next/link'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; + +interface CategoryDropdownProps { + category: CategoriesGetManyOutput[1]; + 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(); + + // TODO: Will improve in mobile screen + // const toggleDropdown = () => { + // if (category.subcategories?.docs?.length) { + // setIsOpen(!isOpen); + // } + // }; + + 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/modules/home/ui/components/search-filters/index.tsx b/src/modules/home/ui/components/search-filters/index.tsx new file mode 100644 index 0000000..3dabc9c --- /dev/null +++ b/src/modules/home/ui/components/search-filters/index.tsx @@ -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 ( +
+ + +
+ +
+ + +
+ ); +}; + +export const SearchFiltersSkeleton = () => { + return ( +
+ + +
+
+
+
+ ); +}; + +export default SearchFilters; diff --git a/src/app/(app)/(home)/search-filters/search-input.tsx b/src/modules/home/ui/components/search-filters/search-input.tsx similarity index 95% rename from src/app/(app)/(home)/search-filters/search-input.tsx rename to src/modules/home/ui/components/search-filters/search-input.tsx index 11f6c33..e22dc93 100644 --- a/src/app/(app)/(home)/search-filters/search-input.tsx +++ b/src/modules/home/ui/components/search-filters/search-input.tsx @@ -1,63 +1,63 @@ -'use client'; - -import { useState } from 'react'; -import Link from 'next/link'; -import { useQuery } from '@tanstack/react-query'; -import { BookmarkCheckIcon, ListFilterIcon, SearchIcon } from 'lucide-react'; - -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { useTRPC } from '@/trpc/client'; - -import CategoriesSidebar from './categories-sidebar'; - -interface SearchInputProps { - disabled?: boolean; -} - -const SearchInput = ({ disabled }: SearchInputProps) => { - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - const trpc = useTRPC(); - const session = useQuery(trpc.auth.session.queryOptions()); - - return ( -
-
- - -
- - - -
- - - - {session.data?.user && ( - - )} -
-
- ); -}; - -export default SearchInput; +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { BookmarkCheckIcon, ListFilterIcon, SearchIcon } from 'lucide-react'; + +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useTRPC } from '@/trpc/client'; + +import CategoriesSidebar from './categories-sidebar'; + +interface SearchInputProps { + disabled?: boolean; +} + +const SearchInput = ({ disabled }: SearchInputProps) => { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + const trpc = useTRPC(); + const session = useQuery(trpc.auth.session.queryOptions()); + + return ( +
+
+ + +
+ + + +
+ + + + {session.data?.user && ( + + )} +
+
+ ); +}; + +export default SearchInput; diff --git a/src/app/(app)/(home)/search-filters/subcategory-menu.tsx b/src/modules/home/ui/components/search-filters/subcategory-menu.tsx similarity index 95% rename from src/app/(app)/(home)/search-filters/subcategory-menu.tsx rename to src/modules/home/ui/components/search-filters/subcategory-menu.tsx index 4c9cc76..e62d47c 100644 --- a/src/app/(app)/(home)/search-filters/subcategory-menu.tsx +++ b/src/modules/home/ui/components/search-filters/subcategory-menu.tsx @@ -1,60 +1,60 @@ -import Link from 'next/link'; - -import { Category } from '@/payload-types'; -import { CategoriesGetManyOutput } from '@/modules/categories/type'; - -interface SubCategoryMenuProps { - category: CategoriesGetManyOutput[1]; - 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; +import Link from 'next/link'; + +import { Category } from '@/payload-types'; +import { CategoriesGetManyOutput } from '@/modules/categories/type'; + +interface SubCategoryMenuProps { + category: CategoriesGetManyOutput[1]; + 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/modules/home/ui/components/search-filters/use-dropdown-position.ts similarity index 96% rename from src/app/(app)/(home)/search-filters/use-dropdown-position.ts rename to src/modules/home/ui/components/search-filters/use-dropdown-position.ts index 006b1d6..0e90b4f 100644 --- a/src/app/(app)/(home)/search-filters/use-dropdown-position.ts +++ b/src/modules/home/ui/components/search-filters/use-dropdown-position.ts @@ -1,36 +1,36 @@ -import { RefObject } from "react"; - -const useDropdownPosition = (ref: RefObject | RefObject) => { - const getDropdownPosition = () => { - if (!ref.current) return { top: 0, left: 0 }; - - 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 { top, left }; - } - - return { getDropdownPosition } -} - +import { RefObject } from "react"; + +const useDropdownPosition = (ref: RefObject | RefObject) => { + const getDropdownPosition = () => { + if (!ref.current) return { top: 0, left: 0 }; + + 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 { top, left }; + } + + return { getDropdownPosition } +} + export default useDropdownPosition; \ No newline at end of file