chore: transfer repo
This commit is contained in:
32
components/layout/ProductGridItems.tsx
Normal file
32
components/layout/ProductGridItems.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Grid from 'components/grid';
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ProductGridItems({ products }: { products: Product[] }) {
|
||||
return (
|
||||
<>
|
||||
{products.map((product) => (
|
||||
<Grid.Item key={product.handle} className="animate-fadeIn">
|
||||
<Link
|
||||
className="relative inline-block h-full w-full"
|
||||
href={`/product/${product.handle}`}
|
||||
prefetch={true}
|
||||
>
|
||||
<GridTileImage
|
||||
alt={product.title}
|
||||
label={{
|
||||
title: product.title,
|
||||
amount: product.priceRange.maxVariantPrice.amount,
|
||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
||||
}}
|
||||
src={product.featuredImage?.url}
|
||||
fill
|
||||
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
/>
|
||||
</Link>
|
||||
</Grid.Item>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
components/layout/categories.tsx
Normal file
59
components/layout/categories.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
const categories = {
|
||||
'Men': [
|
||||
{ name: 'T-Shirts', href: '/collections/mens-t-shirts' },
|
||||
{ name: 'Hoodies', href: '/collections/mens-hoodies' },
|
||||
{ name: 'Pants', href: '/collections/mens-pants' },
|
||||
{ name: 'Accessories', href: '/collections/mens-accessories' }
|
||||
],
|
||||
'Women': [
|
||||
{ name: 'Dresses', href: '/collections/womens-dresses' },
|
||||
{ name: 'Tops', href: '/collections/womens-tops' },
|
||||
{ name: 'Bottoms', href: '/collections/womens-bottoms' },
|
||||
{ name: 'Accessories', href: '/collections/womens-accessories' }
|
||||
]
|
||||
};
|
||||
|
||||
export default function Categories() {
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="flex gap-6">
|
||||
{Object.keys(categories).map((section) => (
|
||||
<button
|
||||
key={section}
|
||||
className="relative py-4 text-sm font-medium text-gray-700 hover:text-gray-900"
|
||||
onMouseEnter={() => setActiveSection(section)}
|
||||
onMouseLeave={() => setActiveSection(null)}
|
||||
>
|
||||
{section}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{activeSection && (
|
||||
<div
|
||||
className="absolute left-0 top-full z-10 w-48 rounded-md bg-white py-2 shadow-lg ring-1 ring-black ring-opacity-5"
|
||||
onMouseEnter={() => setActiveSection(activeSection)}
|
||||
onMouseLeave={() => setActiveSection(null)}
|
||||
>
|
||||
{categories[activeSection as keyof typeof categories].map((category) => (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={category.href}
|
||||
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
{category.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
components/layout/footer.tsx
Normal file
120
components/layout/footer.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useCookieConsent } from "@/components/cookies";
|
||||
import { useTranslation } from "@/lib/hooks/useTranslation";
|
||||
import { container } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
const { openModal } = useCookieConsent();
|
||||
|
||||
// Definiraj linkove s prijevodima
|
||||
const navLinks = [
|
||||
{ name: t('footer.build_box'), href: "/build-box" },
|
||||
{ name: t('footer.products'), href: "/products" },
|
||||
{ name: t('footer.about'), href: "/about" }
|
||||
];
|
||||
|
||||
const legalLinks = [
|
||||
{ name: t('footer.privacy_policy'), href: "/privacy-policy" },
|
||||
{ name: t('footer.terms_of_service'), href: "/terms-of-service" },
|
||||
{ name: t('footer.cookies_settings'), href: "#", isButton: true }
|
||||
];
|
||||
|
||||
const socialLinks = [
|
||||
{ name: t('footer.social.facebook'), href: "https://facebook.com/sentshop", icon: "/assets/images/Facebook.png" },
|
||||
{ name: t('footer.social.instagram'), href: "https://instagram.com/sentshop", icon: "/assets/images/Instagram.png" }
|
||||
];
|
||||
|
||||
const handleCookiesSettings = () => {
|
||||
openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-100">
|
||||
<div className={container}>
|
||||
{/* Main footer content */}
|
||||
<div className="py-12">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center">
|
||||
{/* Logo */}
|
||||
<div className="flex justify-center md:justify-start mb-8 md:mb-0">
|
||||
<Link href="/" aria-label="Logo">
|
||||
<Image
|
||||
src="/assets/images/logo.svg"
|
||||
alt="SENT logo"
|
||||
width={125}
|
||||
height={35}
|
||||
className="w-[125px] h-auto object-contain"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-col items-center md:flex-row md:space-x-8 mb-8 md:mb-0">
|
||||
{navLinks.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
className="text-[16px] font-bold text-gray-700 hover:text-gray-900 mb-4 md:mb-0"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Social media icons */}
|
||||
<div className="flex items-center justify-center md:justify-end space-x-6 mb-8 md:mb-0">
|
||||
{socialLinks.map((social, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={social.href}
|
||||
aria-label={social.name}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<Image
|
||||
src={social.icon}
|
||||
alt={social.name}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legal footer */}
|
||||
<div className="border-t border-gray-200">
|
||||
<div className="py-6">
|
||||
<div className="flex flex-col md:flex-row justify-center items-center text-[14px] leading-[21px] text-gray-500 space-y-4 md:space-y-0 md:space-x-6">
|
||||
<span>© {new Date().getFullYear()} Sent. {t('footer.all_rights_reserved')}</span>
|
||||
{legalLinks.map((link, index) => (
|
||||
link.isButton ? (
|
||||
<button
|
||||
key={index}
|
||||
onClick={handleCookiesSettings}
|
||||
className="hover:text-gray-700 text-[14px] leading-[21px]"
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
className="hover:text-gray-700 text-[14px] leading-[21px]"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
252
components/layout/navbar/index.tsx
Normal file
252
components/layout/navbar/index.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
'use client';
|
||||
|
||||
import { LanguageSwitcher } from '@/components/i18n/LanguageSwitcher';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/lib/hooks/useTranslation';
|
||||
import { container } from '@/lib/utils';
|
||||
import { useCart } from 'components/cart/cart-context';
|
||||
import { Menu, X } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Navbar() {
|
||||
const { t, locale } = useTranslation();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { cart } = useCart();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Create links with translations and proper prefix
|
||||
const getNavLinks = () => {
|
||||
const links = [
|
||||
{ name: t('navbar.build_box'), path: '/build-box' },
|
||||
{ name: t('navbar.products'), path: '/products' },
|
||||
{ name: t('navbar.about'), path: '/about' }
|
||||
];
|
||||
|
||||
// Add locale prefix for English paths
|
||||
if (locale === 'en') {
|
||||
return links.map(link => ({
|
||||
...link,
|
||||
href: `/en${link.path}`
|
||||
}));
|
||||
}
|
||||
|
||||
// Return paths as is for Croatian
|
||||
return links.map(link => ({
|
||||
...link,
|
||||
href: link.path
|
||||
}));
|
||||
};
|
||||
|
||||
const navLinks = getNavLinks();
|
||||
|
||||
// Close mobile menu on resize to desktop
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Prevent scrolling when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Create home link with proper prefix
|
||||
const homeLink = locale === 'en' ? '/en' : '/';
|
||||
|
||||
// Create cart link with proper prefix
|
||||
const cartLink = locale === 'en' ? '/en/cart' : '/cart';
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="border-b border-gray-200 sticky top-0 z-40 bg-white">
|
||||
<div className={`${container} flex h-16 items-center justify-between`}>
|
||||
{/* Logo */}
|
||||
<Link href={homeLink} className="flex items-center h-12">
|
||||
<Image
|
||||
src="/assets/images/logo.svg"
|
||||
alt="Logo"
|
||||
width={125}
|
||||
height={35}
|
||||
className="w-[125px] h-auto object-contain"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav and Cart */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
{navLinks.map((link) => {
|
||||
const isActive = pathname === link.href ||
|
||||
(link.href !== homeLink && pathname.startsWith(link.href));
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className={`text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'text-primary hover:text-primary-dark'
|
||||
: 'text-black hover:text-primary'
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
<Link
|
||||
href={cartLink}
|
||||
className="inline-flex h-10 items-center justify-center rounded-button bg-primary px-6 py-2 text-sm font-medium text-white relative hover:bg-primary-dark"
|
||||
>
|
||||
<span>{t('navbar.cart')}</span>
|
||||
<Image
|
||||
src="/assets/images/cart-icon.png"
|
||||
alt="Cart"
|
||||
width={20}
|
||||
height={20}
|
||||
className="ml-2"
|
||||
/>
|
||||
{cart?.totalQuantity ? (
|
||||
<span className="absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
|
||||
{cart.totalQuantity}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button and cart */}
|
||||
<div className="md:hidden flex items-center space-x-4">
|
||||
<Link
|
||||
href={cartLink}
|
||||
className="relative text-black"
|
||||
aria-label="View cart"
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/cart-icon-black.png"
|
||||
alt="Cart"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
{cart?.totalQuantity ? (
|
||||
<span className="absolute -right-2 -top-3 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
|
||||
{cart.totalQuantity}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="text-gray-500"
|
||||
onClick={() => setIsMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isMenuOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-white md:hidden">
|
||||
{/* Mobile menu header - use same container and height as main navbar */}
|
||||
<div className={`${container} flex h-16 items-center justify-between`}>
|
||||
<Link
|
||||
href={homeLink}
|
||||
className="flex items-center h-12"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/logo.svg"
|
||||
alt="Logo"
|
||||
width={125}
|
||||
height={35}
|
||||
className="w-[125px] h-auto object-contain"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
className="text-gray-500"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu links */}
|
||||
<div className={`${container} flex-1 flex flex-col py-4`}>
|
||||
<div className="space-y-4">
|
||||
{navLinks.map((link) => {
|
||||
const isActive = pathname === link.href ||
|
||||
(link.href !== homeLink && pathname.startsWith(link.href));
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className={`block text-lg font-medium py-2 transition-colors ${
|
||||
isActive
|
||||
? 'text-primary'
|
||||
: 'text-black hover:text-primary'
|
||||
}`}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="py-2">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={cartLink}
|
||||
className="mt-4 flex h-12 w-full items-center justify-center relative"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<span>{t('navbar.cart')}</span>
|
||||
<Image
|
||||
src="/assets/images/cart-icon.png"
|
||||
alt="Cart"
|
||||
width={20}
|
||||
height={20}
|
||||
className="ml-2"
|
||||
/>
|
||||
{cart?.totalQuantity ? (
|
||||
<span className="absolute right-4 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white">
|
||||
{cart.totalQuantity}
|
||||
</span>
|
||||
) : null}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
components/layout/navbar/search.tsx
Normal file
40
components/layout/navbar/search.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import Form from 'next/form';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Search() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<Form action="/search" className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
key={searchParams?.get('q')}
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="Search for products..."
|
||||
autoComplete="off"
|
||||
defaultValue={searchParams?.get('q') || ''}
|
||||
className="w-full px-4 py-2 border"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchSkeleton() {
|
||||
return (
|
||||
<form className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
placeholder="Search for products..."
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
37
components/layout/search/collections.tsx
Normal file
37
components/layout/search/collections.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { getCollections } from 'lib/shopify';
|
||||
import FilterList from './filter';
|
||||
|
||||
async function CollectionList() {
|
||||
const collections = await getCollections();
|
||||
return <FilterList list={collections} title="Collections" />;
|
||||
}
|
||||
|
||||
const skeleton = 'mb-3 h-4 w-5/6 animate-pulse rounded';
|
||||
const activeAndTitles = 'bg-neutral-800 dark:bg-neutral-300';
|
||||
const items = 'bg-neutral-400 dark:bg-neutral-700';
|
||||
|
||||
export default function Collections() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="col-span-2 hidden h-[400px] w-full flex-none py-4 lg:block">
|
||||
<div className={clsx(skeleton, activeAndTitles)} />
|
||||
<div className={clsx(skeleton, activeAndTitles)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
<div className={clsx(skeleton, items)} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CollectionList />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
64
components/layout/search/filter/dropdown.tsx
Normal file
64
components/layout/search/filter/dropdown.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import type { ListItem } from '.';
|
||||
import { FilterItem } from './item';
|
||||
|
||||
export default function FilterItemDropdown({ list }: { list: ListItem[] }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [active, setActive] = useState('');
|
||||
const [openSelect, setOpenSelect] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
setOpenSelect(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
return () => window.removeEventListener('click', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
list.forEach((listItem: ListItem) => {
|
||||
if (
|
||||
('path' in listItem && pathname === listItem.path) ||
|
||||
('slug' in listItem && searchParams.get('sort') === listItem.slug)
|
||||
) {
|
||||
setActive(listItem.title);
|
||||
}
|
||||
});
|
||||
}, [pathname, list, searchParams]);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={ref}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpenSelect(!openSelect);
|
||||
}}
|
||||
className="flex w-full items-center justify-between rounded border border-black/30 px-4 py-2 text-sm dark:border-white/30"
|
||||
>
|
||||
<div>{active}</div>
|
||||
<ChevronDownIcon className="h-4" />
|
||||
</div>
|
||||
{openSelect && (
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpenSelect(false);
|
||||
}}
|
||||
className="absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black"
|
||||
>
|
||||
{list.map((item: ListItem, i) => (
|
||||
<FilterItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
components/layout/search/filter/index.tsx
Normal file
41
components/layout/search/filter/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { SortFilterItem } from 'lib/constants';
|
||||
import { Suspense } from 'react';
|
||||
import FilterItemDropdown from './dropdown';
|
||||
import { FilterItem } from './item';
|
||||
|
||||
export type ListItem = SortFilterItem | PathFilterItem;
|
||||
export type PathFilterItem = { title: string; path: string };
|
||||
|
||||
function FilterItemList({ list }: { list: ListItem[] }) {
|
||||
return (
|
||||
<>
|
||||
{list.map((item: ListItem, i) => (
|
||||
<FilterItem key={i} item={item} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) {
|
||||
return (
|
||||
<>
|
||||
<nav>
|
||||
{title ? (
|
||||
<h3 className="hidden text-xs text-neutral-500 md:block dark:text-neutral-400">
|
||||
{title}
|
||||
</h3>
|
||||
) : null}
|
||||
<ul className="hidden md:block">
|
||||
<Suspense fallback={null}>
|
||||
<FilterItemList list={list} />
|
||||
</Suspense>
|
||||
</ul>
|
||||
<ul className="md:hidden">
|
||||
<Suspense fallback={null}>
|
||||
<FilterItemDropdown list={list} />
|
||||
</Suspense>
|
||||
</ul>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
components/layout/search/filter/item.tsx
Normal file
67
components/layout/search/filter/item.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import type { SortFilterItem } from 'lib/constants';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import type { ListItem, PathFilterItem } from '.';
|
||||
|
||||
function PathFilterItem({ item }: { item: PathFilterItem }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const active = pathname === item.path;
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
const DynamicTag = active ? 'p' : Link;
|
||||
|
||||
newParams.delete('q');
|
||||
|
||||
return (
|
||||
<li className="mt-2 flex text-black dark:text-white" key={item.title}>
|
||||
<DynamicTag
|
||||
href={createUrl(item.path, newParams)}
|
||||
className={clsx(
|
||||
'w-full text-sm underline-offset-4 hover:underline dark:hover:text-neutral-100',
|
||||
{
|
||||
'underline underline-offset-4': active
|
||||
}
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</DynamicTag>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function SortFilterItem({ item }: { item: SortFilterItem }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const active = searchParams.get('sort') === item.slug;
|
||||
const q = searchParams.get('q');
|
||||
const href = createUrl(
|
||||
pathname,
|
||||
new URLSearchParams({
|
||||
...(q && { q }),
|
||||
...(item.slug && item.slug.length && { sort: item.slug })
|
||||
})
|
||||
);
|
||||
const DynamicTag = active ? 'p' : Link;
|
||||
|
||||
return (
|
||||
<li className="mt-2 flex text-sm text-black dark:text-white" key={item.title}>
|
||||
<DynamicTag
|
||||
prefetch={!active ? false : undefined}
|
||||
href={href}
|
||||
className={clsx('w-full hover:underline hover:underline-offset-4', {
|
||||
'underline underline-offset-4': active
|
||||
})}
|
||||
>
|
||||
{item.title}
|
||||
</DynamicTag>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterItem({ item }: { item: ListItem }) {
|
||||
return 'path' in item ? <PathFilterItem item={item} /> : <SortFilterItem item={item} />;
|
||||
}
|
||||
Reference in New Issue
Block a user