mirror of
https://github.com/vercel/commerce.git
synced 2025-05-31 21:46:58 +00:00
Add Navbar
and Search
component (#1)
This commit is contained in:
parent
694c5c17ba
commit
3bc03dd7c8
@ -1,7 +0,0 @@
|
||||
COMPANY_NAME="Vercel Inc."
|
||||
TWITTER_CREATOR="@vercel"
|
||||
TWITTER_SITE="https://nextjs.org/commerce"
|
||||
SITE_NAME="Next.js Commerce"
|
||||
SHOPIFY_REVALIDATION_SECRET=""
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
|
||||
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
|
@ -1,6 +1,14 @@
|
||||
'use client';
|
||||
|
||||
export default function Error({ reset }: { reset: () => void }) {
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Error({ resetAction }: { resetAction?: () => void }) {
|
||||
const router = useRouter();
|
||||
|
||||
const defaultReset = () => {
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
|
||||
<h2 className="text-xl font-bold">Oh no!</h2>
|
||||
@ -10,7 +18,7 @@ export default function Error({ reset }: { reset: () => void }) {
|
||||
</p>
|
||||
<button
|
||||
className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
|
||||
onClick={() => reset()}
|
||||
onClick={resetAction || defaultReset}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
|
@ -1,3 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&display=swap');
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@ -14,8 +15,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-baskerville;
|
||||
}
|
||||
|
||||
a,
|
||||
input,
|
||||
button {
|
||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900;
|
||||
outline: none;
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { CartProvider } from 'components/cart/cart-context';
|
||||
import { Navbar } from 'components/layout/navbar';
|
||||
import { WelcomeToast } from 'components/welcome-toast';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { ensureStartsWith } from 'lib/utils';
|
||||
import { cookies } from 'next/headers';
|
||||
import { ReactNode } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
import './globals.css';
|
||||
|
||||
const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env;
|
||||
@ -44,14 +41,8 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
||||
return (
|
||||
<html lang="en" className={GeistSans.variable}>
|
||||
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||
<CartProvider cartPromise={cart}>
|
||||
<Navbar />
|
||||
<main>
|
||||
{children}
|
||||
<Toaster closeButton />
|
||||
<WelcomeToast />
|
||||
</main>
|
||||
</CartProvider>
|
||||
<Navbar />
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
176
app/page.tsx
176
app/page.tsx
@ -1,7 +1,12 @@
|
||||
import { Carousel } from 'components/carousel';
|
||||
import { ThreeItemGrid } from 'components/grid/three-items';
|
||||
import Error from 'app/error';
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Search } from 'components/layout/search';
|
||||
import { PriceBox } from 'components/price-box';
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import type { Product } from 'lib/shopify/types';
|
||||
import Image from 'next/image';
|
||||
|
||||
//Todo: change to proper metadata
|
||||
export const metadata = {
|
||||
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopify.',
|
||||
openGraph: {
|
||||
@ -9,12 +14,173 @@ export const metadata = {
|
||||
}
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
export default async function HomePage() {
|
||||
const products = await getCollectionProducts({ collection: 'landing' });
|
||||
|
||||
//Todo: change to proper error handling
|
||||
if (!products[0]) return <Error />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThreeItemGrid />
|
||||
<Carousel />
|
||||
<section className="relative">
|
||||
<div className="relative h-screen w-screen">
|
||||
<Image
|
||||
src={
|
||||
products[0].featuredImage.url || '' //Todo: default image
|
||||
}
|
||||
alt={products[0].featuredImage.altText || 'Main product'}
|
||||
fill
|
||||
objectFit="cover"
|
||||
quality={100}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-20 flex w-full flex-col items-center text-lightText">
|
||||
<h1 className="text-xl">{products[0].title}</h1>
|
||||
<span className="mb-6 mt-1 text-sm text-lightText/80">Read more</span>
|
||||
<div className="text-mainBg flex w-[384px] justify-center gap-[10px]">
|
||||
<PriceBox title="Box of 20" price={2460} />
|
||||
<PriceBox title="Single Cigar" price={120} />
|
||||
</div>
|
||||
</div>
|
||||
<Search />
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
//temp: for ProductGridItems test
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: 'prod_001',
|
||||
handle: 'product-1',
|
||||
availableForSale: true,
|
||||
title: 'Product 1',
|
||||
description: 'This is the description for Product 1',
|
||||
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 1</p>',
|
||||
options: [
|
||||
{
|
||||
id: 'option_001',
|
||||
name: 'Size',
|
||||
values: ['S', 'M', 'L']
|
||||
}
|
||||
],
|
||||
priceRange: {
|
||||
maxVariantPrice: {
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD'
|
||||
},
|
||||
minVariantPrice: {
|
||||
amount: '80.00',
|
||||
currencyCode: 'USD'
|
||||
}
|
||||
},
|
||||
featuredImage: {
|
||||
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
|
||||
altText: 'Product 1 Featured Image',
|
||||
width: 500,
|
||||
height: 500
|
||||
},
|
||||
seo: {
|
||||
title: 'Product 1 SEO Title',
|
||||
description: 'This is the SEO description for Product 1'
|
||||
},
|
||||
tags: ['tag1', 'tag2'],
|
||||
updatedAt: new Date().toISOString(),
|
||||
variants: [
|
||||
{
|
||||
id: 'variant_001',
|
||||
title: 'Variant 1',
|
||||
availableForSale: true,
|
||||
selectedOptions: [
|
||||
{
|
||||
name: 'Size',
|
||||
value: 'M'
|
||||
}
|
||||
],
|
||||
price: {
|
||||
amount: '90.00',
|
||||
currencyCode: 'USD'
|
||||
}
|
||||
}
|
||||
],
|
||||
images: [
|
||||
{
|
||||
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
|
||||
altText: 'Product 1 Image 1',
|
||||
width: 500,
|
||||
height: 500
|
||||
},
|
||||
{
|
||||
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
|
||||
altText: 'Product 1 Image 2',
|
||||
width: 400,
|
||||
height: 400
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'prod_002',
|
||||
handle: 'product-2',
|
||||
availableForSale: false,
|
||||
title: 'Product 2',
|
||||
description: 'This is the description for Product 2',
|
||||
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 2</p>',
|
||||
options: [
|
||||
{
|
||||
id: 'option_002',
|
||||
name: 'Color',
|
||||
values: ['Red', 'Blue', 'Green']
|
||||
}
|
||||
],
|
||||
priceRange: {
|
||||
maxVariantPrice: {
|
||||
amount: '120.00',
|
||||
currencyCode: 'USD'
|
||||
},
|
||||
minVariantPrice: {
|
||||
amount: '100.00',
|
||||
currencyCode: 'USD'
|
||||
}
|
||||
},
|
||||
featuredImage: {
|
||||
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
|
||||
altText: 'Product 2 Featured Image',
|
||||
width: 500,
|
||||
height: 500
|
||||
},
|
||||
seo: {
|
||||
title: 'Product 2 SEO Title',
|
||||
description: 'This is the SEO description for Product 2'
|
||||
},
|
||||
tags: ['tag3', 'tag4'],
|
||||
updatedAt: new Date().toISOString(),
|
||||
variants: [
|
||||
{
|
||||
id: 'variant_002',
|
||||
title: 'Variant 2',
|
||||
availableForSale: false,
|
||||
selectedOptions: [
|
||||
{
|
||||
name: 'Color',
|
||||
value: 'Red'
|
||||
}
|
||||
],
|
||||
price: {
|
||||
amount: '110.00',
|
||||
currencyCode: 'USD'
|
||||
}
|
||||
}
|
||||
],
|
||||
images: [
|
||||
{
|
||||
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
|
||||
altText: 'Product 2 Image 1',
|
||||
width: 500,
|
||||
height: 500
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,61 +1,109 @@
|
||||
import CartModal from 'components/cart/modal';
|
||||
import LogoSquare from 'components/logo-square';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
import MobileMenu from './mobile-menu';
|
||||
import Search, { SearchSkeleton } from './search';
|
||||
'use client';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
import {
|
||||
Bars3Icon,
|
||||
MagnifyingGlassIcon,
|
||||
ShoppingBagIcon,
|
||||
UserIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import Logo from 'components/icons/logo';
|
||||
import { LiHTMLAttributes, useState } from 'react';
|
||||
|
||||
export async function Navbar() {
|
||||
const menu = await getMenu('next-js-frontend-header-menu');
|
||||
interface MenuListItemProps extends LiHTMLAttributes<HTMLLIElement> {
|
||||
type: 'main' | 'sub';
|
||||
}
|
||||
|
||||
function MenuListItem({ type, children }: MenuListItemProps) {
|
||||
return (
|
||||
<>
|
||||
{type === 'main' ? (
|
||||
<li className="hover:bg-menuHover/15 px-[10px] py-[15px] hover:rounded-md">{children}</li>
|
||||
) : (
|
||||
<li className="p-[10px]">{children}</li>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className="relative flex items-center justify-between p-4 lg:px-6">
|
||||
<div className="block flex-none md:hidden">
|
||||
<Suspense fallback={null}>
|
||||
<MobileMenu menu={menu} />
|
||||
</Suspense>
|
||||
<nav className="absolute left-0 top-0 z-10 flex w-full items-center px-[4.38rem] py-[3.12rem]">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{/* burger menu */}
|
||||
<button onClick={() => setIsMenuOpen(true)}>
|
||||
<Bars3Icon width={24} height={24} color="white" />
|
||||
</button>
|
||||
{/* logo */}
|
||||
<div className="z-10 justify-center md:flex md:w-1/3">
|
||||
<Logo />
|
||||
</div>
|
||||
{/* 3 icons */}
|
||||
<div className="flex justify-between gap-[1.88rem]">
|
||||
<MagnifyingGlassIcon width={16} height={16} color="white" />
|
||||
<UserIcon width={16} height={16} fill="white" color="white" />
|
||||
<ShoppingBagIcon width={16} height={16} fill="white" color="white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex w-full md:w-1/3">
|
||||
<Link
|
||||
href="/"
|
||||
prefetch={true}
|
||||
className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"
|
||||
>
|
||||
<LogoSquare />
|
||||
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">
|
||||
{SITE_NAME}
|
||||
{/* Categories menu */}
|
||||
{isMenuOpen && (
|
||||
<div className="absolute left-0 top-0 h-screen w-full bg-black/75 text-white">
|
||||
<div className="flex h-full flex-col items-center justify-start pt-[190px]">
|
||||
<div className="grid w-3/4 grid-cols-4 gap-8 text-left text-lightText">
|
||||
<ul>
|
||||
<MenuListItem type="main">All Brands</MenuListItem>
|
||||
<MenuListItem type="main">Special Offers</MenuListItem>
|
||||
<MenuListItem type="main">Collectors</MenuListItem>
|
||||
<MenuListItem type="main">MenuListItemmited Edition</MenuListItem>
|
||||
<MenuListItem type="main">Regional Edition</MenuListItem>
|
||||
<MenuListItem type="main">Vintage</MenuListItem>
|
||||
<MenuListItem type="main">Accessories</MenuListItem>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<MenuListItem type="sub">Bolivar</MenuListItem>
|
||||
<MenuListItem type="sub">Cohiba</MenuListItem>
|
||||
<MenuListItem type="sub">Cuaba</MenuListItem>
|
||||
<MenuListItem type="sub">El Rey Del Mundo</MenuListItem>
|
||||
<MenuListItem type="sub">Juan Lopez</MenuListItem>
|
||||
<MenuListItem type="sub">H. Upmann</MenuListItem>
|
||||
<MenuListItem type="sub">Hoyo De Monterrey</MenuListItem>
|
||||
<MenuListItem type="sub">La Flor De Cano</MenuListItem>
|
||||
<MenuListItem type="sub">La Gloria Cubana</MenuListItem>
|
||||
<MenuListItem type="sub">Montecristo</MenuListItem>
|
||||
<MenuListItem type="sub">Partagas</MenuListItem>
|
||||
</ul>
|
||||
<ul>
|
||||
<MenuListItem type="sub">Bolivar</MenuListItem>
|
||||
<MenuListItem type="sub">Cohiba</MenuListItem>
|
||||
<MenuListItem type="sub">Cuaba</MenuListItem>
|
||||
<MenuListItem type="sub">El Rey Del Mundo</MenuListItem>
|
||||
<MenuListItem type="sub">Juan Lopez</MenuListItem>
|
||||
<MenuListItem type="sub">H. Upmann</MenuListItem>
|
||||
<MenuListItem type="sub">Hoyo De Monterrey</MenuListItem>
|
||||
<MenuListItem type="sub">La Flor De Cano</MenuListItem>
|
||||
<MenuListItem type="sub">La Gloria Cubana</MenuListItem>
|
||||
<MenuListItem type="sub">Montecristo</MenuListItem>
|
||||
<MenuListItem type="sub">Partagas</MenuListItem>
|
||||
</ul>
|
||||
<ul>
|
||||
<MenuListItem type="sub">Bolivar</MenuListItem>
|
||||
<MenuListItem type="sub">Cohiba</MenuListItem>
|
||||
<MenuListItem type="sub">Cuaba</MenuListItem>
|
||||
<MenuListItem type="sub">El Rey Del Mundo</MenuListItem>
|
||||
<MenuListItem type="sub">Juan Lopez</MenuListItem>
|
||||
<MenuListItem type="sub">H. Upmann</MenuListItem>
|
||||
<MenuListItem type="sub">Hoyo De Monterrey</MenuListItem>
|
||||
<MenuListItem type="sub">La Flor De Cano</MenuListItem>
|
||||
<MenuListItem type="sub">La Gloria Cubana</MenuListItem>
|
||||
<MenuListItem type="sub">Montecristo</MenuListItem>
|
||||
<MenuListItem type="sub">Partagas</MenuListItem>
|
||||
</ul>
|
||||
</div>
|
||||
</Link>
|
||||
{menu.length ? (
|
||||
<ul className="hidden gap-6 text-sm md:flex md:items-center">
|
||||
{menu.map((item: Menu) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
prefetch={true}
|
||||
className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden justify-center md:flex md:w-1/3">
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<Search />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex justify-end md:w-1/3">
|
||||
<CartModal />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
@ -3,11 +3,11 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { SearchInput } from 'components/layout/search/search-input';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import Search, { SearchSkeleton } from './search';
|
||||
|
||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
const pathname = usePathname();
|
||||
@ -72,9 +72,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
</button>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<Search />
|
||||
</Suspense>
|
||||
<SearchInput />
|
||||
</div>
|
||||
{menu.length ? (
|
||||
<ul className="flex w-full flex-col">
|
||||
|
@ -1,40 +0,0 @@
|
||||
'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="text-md w-full rounded-lg border bg-white px-4 py-2 text-black placeholder:text-neutral-500 md:text-sm dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<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 rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import FilterList from 'components/layout/search/*not-in-use/filter';
|
||||
import { getCollections } from 'lib/shopify';
|
||||
import FilterList from './filter';
|
||||
|
||||
async function CollectionList() {
|
||||
const collections = await getCollections();
|
12
components/layout/search/filter.tsx
Normal file
12
components/layout/search/filter.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { FunnelIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export function Filter() {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-end gap-[10px] text-sm text-lightText">
|
||||
<FunnelIcon width={18} height={18} opacity={0.5} />
|
||||
<button type="button">Size</button>
|
||||
<div className="h-[13px] border border-y-0 border-l-0 border-r-lightText/50"></div>
|
||||
<button type="button">Strength</button>
|
||||
</div>
|
||||
);
|
||||
}
|
15
components/layout/search/index.tsx
Normal file
15
components/layout/search/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Filter } from 'components/layout/search/filter';
|
||||
import { SearchInput } from 'components/layout/search/search-input';
|
||||
import { Sort } from 'components/layout/search/sort';
|
||||
|
||||
export function Search() {
|
||||
return (
|
||||
<>
|
||||
<section className="bg-brownBg flex justify-between gap-[30px] px-[50px] py-4">
|
||||
<SearchInput />
|
||||
<Filter />
|
||||
<Sort />
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
26
components/layout/search/search-input.tsx
Normal file
26
components/layout/search/search-input.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import Form from 'next/form';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export function SearchInput() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<Form action="/search" className="w-max-[550px] flex w-full gap-[10px] lg:w-80 xl:w-full">
|
||||
<div className="flex h-full items-center text-lightText">
|
||||
<MagnifyingGlassIcon className="h-4 stroke-2" />
|
||||
</div>
|
||||
<input
|
||||
key={searchParams?.get('q')}
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="eg. Finos Rerserva"
|
||||
autoComplete="off"
|
||||
defaultValue={searchParams?.get('q') || ''}
|
||||
className="w-full bg-inherit text-base text-lightText placeholder:text-lightText/50 md:text-sm"
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
12
components/layout/search/sort.tsx
Normal file
12
components/layout/search/sort.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { ArrowsUpDownIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export function Sort() {
|
||||
return (
|
||||
<div className="flex w-fit items-center justify-end gap-[10px] whitespace-nowrap text-sm text-lightText">
|
||||
<ArrowsUpDownIcon width={18} height={18} opacity={0.5} />
|
||||
<button type="button">A-Z</button>
|
||||
<div className="h-[13px] border border-y-0 border-l-0 border-r-lightText/50"></div>
|
||||
<button type="button">Price</button>
|
||||
</div>
|
||||
);
|
||||
}
|
13
components/price-box.tsx
Normal file
13
components/price-box.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
interface PriceBoxProps {
|
||||
title: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export const PriceBox = ({ title, price }: PriceBoxProps) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-[5px] rounded bg-black/50 p-2">
|
||||
<p>{title}</p>
|
||||
<p className="text-xs text-lightText/80">£{price.toLocaleString('en-GB')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -28,10 +28,13 @@
|
||||
"@types/node": "20.14.12",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"react-slick": "^0.30.2",
|
||||
"swiper": "^11.1.12",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
|
70
pnpm-lock.yaml
generated
70
pnpm-lock.yaml
generated
@ -48,6 +48,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
'@types/react-slick':
|
||||
specifier: ^0.23.13
|
||||
version: 0.23.13
|
||||
autoprefixer:
|
||||
specifier: ^10.4.19
|
||||
version: 10.4.19(postcss@8.4.39)
|
||||
@ -60,6 +63,12 @@ importers:
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.6.5
|
||||
version: 0.6.5(prettier@3.3.3)
|
||||
react-slick:
|
||||
specifier: ^0.30.2
|
||||
version: 0.30.2(react-dom@19.0.0-rc-3208e73e-20240730(react@19.0.0-rc-3208e73e-20240730))(react@19.0.0-rc-3208e73e-20240730)
|
||||
swiper:
|
||||
specifier: ^11.1.12
|
||||
version: 11.1.12
|
||||
tailwindcss:
|
||||
specifier: ^3.4.6
|
||||
version: 3.4.6
|
||||
@ -382,6 +391,9 @@ packages:
|
||||
'@types/react-dom@18.3.0':
|
||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||
|
||||
'@types/react-slick@0.23.13':
|
||||
resolution: {integrity: sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==}
|
||||
|
||||
'@types/react@18.3.3':
|
||||
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
|
||||
|
||||
@ -452,6 +464,9 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
@ -511,6 +526,9 @@ packages:
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
enquire.js@2.1.6:
|
||||
resolution: {integrity: sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==}
|
||||
|
||||
escalade@3.1.2:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -602,6 +620,9 @@ packages:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
json2mq@0.2.0:
|
||||
resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==}
|
||||
|
||||
lilconfig@2.1.0:
|
||||
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -616,6 +637,9 @@ packages:
|
||||
lodash.castarray@4.4.0:
|
||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||
|
||||
lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
@ -832,6 +856,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: 19.0.0-rc-3208e73e-20240730
|
||||
|
||||
react-slick@0.30.2:
|
||||
resolution: {integrity: sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==}
|
||||
peerDependencies:
|
||||
react: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react@19.0.0-rc-3208e73e-20240730:
|
||||
resolution: {integrity: sha512-4TmFOcgSfwM8w18vXLnEt8tb3ilO9a0GRJA9zQSYjZ5ie6g/zkxagRvZvZbEmhaNgDSF/PKmEdWmfBtlUBcjkA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -843,6 +873,9 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
@ -895,6 +928,9 @@ packages:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
string-convert@0.2.1:
|
||||
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@ -933,6 +969,10 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swiper@11.1.12:
|
||||
resolution: {integrity: sha512-PUkCToYAZMB4kP7z+YfPnkMHOMwMO71g8vUhz2o5INGIgIMb6Sb0XiP6cEJFsiFTd7FRDn5XCbg+KVKPDZqXLw==}
|
||||
engines: {node: '>= 4.7.0'}
|
||||
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
@ -1261,6 +1301,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@types/react-slick@0.23.13':
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
'@types/react@18.3.3':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.12
|
||||
@ -1334,6 +1378,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
@ -1383,6 +1429,8 @@ snapshots:
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
enquire.js@2.1.6: {}
|
||||
|
||||
escalade@3.1.2: {}
|
||||
|
||||
fast-glob@3.3.2:
|
||||
@ -1471,6 +1519,10 @@ snapshots:
|
||||
|
||||
jiti@1.21.6: {}
|
||||
|
||||
json2mq@0.2.0:
|
||||
dependencies:
|
||||
string-convert: 0.2.1
|
||||
|
||||
lilconfig@2.1.0: {}
|
||||
|
||||
lilconfig@3.1.2: {}
|
||||
@ -1479,6 +1531,8 @@ snapshots:
|
||||
|
||||
lodash.castarray@4.4.0: {}
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
@ -1623,6 +1677,16 @@ snapshots:
|
||||
react: 19.0.0-rc-3208e73e-20240730
|
||||
scheduler: 0.25.0-rc-3208e73e-20240730
|
||||
|
||||
react-slick@0.30.2(react-dom@19.0.0-rc-3208e73e-20240730(react@19.0.0-rc-3208e73e-20240730))(react@19.0.0-rc-3208e73e-20240730):
|
||||
dependencies:
|
||||
classnames: 2.5.1
|
||||
enquire.js: 2.1.6
|
||||
json2mq: 0.2.0
|
||||
lodash.debounce: 4.0.8
|
||||
react: 19.0.0-rc-3208e73e-20240730
|
||||
react-dom: 19.0.0-rc-3208e73e-20240730(react@19.0.0-rc-3208e73e-20240730)
|
||||
resize-observer-polyfill: 1.5.1
|
||||
|
||||
react@19.0.0-rc-3208e73e-20240730: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
@ -1633,6 +1697,8 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve@1.22.8:
|
||||
dependencies:
|
||||
is-core-module: 2.15.0
|
||||
@ -1699,6 +1765,8 @@ snapshots:
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
string-convert@0.2.1: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@ -1736,6 +1804,8 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swiper@11.1.12: {}
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwindcss@3.4.6:
|
||||
|
@ -6,7 +6,13 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-geist-sans)']
|
||||
baskerville: ['Libre Baskerville', 'serif']
|
||||
},
|
||||
colors: {
|
||||
lightText: '#FFF2DB',
|
||||
mainBg: '#F8E8D1',
|
||||
menuHover: '#C6BAA7',
|
||||
brownBg: '#39170A'
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user