working on nav bar

This commit is contained in:
karl 2025-03-28 11:36:41 -04:00
parent ad785c9f9a
commit d9625e21bd
25 changed files with 2023 additions and 847 deletions

View File

@ -1,16 +1,16 @@
import type { Metadata } from 'next';
import type { Metadata } from 'next'
import Prose from 'components/prose';
import { getPage } from 'lib/shopify';
import { notFound } from 'next/navigation';
import Prose from 'components/prose'
import { getPage } from 'lib/shopify'
import { notFound } from 'next/navigation'
export async function generateMetadata(props: {
params: Promise<{ page: string }>;
params: Promise<{ page: string }>
}): Promise<Metadata> {
const params = await props.params;
const page = await getPage(params.page);
const params = await props.params
const page = await getPage(params.page)
if (!page) return notFound();
if (!page) return notFound()
return {
title: page.seo?.title || page.title,
@ -18,28 +18,34 @@ export async function generateMetadata(props: {
openGraph: {
publishedTime: page.createdAt,
modifiedTime: page.updatedAt,
type: 'article'
}
};
type: 'article',
},
}
}
export default async function Page(props: { params: Promise<{ page: string }> }) {
const params = await props.params;
const page = await getPage(params.page);
export default async function Page(props: {
params: Promise<{ page: string }>
}) {
const params = await props.params
const page = await getPage(params.page)
if (!page) return notFound();
console.log('page', params)
if (!page) return notFound()
return (
<>
<h1 className="mb-8 text-5xl font-bold">{page.title}</h1>
<Prose className="mb-8" html={page.body} />
<p className="text-sm italic">
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(page.updatedAt))}.`}
{`This document was last updated on ${new Intl.DateTimeFormat(
undefined,
{
year: 'numeric',
month: 'long',
day: 'numeric',
},
).format(new Date(page.updatedAt))}.`}
</p>
</>
);
)
}

View File

@ -30,8 +30,9 @@
--accent-7: #333333;
--accent-8: #111111;
--accent-9: #000;
--font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',
'Helvetica', sans-serif;
--font-sans:
-apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', 'Helvetica',
sans-serif;
}
[data-theme='dark'] {

View File

@ -1,3 +1,2 @@
@import './base.css';
@import './components.css';
@import './utilities.css';

View File

@ -1,40 +1,45 @@
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 { ReactNode } from 'react';
import { Toaster } from 'sonner';
import './globals.css';
import { baseUrl } from 'lib/utils';
import { CartProvider } from 'components/cart/cart-context'
// import { Navbar } from 'components/layout/navbar'
import NavbarV2 from 'components/layout/navbar/NavbarV2'
import { WelcomeToast } from 'components/welcome-toast'
import { GeistSans } from 'geist/font/sans'
import { getCart } from 'lib/shopify'
import { ReactNode } from 'react'
import { Toaster } from 'sonner'
import './globals.css'
import './assets/main.css'
import '../components/layout/navbar/Navbar.css'
const { SITE_NAME } = process.env;
import { baseUrl } from 'lib/utils'
const { SITE_NAME } = process.env
export const metadata = {
metadataBase: new URL(baseUrl),
title: {
default: SITE_NAME!,
template: `%s | ${SITE_NAME}`
template: `%s | ${SITE_NAME}`,
},
robots: {
follow: true,
index: true
}
};
index: true,
},
}
export default async function RootLayout({
children
children,
}: {
children: ReactNode;
children: ReactNode
}) {
// Don't await the fetch, pass the Promise to the context provider
const cart = getCart();
const cart = getCart()
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 />
{/* <Navbar /> */}
<NavbarV2 />
<main>
{children}
<Toaster closeButton />
@ -43,5 +48,5 @@ export default async function RootLayout({
</CartProvider>
</body>
</html>
);
)
}

View File

@ -13,9 +13,9 @@ export const metadata = {
export default function HomePage() {
return (
<>
{/* <ThreeItemGrid /> */}
{/* <Carousel /> */}
{/* <Footer /> */}
<ThreeItemGrid />
<Carousel />
<Footer />
</>
)
}

View File

@ -1,27 +1,27 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { GridTileImage } from 'components/grid/tile';
import Footer from 'components/layout/footer';
import { Gallery } from 'components/product/gallery';
import { ProductProvider } from 'components/product/product-context';
import { ProductDescription } from 'components/product/product-description';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct, getProductRecommendations } from 'lib/shopify';
import { Image } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
import { GridTileImage } from 'components/grid/tile'
import Footer from 'components/layout/footer'
import { Gallery } from 'components/product/gallery'
import { ProductProvider } from 'components/product/product-context'
import { ProductDescription } from 'components/product/product-description'
import { HIDDEN_PRODUCT_TAG } from 'lib/constants'
import { getProduct, getProductRecommendations } from 'lib/shopify'
import { Image } from 'lib/shopify/types'
import Link from 'next/link'
import { Suspense } from 'react'
export async function generateMetadata(props: {
params: Promise<{ handle: string }>;
params: Promise<{ handle: string }>
}): Promise<Metadata> {
const params = await props.params;
const product = await getProduct(params.handle);
const params = await props.params
const product = await getProduct(params.handle)
if (!product) return notFound();
if (!product) return notFound()
const { url, width, height, altText: alt } = product.featuredImage || {};
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
const { url, width, height, altText: alt } = product.featuredImage || {}
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG)
return {
title: product.seo.title || product.title,
@ -46,16 +46,16 @@ export async function generateMetadata(props: {
],
}
: null,
};
}
}
export default async function ProductPage(props: {
params: Promise<{ handle: string }>;
params: Promise<{ handle: string }>
}) {
const params = await props.params;
const product = await getProduct(params.handle);
const params = await props.params
const product = await getProduct(params.handle)
if (!product) return notFound();
if (!product) return notFound()
const productJsonLd = {
'@context': 'https://schema.org',
@ -72,22 +72,22 @@ export default async function ProductPage(props: {
highPrice: product.priceRange.maxVariantPrice.amount,
lowPrice: product.priceRange.minVariantPrice.amount,
},
};
}
return (
<ProductProvider>
<script
type='application/ld+json'
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd),
}}
/>
<div className='mx-auto max-w-(--breakpoint-2xl) px-4'>
<div className='flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black'>
<div className='h-full w-full basis-full lg:basis-4/6'>
<div className="mx-auto max-w-(--breakpoint-2xl) px-4">
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black">
<div className="h-full w-full basis-full lg:basis-4/6">
<Suspense
fallback={
<div className='relative aspect-square h-full max-h-[550px] w-full overflow-hidden' />
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden" />
}
>
<Gallery
@ -99,7 +99,7 @@ export default async function ProductPage(props: {
</Suspense>
</div>
<div className='basis-full lg:basis-2/6'>
<div className="basis-full lg:basis-2/6">
<Suspense fallback={null}>
<ProductDescription product={product} />
</Suspense>
@ -109,25 +109,25 @@ export default async function ProductPage(props: {
</div>
<Footer />
</ProductProvider>
);
)
}
async function RelatedProducts({ id }: { id: string }) {
const relatedProducts = await getProductRecommendations(id);
const relatedProducts = await getProductRecommendations(id)
if (!relatedProducts.length) return null;
if (!relatedProducts.length) return null
return (
<div className='py-8'>
<h2 className='mb-4 text-2xl font-bold'>Related Products</h2>
<ul className='flex w-full gap-4 overflow-x-auto pt-1'>
<div className="py-8">
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
{relatedProducts.map((product) => (
<li
key={product.handle}
className='aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5'
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
>
<Link
className='relative h-full w-full'
className="relative h-full w-full"
href={`/product/${product.handle}`}
prefetch={true}
>
@ -140,12 +140,12 @@ async function RelatedProducts({ id }: { id: string }) {
}}
src={product.featuredImage?.url}
fill
sizes='(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw'
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
/>
</Link>
</li>
))}
</ul>
</div>
);
)
}

View File

@ -1,35 +1,42 @@
import { getCollection, getCollectionProducts } from 'lib/shopify';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getCollection, getCollectionProducts } from 'lib/shopify'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants';
import Grid from 'components/grid'
import ProductGridItems from 'components/layout/product-grid-items'
import { defaultSort, sorting } from 'lib/constants'
export async function generateMetadata(props: {
params: Promise<{ collection: string }>;
params: Promise<{ collection: string }>
}): Promise<Metadata> {
const params = await props.params;
const collection = await getCollection(params.collection);
const params = await props.params
const collection = await getCollection(params.collection)
if (!collection) return notFound();
if (!collection) return notFound()
return {
title: collection.seo?.title || collection.title,
description:
collection.seo?.description || collection.description || `${collection.title} products`
};
collection.seo?.description ||
collection.description ||
`${collection.title} products`,
}
}
export default async function CategoryPage(props: {
params: Promise<{ collection: string }>;
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
params: Promise<{ collection: string }>
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
const searchParams = await props.searchParams
const params = await props.params
const { sort } = searchParams as { [key: string]: string }
const { sortKey, reverse } =
sorting.find((item) => item.slug === sort) || defaultSort
const products = await getCollectionProducts({
collection: params.collection,
sortKey,
reverse,
})
return (
<section>
@ -41,5 +48,5 @@ export default async function CategoryPage(props: {
</Grid>
)}
</section>
);
)
}

View File

@ -0,0 +1,31 @@
.root {
@apply inset-0 fixed;
left: 72px;
z-index: 10;
height: 100vh;
min-width: 100vw;
transition: none;
}
@media screen(lg) {
.root {
@apply static;
min-width: inherit;
height: inherit;
}
}
.link {
@apply text-primary flex cursor-pointer px-6 py-3
transition ease-in-out duration-150 leading-6
font-medium items-center capitalize w-full box-border
outline-0;
}
.link:hover {
@apply bg-accent-1 outline-none;
}
.link.active {
@apply font-bold bg-accent-2;
}

View File

@ -0,0 +1,86 @@
import cn from 'clsx'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import { Moon, Sun } from '@components/icons'
import s from './CustomerMenuContent.module.css'
import useLogout from '@framework/auth/use-logout'
import {
DropdownContent,
DropdownMenuItem,
} from '@components/ui/Dropdown/Dropdown'
const LINKS = [
{
name: 'My Orders',
href: '/orders',
},
{
name: 'My Profile',
href: '/profile',
},
{
name: 'My Cart',
href: '/cart',
},
]
export default function CustomerMenuContent() {
const router = useRouter()
const logout = useLogout()
const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
function handleClick(_: React.MouseEvent<HTMLAnchorElement>, href: string) {
router.push(href)
}
return (
<DropdownContent
asChild
side="bottom"
sideOffset={10}
className={s.root}
id="CustomerMenuContent"
>
{LINKS.map(({ name, href }) => (
<DropdownMenuItem key={href}>
<a
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={(e) => handleClick(e, href)}
>
{name}
</a>
</DropdownMenuItem>
))}
<DropdownMenuItem>
<a
className={cn(s.link, 'justify-between')}
onClick={() => {
setTheme(theme === 'dark' ? 'light' : 'dark')
}}
>
<div>
Theme: <strong>{theme}</strong>{' '}
</div>
<div className="ml-3">
{theme == 'dark' ? (
<Moon width={20} height={20} />
) : (
<Sun width={20} height={20} />
)}
</div>
</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a
className={cn(s.link, 'border-t border-accent-2 mt-4')}
onClick={() => logout()}
>
Logout
</a>
</DropdownMenuItem>
</DropdownContent>
)
}

View File

@ -0,0 +1 @@
export { default } from './CustomerMenuContent'

View File

@ -0,0 +1,7 @@
.root {
@apply px-4 sm:px-6 sm:w-full flex-1 z-20;
}
.item {
@apply text-xl font-bold py-2;
}

View File

@ -0,0 +1,38 @@
import Link from 'next/link'
import s from './MenuSidebarView.module.css'
import { useUI } from '@components/ui/context'
import SidebarLayout from '@components/common/SidebarLayout'
import type { Link as LinkProps } from './index'
export default function MenuSidebarView({
links = [],
}: {
links?: LinkProps[]
}) {
const { closeSidebar } = useUI()
return (
<SidebarLayout handleClose={() => closeSidebar()}>
<div className={s.root}>
<nav>
<ul>
<li className={s.item} onClick={() => closeSidebar()}>
<Link href="/search">All</Link>
</li>
{links.map((l: any) => (
<li
key={l.href}
className={s.item}
onClick={() => closeSidebar()}
>
<Link href={l.href}>{l.label}</Link>
</li>
))}
</ul>
</nav>
</div>
</SidebarLayout>
)
}
MenuSidebarView

View File

@ -0,0 +1,5 @@
export { default } from './MenuSidebarView'
export interface Link {
href: string
label: string
}

View File

@ -0,0 +1,59 @@
.root {
@apply relative flex items-center;
}
.list {
@apply flex flex-row items-center justify-items-end h-full;
}
.item {
@apply ml-6 cursor-pointer relative transition ease-in-out
duration-100 flex items-center outline-none text-primary;
}
.item:hover {
@apply text-accent-6 transition scale-110 duration-100;
}
.item:first-child {
@apply ml-0;
}
.item:focus,
.item:active {
@apply outline-none;
}
.bagCount {
@apply border border-black bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
padding-left: 2.5px;
padding-right: 2.5px;
min-width: 1.25rem;
min-height: 1.25rem;
}
.avatarButton {
@apply inline-flex justify-center rounded-full;
}
.mobileMenu {
@apply flex lg:hidden ml-6 text-white;
}
.avatarButton:focus,
.mobileMenu:focus {
@apply outline-none;
}
.dropdownDesktop {
@apply hidden -z-10;
}
@media screen(lg) {
.dropdownDesktop {
@apply block;
}
.dropdownMobile {
@apply hidden;
}
}

View File

@ -0,0 +1,66 @@
// import cn from 'clsx'
// import Link from 'next/link'
// import s from './UserNav.module.css'
// import useCart from '@framework/cart/use-cart'
// import { useUI } from '@components/ui/context'
// import { Heart, Bag, Menu } from '@components/icons'
// import CustomerMenuContent from './CustomerMenuContent'
// import useCustomer from '@framework/customer/use-customer'
// import React from 'react'
// import {
// Dropdown,
// DropdownTrigger as DropdownTriggerInst,
// Button,
// } from '@components/ui'
// import type { LineItem } from '@commerce/types/cart'
// const countItem = (count: number, item: LineItem) => count + item.quantity
// const UserNav: React.FC<{
// className?: string
// }> = ({ className }) => {
// const { data } = useCart()
// const { data: isCustomerLoggedIn } = useCustomer()
// const {
// toggleSidebar,
// closeSidebarIfPresent,
// openModal,
// setSidebarView,
// openSidebar,
// displaySidebar,
// } = useUI()
// const itemsCount = data?.lineItems?.reduce(countItem, 0) ?? 0
// const DropdownTrigger = isCustomerLoggedIn
// ? DropdownTriggerInst
// : React.Fragment
// return (
// <nav className={cn(s.root, className)}>
// <ul className={s.list}>
// {process.env.COMMERCE_CART_ENABLED && (
// <li className={s.item}>
// <Button
// className={s.item}
// variant="naked"
// onClick={() => {
// setSidebarView('CART_VIEW')
// openSidebar()
// }}
// aria-label={`Cart items: ${itemsCount}`}
// >
// <Bag className={displaySidebar ? undefined : 'text-white'} />
// {itemsCount > 0 && (
// <span className={s.bagCount}>{itemsCount}</span>
// )}
// </Button>
// </li>
// )}
// </ul>
// </nav>
// )
// }
// export default UserNav

View File

@ -0,0 +1,3 @@
// export { default } from './UserNav'
export { default as MenuSidebarView } from './MenuSidebarView'
export { default as CustomerMenuContent } from './CustomerMenuContent'

View File

@ -0,0 +1,113 @@
header.main {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 50;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
header.main.is-hidden {
transform: translateY(-120px);
opacity: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
header.main nav > div:first-child {
background-color: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
}
header.main nav > div:nth-child(2) {
background-color: rgba(0, 0, 0, 0.75);
border-left: none !important;
border-right: none !important;
}
.sub-nav a.sub-nav__link img {
transition: transform 0.1s ease-out;
}
.sub-nav a.sub-nav__link:hover img {
transform: scale(1.1);
transition: transform 0.1s ease-out;
}
.sub-nav a.sub-nav__link:hover h3 {
color: #075985;
}
.sub-nav {
pointer-events: none;
}
.sub-nav a.sub-nav__link {
pointer-events: all;
}
/* .sub-nav a.sub-nav__link.is-active {
pointer-events: none;
} */
/* img */
.sub-nav:hover a.sub-nav__link img {
filter: blur(3px);
transition: all 0.1s ease-out;
}
/* h3 */
.sub-nav:hover a.sub-nav__link h3 {
filter: blur(3px);
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link h3 {
opacity: 0.5;
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link:hover h3 {
opacity: 1;
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link.is-active h3 {
opacity: 1;
transition: all 0.1s ease-out;
}
.sub-nav:hover a.sub-nav__link:hover {
opacity: 1;
transition: all 0.1s ease-out;
}
/* img */
.sub-nav:hover a.sub-nav__link:hover img {
filter: blur(0px);
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link img {
opacity: 0.5;
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link:hover img {
opacity: 1;
transition: all 0.1s ease-out;
}
.sub-nav.has-active a.sub-nav__link.is-active img {
opacity: 1;
transition: all 0.1s ease-out;
}
/* h3 */
.sub-nav:hover a.sub-nav__link:hover h3 {
filter: blur(0px);
transition: all 0.1s ease-out;
}

View File

@ -0,0 +1,35 @@
.root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
min-height: 74px;
}
.nav {
@apply relative flex flex-row justify-between py-4 md:py-4;
}
.navMenu {
@apply hidden ml-6 space-x-4 lg:block;
}
.link {
@apply inline-flex items-center leading-6
transition ease-in-out duration-75 cursor-pointer
text-accent-5;
}
.link:hover {
@apply text-accent-9;
}
.link:focus {
@apply outline-none text-accent-8;
}
.logo {
@apply cursor-pointer rounded-full border transform duration-100 ease-in-out;
&:hover {
@apply shadow-md;
transform: scale(1.05);
}
}

View File

@ -0,0 +1,703 @@
'use client'
import { FC, useEffect, useMemo, useRef } from 'react'
import Link from 'next/link'
// import { UserNav } from '@components/common'
import { useRouter, usePathname } from 'next/navigation'
import Image from 'next/image'
import { Fragment, useState } from 'react'
import { Dialog, Tab, Transition } from '@headlessui/react'
import {
Bars3Icon,
MagnifyingGlassIcon,
QuestionMarkCircleIcon,
XMarkIcon,
} from '@heroicons/react/24/outline'
// Product data
import { GLOVES_DATA } from 'data/Gloves'
import { INDUSTRIAL_DATA } from 'data/Industrial'
let _scrollTopValue: number | null = null
const navigation = {
pages: [
{ name: 'Industrial', href: '/collections/industrial' },
{ name: 'Abrasive', href: '/collections/abrasives' },
{ name: 'Adhesive', href: '/collections/adhesives' },
{ name: 'Gloves', href: '/collections/gloves' },
{
name: 'Oscillating Accessories',
href: '/collections/oscillating-accessories',
},
],
}
interface Link {
href: string
label: string
}
interface NavbarProps {
links?: Link[]
}
const NAVIGATION = [
{
category: 'Industrial',
link: '/collections/industrial',
// hidden: true,
subMenus: [
{
name: 'Warehouse Racks',
link: '/collections/industrial/warehouse-racks',
image: INDUSTRIAL_DATA.WAREHOUSE_RACKS.navbarImage,
},
{
name: 'Fences',
link: '/collections/industrial/fences',
image: INDUSTRIAL_DATA.FENCES.navbarImage,
},
{
name: 'Pallet Trucks & Forklifts',
link: '/collections/industrial/pallet-trucks-forklifts',
image: INDUSTRIAL_DATA.FORKLIFTS.navbarImage,
},
{
name: 'Warehouse Accessories',
link: '/collections/industrial/warehouse-accessories',
image: INDUSTRIAL_DATA.WAREHOUSE_ACCESSORIES.navbarImage,
},
],
},
{
category: 'Abrasives',
link: '/collections/abrasives',
subMenus: [
{
name: 'Cut off Discs',
link: '/collections/abrasives/cut-off-discs',
image: 'https://linconson-a.netlify.app/Abrasive.jpg',
},
{
name: 'Flap Discs',
link: '/collections/abrasives/flap-discs',
image: 'https://linconson-a.netlify.app/flap.jpg',
},
{
name: 'Grinding Wheels',
link: '/collections/abrasives/metal-grinding-wheel',
image: '/assets/navbar/grinding_wheel.jpeg',
},
{
name: 'Sanding Discs',
link: '/collections/abrasives/sanding-discs',
image: '/assets/navbar/sanding_disc.jpeg',
},
],
},
{
category: 'Adhesives',
link: '/collections/adhesives',
subMenus: [
{
name: 'Grip Tape',
link: '/collections/adhesives/grip-tape',
// image: 'https://linconson-a.netlify.app/gtape.jpg',
image: '/assets/navbar/grip_tape.jpeg',
},
{
name: 'Tread Tape',
link: '/collections/adhesives/tread-tape',
image: '/assets/navbar/tread_tape.jpg',
},
{
name: 'Carpet Tread',
link: '/collections/adhesives/carpet-tread',
image: '/assets/navbar/carpet_tread.jpg',
},
],
},
{
category: 'Gloves',
link: '/collections/gloves',
subMenus: [
{
name: 'Latex foam',
link: '#latex-foam',
image: GLOVES_DATA.LATEX_FOAM.navbarImage,
},
{
name: 'Wrinkled Foam',
link: '#wrinkled-foam',
image: GLOVES_DATA.WRINKLED_FOAM.navbarImage,
},
{
name: 'PU Coated',
link: '#pu-coated',
image: GLOVES_DATA.PU_COATED.navbarImage,
},
{
name: 'PU Cut-resistant',
link: '#pu-cut-resistant',
image: GLOVES_DATA.PU_CUT_RESISTANT.navbarImage,
},
{
name: 'Nitrile Cut-resistant',
link: '#nitrile-cut-resistant',
image: GLOVES_DATA.NITRILE_CUT_RESISTANT.navbarImage,
},
{
name: 'Latex Cut-resistant',
link: '#latex-cut-resistant',
image: GLOVES_DATA.LATEX_CUT_RESISTANT.navbarImage,
},
{
name: 'Nitrile Coated',
link: '#nitrile-coated',
image: GLOVES_DATA.NITRILE_COATED.navbarImage,
},
{
name: 'Spandex',
link: '#spandex',
image: GLOVES_DATA.SPANDEX.navbarImage,
},
{
name: 'Cotton Latex',
link: '#cotton-latex',
image: GLOVES_DATA.COTTON_LATEX.navbarImage,
},
{
name: 'Winter',
link: '#winter',
image: GLOVES_DATA.WINTER.navbarImage,
},
{
name: 'Leather',
link: '#leather',
image: GLOVES_DATA.LEATHER.navbarImage,
},
],
},
{
category: 'Oscillating Accessories',
link: '/collections/oscillating-accessories',
},
]
const Navbar: FC<NavbarProps> = ({ links }) => {
const router = useRouter()
const pathname = usePathname()
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [])
const correspondingNavItem = useMemo(
() =>
isMounted
? NAVIGATION.find((_item) => pathname?.includes(_item.link))
: null,
[pathname, isMounted],
)
const correspondingNavSubMenu = useMemo(() => {
if (!isMounted) return null
if (correspondingNavItem && Array.isArray(correspondingNavItem.subMenus)) {
return (correspondingNavItem.subMenus as object[]).find((subMenu: any) =>
pathname?.includes(subMenu.link),
)
}
}, [correspondingNavItem, pathname, isMounted])
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const isHomePage = useMemo(
() => isMounted && pathname === '/',
[pathname, isMounted],
)
/*
* Fixed nav positioning
*/
const headerRef = useRef<HTMLElement>(null)
const [showNav, setShowNav] = useState(true)
const trackScrolling = () => {
if (typeof window !== 'undefined') {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
if (
(_scrollTopValue === null || scrollTop > _scrollTopValue) &&
scrollTop > 400
) {
setShowNav(false)
} else {
setShowNav(true)
}
_scrollTopValue = scrollTop <= 0 ? 0 : scrollTop
}
}
useEffect(() => {
document.addEventListener('scroll', trackScrolling, { passive: true })
if (typeof window !== 'undefined') {
window.addEventListener('resize', trackScrolling, { passive: true })
}
}, [])
return (
<>
{/* Mobile menu */}
<Transition.Root show={mobileMenuOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50 lg:hidden"
onClose={setMobileMenuOpen}
>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative flex w-full max-w-xs flex-col overflow-y-auto bg-white pb-12 shadow-xl">
<div className="flex px-4 pt-5 pb-2">
<button
type="button"
className="-m-2 inline-flex items-center justify-center rounded-md p-2 text-gray-400"
onClick={() => setMobileMenuOpen(false)}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
{/* Links */}
<Tab.Group as="div" className="mt-2">
<div className="border-b border-gray-200">
<Tab.List className="-mb-px flex space-x-8 px-4">
{/* {navigation.categories.map((category) => (
<Tab
key={category.name}
className={({ selected }) =>
classNames(
selected
? 'text-indigo-600 border-indigo-600'
: 'text-gray-900 border-transparent',
'flex-1 whitespace-nowrap border-b-2 py-4 px-1 text-base font-medium'
)
}
>
{category.name}
</Tab>
))} */}
</Tab.List>
</div>
<Tab.Panels as={Fragment}>
{/* {navigation.categories.map((category) => (
<Tab.Panel
key={category.name}
className="space-y-12 px-4 py-6"
>
<div className="grid grid-cols-2 gap-x-4 gap-y-10">
{category.featured.map((item) => (
<div key={item.name} className="group relative">
<div className="aspect-w-1 aspect-h-1 overflow-hidden rounded-md bg-gray-100 group-hover:opacity-75">
<img
src={item.imageSrc}
alt={item.imageAlt}
className="object-cover object-center"
/>
</div>
<a
href={item.href}
className="mt-6 block text-sm font-medium text-gray-900"
>
<span
className="absolute inset-0 z-10"
aria-hidden="true"
/>
{item.name}
</a>
<p
aria-hidden="true"
className="mt-1 text-sm text-gray-500"
>
Shop now
</p>
</div>
))}
</div>
</Tab.Panel>
))} */}
</Tab.Panels>
</Tab.Group>
<div className="space-y-6 border-t border-gray-200 py-6 px-4">
{navigation.pages.map((page) => (
<div key={page.name} className="flow-root">
<Link
href={page.href}
className="-m-2 block p-2 font-medium text-gray-900"
onClick={(e) => {
setMobileMenuOpen(false)
}}
>
{page.name}
</Link>
</div>
))}
</div>
{/*
<div className="space-y-6 border-t border-gray-200 py-6 px-4">
<div className="flow-root">
<a
href="#"
className="-m-2 block p-2 font-medium text-gray-900"
>
Create an account
</a>
</div>
<div className="flow-root">
<a
href="#"
className="-m-2 block p-2 font-medium text-gray-900"
>
Sign in
</a>
</div>
</div> */}
{/* <div className="space-y-6 border-t border-gray-200 py-6 px-4">
<form>
<div className="inline-block">
<label htmlFor="mobile-currency" className="sr-only">
Currency
</label>
<div className="group relative -ml-2 rounded-md border-transparent focus-within:ring-2 focus-within:ring-white">
<select
id="mobile-currency"
name="currency"
className="flex items-center rounded-md border-transparent bg-none py-0.5 pl-2 pr-5 text-sm font-medium text-gray-700 focus:border-transparent focus:outline-none focus:ring-0 group-hover:text-gray-800"
>
{currencies.map((currency) => (
<option key={currency}>{currency}</option>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center">
<ChevronDownIcon
className="h-5 w-5 text-gray-500"
aria-hidden="true"
/>
</div>
</div>
</div>
</form>
</div> */}
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
{/* Navigation */}
<header className={`main ${!showNav ? 'is-hidden' : ''}`} ref={headerRef}>
<nav aria-label="Top">
{/* Top navigation */}
<div className="">
<div className="mx-auto flex h-10 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="flex items-center space-x-6">
{/* <a
href="#"
className="text-sm font-medium text-white hover:text-gray-100"
>
Sign in
</a>
<a
href="#"
className="text-sm font-medium text-white hover:text-gray-100"
>
Create an account
</a> */}
</div>
</div>
</div>
{/* Secondary navigation */}
<div
className="backdrop-blur-md backdrop-filter"
style={{
// backgroundColor: isHomePage ? undefined : '#192231',
border: isHomePage
? '1px solid rgb(166 180 204 / 21%)'
: '1px solid rgb(166 180 204 / 15%)',
borderRight: 'none',
borderLeft: 'none',
}}
>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div>
<div className="flex h-16 items-center justify-between">
{/* Logo (lg+) */}
<div className="hidden lg:flex lg:flex-1 lg:items-center">
<Link href="/">
<span className="sr-only">Linconson</span>
<Image
className="h-8 w-auto"
src={
pathname?.indexOf('/collections/industrial') === 0
? '/logo-industrial.png'
: '/logo.png'
}
width="100"
height="30"
alt=""
/>
</Link>
</div>
<div className="hidden h-full lg:flex">
<div className="flex h-full justify-center space-x-8">
{NAVIGATION.map((menuContent) => (
<Link
key={menuContent.category}
href={menuContent.link}
className={`flex items-center text-sm font-medium text-white ${
correspondingNavItem
? menuContent.link.includes(
correspondingNavItem.link,
)
? ''
: 'text-opacity-50'
: ''
}`}
>
{menuContent.category}
</Link>
))}
</div>
</div>
{/* Mobile menu and search (lg-) */}
<div className="flex flex-1 items-center lg:hidden">
<button
type="button"
className="-ml-2 p-2 text-white"
onClick={() => setMobileMenuOpen(true)}
>
<span className="sr-only">Open menu</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
{/* Search */}
<Link href="/search" className="ml-2 p-2 text-white">
<span className="sr-only">Search</span>
<MagnifyingGlassIcon
className="h-6 w-6"
aria-hidden="true"
/>
</Link>
</div>
{/* Logo (lg-) */}
<Link href="/" className="lg:hidden">
<span className="sr-only">Linconson</span>
<Image
className="h-8 w-auto"
src={
pathname?.indexOf('/collections/industrial') === 0
? '/logo-industrial.png'
: '/logo.png'
}
width="100"
height="30"
alt=""
/>
</Link>
<div className="flex flex-1 items-center justify-end">
<Link
href="/search"
className="hidden text-sm font-medium text-white lg:flex items-center"
>
Search
<MagnifyingGlassIcon
className="h-4 w-4 ml-2"
aria-hidden="true"
/>
</Link>
<div className="flex items-center lg:ml-8">
{/* Help */}
<Link
href="/contact-us"
className="p-2 text-white lg:hidden"
>
<span className="sr-only">Help</span>
<QuestionMarkCircleIcon
className="h-6 w-6"
aria-hidden="true"
/>
</Link>
<Link
href="/contact-us"
className="hidden text-sm font-medium text-white lg:block"
>
Help
</Link>
{/* Cart */}
<div className="ml-4 flow-root lg:ml-8">
{/* <UserNav /> */}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
{/* <NavbarRoot>
<Container clean className="mx-auto max-w-8xl px-6">
<div className={s.nav}>
<div className="flex items-center flex-1">
<Link href="/" className={s.logo} aria-label="Logo">
<Logo />
</Link>
<nav className={s.navMenu}>
{NAVIGATION.map((menuContent) => (
<Link
key={menuContent.category}
href={menuContent.link}
className={s.link}
>
{menuContent.category}
</Link>
))}
</nav>
</div> */}
{/* {process.env.COMMERCE_SEARCH_ENABLED && (
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
)} */}
{/* <div className="flex items-center justify-end flex-1 space-x-8">
<UserNav />
</div>
</div>
{process.env.COMMERCE_SEARCH_ENABLED && (
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
)}
</Container>
</NavbarRoot> */}
<div
style={{
height:
pathname === '/'
? '100vh'
: headerRef.current
? headerRef.current.offsetHeight
: 106,
}}
></div>
{correspondingNavItem && correspondingNavItem.subMenus && (
<div
className={`bg-gray-100 sub-nav ${
correspondingNavSubMenu ? 'has-active' : ''
}`}
>
<div className="mx-auto max-w-7xl text-white flex flex-wrap justify-center py-8 gap-y-6">
{correspondingNavItem.subMenus?.map((subMenu) => {
const _isActive = pathname?.includes(subMenu.link)
return (
<Link
key={subMenu.name}
className={`flex-none w-auto iphone:w-1/2 py-0 sub-nav__link ${
_isActive ? 'is-active' : ''
}`}
href={subMenu.link}
onClick={
subMenu.link && subMenu.link[0] === '#'
? (e) => {
e.preventDefault()
// Smooth scroll to item
if (subMenu.link && document) {
const elementToScrollTo = document.getElementById(
subMenu.link.replace('#', ''),
)
if (elementToScrollTo) {
elementToScrollTo.scrollIntoView()
}
}
}
: undefined
}
>
<div className="mx-auto max-w-xs items-center px-2 lg:max-w-none lg:px-4 text-center">
<div style={{ height: '3.5rem' }}>
<Image
src={subMenu.image}
alt=""
className="h-14 w-auto mx-auto"
width={60}
height={60}
style={{ mixBlendMode: 'darken' }}
/>
</div>
<h3
className={`text-gray-900 mt-4 text-xs ${
_isActive ? 'font-semibold text-sky-800' : ''
}`}
>
{subMenu.name}
</h3>
</div>
</Link>
)
})}
</div>
</div>
)}
</>
)
}
export default Navbar

View File

@ -49,7 +49,7 @@ export async function Navbar() {
</div>
<div className="hidden justify-center md:flex md:w-1/3">
<Suspense fallback={<SearchSkeleton />}>
<Search />
{/* <Search /> */}
<SearchButton />
</Suspense>
</div>

View File

@ -2,6 +2,7 @@
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import Form from 'next/form'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
export default function Search() {
@ -44,8 +45,14 @@ export function SearchSkeleton() {
export function SearchButton() {
return (
<div>
<h1>Search</h1>
<div className="flex flex-1 items-center justify-end">
<Link
href="/search"
className="hidden text-sm font-medium text-white lg:flex items-center"
>
Search
<MagnifyingGlassIcon className="h-4 w-4 ml-2" aria-hidden="true" />
</Link>
</div>
)
}

72
data/Gloves.tsx Normal file
View File

@ -0,0 +1,72 @@
import LATEX_FOAM from '../app/images/gloves/top_navbar/latex_foam.jpeg'
import WRINKLED_FOAM from '../app/images/gloves/top_navbar/wrinkled_foam.jpeg'
import PU_COATED from '../app/images/gloves/top_navbar/pu_coated.jpeg'
import PU_CUT_RESISTANT from '../app/images/gloves/top_navbar/cut_resistant.jpeg'
import NITRILE_CUT_RESISTANT from '../app/images/gloves/top_navbar/nitrile_cut_resistant.jpeg'
import LATEX_CUT_RESISTANT from '../app/images/gloves/top_navbar/latex_cut_resistant.jpeg'
import NITRILE_COATED from '../app/images/gloves/top_navbar/nitrile_coated.jpeg'
import SPANDEX from '../app/images/gloves/top_navbar/spandex.jpeg'
import COTTON_LATEX from '../app/images/gloves/top_navbar/latex.jpeg'
import WINTER from '../app/images/gloves/top_navbar/winter.jpeg'
import LEATHER from '../app/images/gloves/top_navbar/leather.jpeg'
import LATEX_FOAM_CONTENT_IMAGE from '../app/images/gloves/content/latex_foam.jpeg'
import WRINKLED_FOAM_CONTENT_IMAGE from '../app/images/gloves/content/wrinkled_foam.jpeg'
import PU_COATED_CONTENT_IMAGE from '../app/images/gloves/content/pu_coated.jpeg'
import PU_CUT_RESISTANT_CONTENT_IMAGE from '../app/images/gloves/content/cut_resistant.jpeg'
import NITRILE_CUT_RESISTANT_CONTENT_IMAGE from '../app/images/gloves/content/nitrile_cut_resistant.jpeg'
import LATEX_CUT_RESISTANT_CONTENT_IMAGE from '../app/images/gloves/content/latex_cut_resistant.jpeg'
import NITRILE_COATED_CONTENT_IMAGE from '../app/images/gloves/content/nitrile_coated.jpeg'
import SPANDEX_CONTENT_IMAGE from '../app/images/gloves/content/spandex.jpeg'
import COTTON_LATEX_CONTENT_IMAGE from '../app/images/gloves/content/cotton_latex.jpeg'
import WINTER_CONTENT_IMAGE from '../app/images/gloves/content/winter.jpeg'
import LEATHER_CONTENT_IMAGE from '../app/images/gloves/content/leather.jpeg'
// app/images/gloves/content/cotton_latex.jpeg
export const GLOVES_DATA = {
LATEX_FOAM: {
navbarImage: LATEX_FOAM,
contentImage: LATEX_FOAM_CONTENT_IMAGE,
},
WRINKLED_FOAM: {
navbarImage: WRINKLED_FOAM,
contentImage: WRINKLED_FOAM_CONTENT_IMAGE,
},
PU_COATED: {
navbarImage: PU_COATED,
contentImage: PU_COATED_CONTENT_IMAGE,
},
PU_CUT_RESISTANT: {
navbarImage: PU_CUT_RESISTANT,
contentImage: PU_CUT_RESISTANT_CONTENT_IMAGE,
},
NITRILE_CUT_RESISTANT: {
navbarImage: NITRILE_CUT_RESISTANT,
contentImage: NITRILE_CUT_RESISTANT_CONTENT_IMAGE,
},
LATEX_CUT_RESISTANT: {
navbarImage: LATEX_CUT_RESISTANT,
contentImage: LATEX_CUT_RESISTANT_CONTENT_IMAGE,
},
NITRILE_COATED: {
navbarImage: NITRILE_COATED,
contentImage: NITRILE_COATED_CONTENT_IMAGE,
},
SPANDEX: {
navbarImage: SPANDEX,
contentImage: SPANDEX_CONTENT_IMAGE,
},
COTTON_LATEX: {
navbarImage: COTTON_LATEX,
contentImage: COTTON_LATEX_CONTENT_IMAGE,
},
WINTER: {
navbarImage: WINTER,
contentImage: WINTER_CONTENT_IMAGE,
},
LEATHER: {
navbarImage: LEATHER,
contentImage: LEATHER_CONTENT_IMAGE,
},
}

26
data/Industrial.tsx Normal file
View File

@ -0,0 +1,26 @@
import PALLET_TRUCKS from '../app/images/industrial/top_navbar/pallet_trucks.jpg'
import WAREHOUSE_RACKS from '../app/images/industrial/top_navbar/warehouse_racks.png'
import WAREHOUSE_ACCESSORIES from '../app/images/industrial/top_navbar/warehouse_acc.png'
import FORKLIFTS from '../app/images/industrial/top_navbar/forklifts.png'
import FENCES from '../app/images/industrial/top_navbar/fences.png'
//data/Industrial.tsx
export const INDUSTRIAL_DATA = {
PALLET_TRUCKS: {
navbarImage: PALLET_TRUCKS,
// contentImage: LATEX_FOAM_CONTENT_IMAGE,
},
WAREHOUSE_RACKS: {
navbarImage: WAREHOUSE_RACKS,
},
WAREHOUSE_ACCESSORIES: {
navbarImage: WAREHOUSE_ACCESSORIES,
},
FORKLIFTS: {
navbarImage: FORKLIFTS,
},
FENCES: {
navbarImage: FENCES,
},
}

View File

@ -13,6 +13,7 @@
"@heroicons/react": "^2.2.0",
"clsx": "^2.1.1",
"geist": "^1.3.1",
"lodash.throttle": "^4.1.1",
"next": "15.3.0-canary.13",
"react": "19.0.0",
"react-dom": "19.0.0",

1381
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff