More
This commit is contained in:
@ -4,13 +4,34 @@
|
|||||||
"next/typescript",
|
"next/typescript",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
"prettier"
|
"prettier",
|
||||||
|
"plugin:@next/next/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["prettier"],
|
"plugins": ["prettier", "eslint-plugin-import"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"lines-between-class-members": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"exceptAfterSingleLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc",
|
||||||
|
"caseInsensitive": true
|
||||||
|
},
|
||||||
|
"newlines-between": "always"
|
||||||
|
}
|
||||||
|
], // Optionally, disable the default ESLint sort rule if you're using it
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"react/no-unescaped-entities": "error",
|
"react/no-unescaped-entities": "error",
|
||||||
"react/react-in-jsx-scope": "off"
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": [2, { "ignore": ["className", "position"] }],
|
||||||
|
"import/no-deprecated": "warn"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
|
|||||||
20
components.json
Normal file
20
components.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
package.json
23
package.json
@ -9,15 +9,35 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@medusajs/js-sdk": "^0.0.2-preview-20241001060739",
|
||||||
"@medusajs/medusa": "^1.20.10",
|
"@medusajs/medusa": "^1.20.10",
|
||||||
|
"@medusajs/modules-sdk": "^1.13.0-preview-20241001060739",
|
||||||
|
"@medusajs/pricing": "^0.1.13-preview-20241001060739",
|
||||||
|
"@medusajs/product": "^0.3.13-preview-20241001060739",
|
||||||
|
"@mikro-orm/core": "^5.9.7",
|
||||||
|
"@mikro-orm/postgresql": "^5.9.7",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.1",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-slider": "^1.2.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@tanstack/react-query": "4.22",
|
"@tanstack/react-query": "4.22",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.447.0",
|
||||||
"medusa-react": "^9.0.18",
|
"medusa-react": "^9.0.18",
|
||||||
"next": "14.2.13",
|
"next": "14.2.13",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"swiper": "^11.1.14"
|
"swiper": "^11.1.14",
|
||||||
|
"swr": "^2.2.5",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -26,6 +46,7 @@
|
|||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.13",
|
"eslint-config-next": "14.2.13",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import dryingPic from '/public/images/about-us/03-drying.jpg';
|
|||||||
import chefPic from '/public/images/about-us/04-chef.jpg';
|
import chefPic from '/public/images/about-us/04-chef.jpg';
|
||||||
import barkPic from '/public/images/about-us/05-bark.jpg';
|
import barkPic from '/public/images/about-us/05-bark.jpg';
|
||||||
import italyPic from '/public/images/about-us/06-italy.jpg';
|
import italyPic from '/public/images/about-us/06-italy.jpg';
|
||||||
|
|
||||||
import { AnimatedImage } from '@/components/animated-image';
|
import { AnimatedImage } from '@/components/animated-image';
|
||||||
|
|
||||||
export default function AboutUsPage() {
|
export default function AboutUsPage() {
|
||||||
|
|||||||
@ -4,21 +4,30 @@ import bluePralineBoxOpenPic from '/public/images/diwali/03-blue-praline-box-ope
|
|||||||
import bluePralineBoxTwoPic from '/public/images/diwali/04-blue-praline-box-two.jpg';
|
import bluePralineBoxTwoPic from '/public/images/diwali/04-blue-praline-box-two.jpg';
|
||||||
import goldPralineBoxPic from '/public/images/diwali/05-gold-praline-box.jpg';
|
import goldPralineBoxPic from '/public/images/diwali/05-gold-praline-box.jpg';
|
||||||
import redBarkBoxPic from '/public/images/diwali/06-red-bark-box.jpg';
|
import redBarkBoxPic from '/public/images/diwali/06-red-bark-box.jpg';
|
||||||
import { AnimatedText } from '@/components/animated-text';
|
|
||||||
import { AnimatedImage } from '@/components/animated-image';
|
import { AnimatedImage } from '@/components/animated-image';
|
||||||
|
import { AnimatedText } from '@/components/animated-text';
|
||||||
// import blueSpreadPic from '/public/images/diwali/07-blue-spread.jpg';
|
// import blueSpreadPic from '/public/images/diwali/07-blue-spread.jpg';
|
||||||
|
|
||||||
export default function AboutUsPage() {
|
export default function FestivalPage() {
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className='bg-white overflow-x-clip'>
|
||||||
<section className='flex flex-col items-center'>
|
<section className='flex flex-col items-center'>
|
||||||
<div className='w-full'>
|
<div className='relative w-full h-svh'>
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
src={bannerPic}
|
src={bannerPic}
|
||||||
alt='Praline'
|
alt='Praline'
|
||||||
className='w-full h-auto'
|
className='w-full h-full object-cover object-center'
|
||||||
sizes='100vw'
|
sizes='100vw'
|
||||||
/>
|
/>
|
||||||
|
<a
|
||||||
|
className='absolute left-1/2 transform -translate-x-1/2 bottom-20 pillLink'
|
||||||
|
href='/catalogues/mozimo-diwali-catalogue-2024.pdf'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
Catalog
|
||||||
|
<span className='arrow'>→</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<AnimatedText finishClass='delay-300'>
|
<AnimatedText finishClass='delay-300'>
|
||||||
<h2 className='text-s-headline font-normal text-justify font-samantha p-12'>
|
<h2 className='text-s-headline font-normal text-justify font-samantha p-12'>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import '../globals.css';
|
import '../globals.css';
|
||||||
// import './globalicons.css';
|
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { Footer } from '@/components/footer';
|
import { Footer } from '@/components/footer';
|
||||||
import { Navbar } from '@/components/navbar';
|
import { Navbar } from '@/components/navbar';
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
import brandStoryPic from '/public/images/homepage/brand-story.jpg';
|
import brandStoryPic from '/public/images/homepage/brand-story.jpg';
|
||||||
import { HomepageVideo } from '@/components/homepage-video';
|
|
||||||
import { HeroSwiper } from '@/components/swiper';
|
|
||||||
import { Collections } from '@/components/our-collection';
|
|
||||||
import { ChocolateCategories } from '@/components/category-slider';
|
|
||||||
import { InstagramFeed } from '@/components/instagram';
|
|
||||||
import { Spotlight } from '@/components/spotlight';
|
|
||||||
import { AnimatedText } from '@/components/animated-text';
|
|
||||||
import { AnimatedImage } from '@/components/animated-image';
|
import { AnimatedImage } from '@/components/animated-image';
|
||||||
|
import { AnimatedText } from '@/components/animated-text';
|
||||||
|
import { ChocolateCategories } from '@/components/category-slider';
|
||||||
|
import { HomepageVideo } from '@/components/homepage-video';
|
||||||
|
import { InstagramFeed } from '@/components/instagram';
|
||||||
|
import { Collections } from '@/components/our-collection';
|
||||||
|
import { Spotlight } from '@/components/spotlight';
|
||||||
|
import { HeroSwiper } from '@/components/swiper';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<div className='overflow-x-hidden'>
|
<div className='overflow-x-hidden'>
|
||||||
<HeroSwiper />
|
<HeroSwiper />
|
||||||
{/* First Two-Column Layout */}
|
{/* First Two-Column Layout */}
|
||||||
<section className='flex flex-col md:flex-row items-center bg-white md:h-[90vh] overflow-clip'>
|
<section className='flex flex-col md:flex-row items-center bg-white h-[90vh] overflow-clip'>
|
||||||
{/* Left Column - Text with Header */}
|
{/* Left Column - Text with Header */}
|
||||||
<div className='w-full md:w-1/2 h-[90vh] md:h-auto'>
|
<div className='w-full md:w-1/2 md:h-auto'>
|
||||||
<div className='m-8 space-y-10'>
|
<div className='m-8 space-y-10'>
|
||||||
<AnimatedText finishClass='delay-300'>
|
<AnimatedText finishClass='delay-300'>
|
||||||
<h2 className='text-s-title font-normal text-justify font-montera'>
|
<h2 className='text-s-title font-normal text-justify font-montera'>
|
||||||
@ -43,7 +44,7 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Right Column - Image */}
|
{/* Right Column - Image */}
|
||||||
<div className='w-full md:w-1/2 items-start h-[90vh] md:h-auto'>
|
<div className='w-full md:w-1/2 items-start'>
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
// ref={brandStoryPicRef}
|
// ref={brandStoryPicRef}
|
||||||
src={brandStoryPic}
|
src={brandStoryPic}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import '../globals.css';
|
import '../globals.css';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Providers } from '../providers';
|
|
||||||
|
import { Providers } from '@/app/providers';
|
||||||
import { Footer } from '@/components/footer';
|
import { Footer } from '@/components/footer';
|
||||||
import { Navbar } from '@/components/navbar';
|
import { Navbar } from '@/components/navbar';
|
||||||
|
|
||||||
|
|||||||
262
src/app/(storefront)/login/page.tsx
Normal file
262
src/app/(storefront)/login/page.tsx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
export default function AuthPageComponent() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('login');
|
||||||
|
const [firstName, setFirstName] = useState('');
|
||||||
|
const [lastName, setLastName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const handleSubmitLogin = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Handle form submission here
|
||||||
|
if (!email || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// obtain JWT token
|
||||||
|
const { token } = await fetch(
|
||||||
|
`http://localhost:9000/auth/customer/emailpass`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env
|
||||||
|
.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
).then((res) => res.json());
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create customer
|
||||||
|
const { customer } = await fetch(
|
||||||
|
`http://localhost:9000/store/customers/me`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'x-publishable-api-key': process.env
|
||||||
|
.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).then((res) => res.json());
|
||||||
|
|
||||||
|
console.log(token, customer);
|
||||||
|
// Save the token and customer to localStorage
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
localStorage.setItem('user', customer);
|
||||||
|
setLoading(false);
|
||||||
|
router.push('/products');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitRegister = async (
|
||||||
|
event: React.FormEvent<HTMLFormElement>,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Handle form submission here
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// obtain JWT token
|
||||||
|
const { token } = await fetch(
|
||||||
|
`http://localhost:9000/auth/customer/emailpass/register`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env
|
||||||
|
.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
).then((res) => res.json());
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create customer
|
||||||
|
const { customer } = await fetch(`http://localhost:9000/store/customers`, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'x-publishable-api-key': process.env
|
||||||
|
.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY as string,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email,
|
||||||
|
}),
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
console.log(token, customer);
|
||||||
|
// Save the token and customer to localStorage
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
localStorage.setItem('user', customer);
|
||||||
|
setLoading(false);
|
||||||
|
router.push('/products');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen flex items-center justify-center bg-gray-100 px-4 py-12 sm:px-6 lg:px-8'>
|
||||||
|
<Card className='w-full max-w-md'>
|
||||||
|
<CardHeader className='space-y-1'>
|
||||||
|
<CardTitle className='text-2xl font-bold'>
|
||||||
|
Welcome to Our E-commerce Store
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Login or create an account to start shopping
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
|
<TabsTrigger value='login'>Login</TabsTrigger>
|
||||||
|
<TabsTrigger value='register'>Register</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='login'>
|
||||||
|
<form onSubmit={handleSubmitLogin} className='space-y-4'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='login-email'>Email</Label>
|
||||||
|
<Input
|
||||||
|
id='login-email'
|
||||||
|
type='email'
|
||||||
|
placeholder='m@example.com'
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
name='email'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='login-password'>Password</Label>
|
||||||
|
<Input
|
||||||
|
id='login-password'
|
||||||
|
type='password'
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
name='password'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type='submit' className='w-full'>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='register'>
|
||||||
|
<form onSubmit={handleSubmitRegister} className='space-y-4'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-first-name'>First Name</Label>
|
||||||
|
<Input
|
||||||
|
id='register-first-name'
|
||||||
|
type='text'
|
||||||
|
placeholder='John'
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-last-name'>Last Name</Label>
|
||||||
|
<Input
|
||||||
|
id='register-last-name'
|
||||||
|
type='text'
|
||||||
|
placeholder='Doe'
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-email'>Email</Label>
|
||||||
|
<Input
|
||||||
|
id='register-email'
|
||||||
|
type='email'
|
||||||
|
placeholder='m@example.com'
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-password'>Password</Label>
|
||||||
|
<Input
|
||||||
|
id='register-password'
|
||||||
|
type='password'
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type='submit' className='w-full'>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex flex-wrap items-center justify-between gap-2'>
|
||||||
|
<div className='text-sm text-muted-foreground'>
|
||||||
|
<span className='mr-1 hidden sm:inline-block'>
|
||||||
|
{activeTab === 'login'
|
||||||
|
? "Don't have an account?"
|
||||||
|
: 'Already have an account?'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
className='p-0'
|
||||||
|
onClick={() =>
|
||||||
|
setActiveTab(activeTab === 'login' ? 'register' : 'login')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{activeTab === 'login' ? 'Sign up' : 'Log in'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{activeTab === 'login' && (
|
||||||
|
<Button variant='link' className='p-0'>
|
||||||
|
Forgot password?
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,28 +1,75 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { FC } from 'react';
|
import { ChevronLeft, ChevronRight, ShoppingCart, Star } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import Image from 'next/image';
|
||||||
import { getProductByHandle } from '@/lib/product-by-handle';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
import { Product, ProductVariant } from '@/lib/product-interface';
|
||||||
|
|
||||||
interface ProductDetailsProps {
|
interface ProductDetailsProps {
|
||||||
handle: string;
|
handle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductDetails: FC<ProductDetailsProps> = ({ handle }) => {
|
const ProductDetails: FC<ProductDetailsProps> = ({ handle }) => {
|
||||||
const [product, setProduct] = useState<PricedProduct | null>(null);
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
|
const [product, setProduct] = useState<Product | null>();
|
||||||
|
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<unknown>(null);
|
const [error, setError] = useState<unknown>(null);
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
if (product) {
|
||||||
|
setCurrentImage((currentImage + 1) % product.images.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevImage = () => {
|
||||||
|
if (product) {
|
||||||
|
setCurrentImage(
|
||||||
|
(currentImage - 1 + product.images.length) % product.images.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProduct = async () => {
|
const fetchProduct = async () => {
|
||||||
try {
|
try {
|
||||||
const { product } = await getProductByHandle(handle);
|
const params = new URLSearchParams({
|
||||||
|
handle: handle,
|
||||||
|
fields: `*variants.calculated_price`,
|
||||||
|
region_id: 'reg_01J9412Z2W1GV785DV2E4X6NSE',
|
||||||
|
});
|
||||||
|
const products: Product[] = await fetch(
|
||||||
|
`http://localhost:9000/store/products?${params}`,
|
||||||
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key':
|
||||||
|
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ||
|
||||||
|
'pk_6c28ea35a3372ba52adabcd91a000151e139de469fd340743cc0d20fe3b9df97',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.products);
|
||||||
|
console.log(products);
|
||||||
|
|
||||||
|
const product = products[0];
|
||||||
console.log(product);
|
console.log(product);
|
||||||
if (!product) {
|
if (!product) {
|
||||||
setProduct(null);
|
setProduct(null);
|
||||||
} else {
|
} else {
|
||||||
setProduct(product);
|
setProduct(product);
|
||||||
|
setSelectedVariant(product.variants[0]);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(err);
|
setError(err);
|
||||||
@ -48,29 +95,163 @@ const ProductDetails: FC<ProductDetailsProps> = ({ handle }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container mx-auto pt-28'>
|
<div className='container mx-auto px-4 py-8'>
|
||||||
<h1 className='text-4xl font-bold'>{product.title}</h1>
|
<div className='grid md:grid-cols-2 gap-8'>
|
||||||
<div className='flex flex-col md:flex-row gap-6 mt-4'>
|
<div className='relative'>
|
||||||
{/* Product Images */}
|
<Image
|
||||||
<div className='flex-1'>
|
width={200}
|
||||||
{!!product.images?.length && (
|
height={192}
|
||||||
<img
|
src={product.images[currentImage].url}
|
||||||
src={product.images[0].url}
|
alt={`${product.title} image ${currentImage + 1}`}
|
||||||
alt={product.title}
|
className='w-full h-auto rounded-lg shadow-lg'
|
||||||
className='rounded-lg shadow-lg'
|
|
||||||
/>
|
/>
|
||||||
)}
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='absolute left-2 top-1/2 transform -translate-y-1/2'
|
||||||
|
onClick={prevImage}
|
||||||
|
>
|
||||||
|
<ChevronLeft className='h-4 w-4' />
|
||||||
|
<span className='sr-only'>Previous image</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='absolute right-2 top-1/2 transform -translate-y-1/2'
|
||||||
|
onClick={nextImage}
|
||||||
|
>
|
||||||
|
<ChevronRight className='h-4 w-4' />
|
||||||
|
<span className='sr-only'>Next image</span>
|
||||||
|
</Button>
|
||||||
|
<div className='flex justify-center mt-4 space-x-2'>
|
||||||
|
{product.images.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-3 h-3 rounded-full ${
|
||||||
|
index === currentImage ? 'bg-primary' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className='text-3xl font-bold mb-4'>{product.title}</h1>
|
||||||
|
<div className='flex items-center mb-4'>
|
||||||
|
<div className='flex text-yellow-400'>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star key={i} className='w-5 h-5 fill-current' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 text-sm text-gray-600'>(128 reviews)</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-2xl font-bold mb-4'>
|
||||||
|
${selectedVariant?.calculated_price?.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
<p className='text-gray-600 mb-6'>{product.description}</p>
|
||||||
|
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Choose Variant:</h3>
|
||||||
|
<RadioGroup
|
||||||
|
defaultValue={selectedVariant?.id}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSelectedVariant(
|
||||||
|
product.variants.find((v) => v.id === value) ||
|
||||||
|
product.variants[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{product.variants.map((variant) => (
|
||||||
|
<div key={variant.id} className='flex items-center space-x-2'>
|
||||||
|
<RadioGroupItem value={variant.id} id={variant.id} />
|
||||||
|
<Label htmlFor={variant.id}>
|
||||||
|
{variant.title} - ${variant?.calculated_price?.toFixed(2)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Product Info */}
|
<div className='flex items-center mb-6'>
|
||||||
<div className='flex-1'>
|
<Button
|
||||||
<p className='text-lg font-semibold text-gray-500'>
|
variant='outline'
|
||||||
Price: ${product.variants[0].prices[0].amount / 100}
|
size='sm'
|
||||||
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</Button>
|
||||||
|
<span className='mx-4'>{quantity}</span>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button className='w-full md:w-auto'>
|
||||||
|
<ShoppingCart className='mr-2 h-4 w-4' /> Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Product Details</h2>
|
||||||
|
<ul className='list-disc pl-5 space-y-2 text-gray-600'>
|
||||||
|
<li>Single-origin cocoa beans from Ecuador</li>
|
||||||
|
<li>Fair trade certified</li>
|
||||||
|
<li>No artificial flavors or preservatives</li>
|
||||||
|
<li>100g (3.5oz) bar</li>
|
||||||
|
<li>Available in different cocoa percentages</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Customer Reviews</h2>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className='p-4'>
|
||||||
|
<div className='flex items-center mb-2'>
|
||||||
|
<div className='flex text-yellow-400'>
|
||||||
|
{[...Array(5)].map((_, j) => (
|
||||||
|
<Star key={j} className='w-4 h-4 fill-current' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 text-sm font-medium'>John Doe</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
This chocolate is absolutely divine! The rich flavor and
|
||||||
|
smooth texture make it a perfect treat for any chocolate
|
||||||
|
lover. I especially love the 85% cocoa variant for its intense
|
||||||
|
flavor.
|
||||||
</p>
|
</p>
|
||||||
<p className='text-lg text-gray-700'>{product.description}</p>
|
</CardContent>
|
||||||
<button className='mt-4 px-6 py-2 bg-black text-white rounded hover:bg-gray-700'>
|
</Card>
|
||||||
Add to Cart
|
))}
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Related Products</h2>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-6'>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className='p-4'>
|
||||||
|
<Image
|
||||||
|
width={200}
|
||||||
|
height={192}
|
||||||
|
src={product.images[0].url}
|
||||||
|
alt={`Related product ${i + 1}`}
|
||||||
|
className='w-full h-48 object-cover mb-4 rounded'
|
||||||
|
/>
|
||||||
|
<h3 className='font-bold mb-2'>Milk Chocolate Truffles</h3>
|
||||||
|
<p className='text-gray-600 mb-2'>
|
||||||
|
Smooth, creamy milk chocolate truffles
|
||||||
|
</p>
|
||||||
|
<p className='font-bold'>$9.99</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { getProductsList } from '@/lib/product';
|
|
||||||
import ProductDetails from './ProductDetails';
|
import ProductDetails from './ProductDetails';
|
||||||
|
|
||||||
interface ProductPageProps {
|
interface ProductPageProps {
|
||||||
@ -7,17 +7,30 @@ interface ProductPageProps {
|
|||||||
handle: string;
|
handle: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface Product {
|
||||||
|
handle: string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
// Fetch the list of products
|
// Fetch the list of products
|
||||||
const products = await getProductsList({}).then(
|
const products: Product[] = await fetch(
|
||||||
({ response }) => response.products,
|
`http://localhost:9000/store/products`,
|
||||||
);
|
{
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key':
|
||||||
|
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ||
|
||||||
|
'pk_6c28ea35a3372ba52adabcd91a000151e139de469fd340743cc0d20fe3b9df97',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.products);
|
||||||
|
|
||||||
if (!products) {
|
if (!products) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate static params based on product handles
|
// Generate static params based on product handles
|
||||||
const staticParams = products.map((product) => ({
|
const staticParams = products.map((product) => ({
|
||||||
handle: product.handle,
|
handle: product.handle,
|
||||||
|
|||||||
@ -1,16 +1,39 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useProducts } from 'medusa-react';
|
import { HttpTypes } from '@medusajs/types';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
const { products, isLoading, error } = useProducts();
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
if (isLoading) {
|
const [products, setProducts] = useState<HttpTypes.StoreProduct[]>([]);
|
||||||
return <div>Loading...</div>;
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (error) return <p>Error: {error.message}</p>;
|
const params = new URLSearchParams({
|
||||||
|
fields: `*variants.calculated_price`,
|
||||||
|
region_id: 'reg_01J9412Z2W1GV785DV2E4X6NSE',
|
||||||
|
});
|
||||||
|
fetch(`http://localhost:9000/store/products?${params}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key':
|
||||||
|
process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ||
|
||||||
|
'pk_6c28ea35a3372ba52adabcd91a000151e139de469fd340743cc0d20fe3b9df97',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.products)
|
||||||
|
.then((data) => {
|
||||||
|
setProducts(data);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container mx-auto py-12 pt-28'>
|
<div className='container mx-auto py-12 pt-28'>
|
||||||
@ -29,7 +52,7 @@ export default function ProductsPage() {
|
|||||||
<h2 className='mt-4 text-lg font-semibold'>{product.title}</h2>
|
<h2 className='mt-4 text-lg font-semibold'>{product.title}</h2>
|
||||||
<p className='mt-2 text-gray-600'>{product.description}</p>
|
<p className='mt-2 text-gray-600'>{product.description}</p>
|
||||||
<p className='mt-2 text-lg font-bold text-gray-900'>
|
<p className='mt-2 text-lg font-bold text-gray-900'>
|
||||||
${product.variants[0]?.prices[0].amount / 100}
|
{/* ${product.variants?[0]?.calculated_price / 100} */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -3,29 +3,15 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #f8f6f5;
|
--normal-font: 1.25rem;
|
||||||
--foreground: #703133;
|
--title-font: 3rem;
|
||||||
--normal-font: 2rem;
|
--headline-font: 5rem;
|
||||||
--title-font: 5rem;
|
--items-font: 6.25rem;
|
||||||
--headline-font: 8rem;
|
|
||||||
--items-font: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #f8f6f5;
|
|
||||||
--foreground: #703133;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 62.5%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--foreground);
|
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
font-family: Renner, sans-serif;
|
font-family: Renner, sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
@ -133,3 +119,88 @@ animation-range-end: contain; */
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 10% 3.9%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillLink {
|
||||||
|
@apply bg-white text-black rounded-full px-6 py-2 text-lg font-sans shadow-md transition duration-300 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillLink:not(:hover) .arrow {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
@apply ml-2 transform -translate-x-5 opacity-0 transition-transform duration-300 ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillLink:hover .arrow {
|
||||||
|
@apply transform duration-300 translate-x-0 opacity-100 transition-transform ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillLink:hover {
|
||||||
|
@apply shadow-lg transform duration-300 transition-transform ease-in-out;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
// src/app/providers.tsx
|
// src/app/providers.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ReactNode, useState } from 'react';
|
|
||||||
import { MedusaProvider, CartProvider } from 'medusa-react';
|
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import { CartProvider, MedusaProvider } from 'medusa-react';
|
||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
interface ProvidersProps {
|
interface ProvidersProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -16,6 +16,7 @@ export function Providers({ children }: ProvidersProps) {
|
|||||||
<MedusaProvider
|
<MedusaProvider
|
||||||
baseUrl='http://localhost:9000' // Replace with your Medusa backend URL
|
baseUrl='http://localhost:9000' // Replace with your Medusa backend URL
|
||||||
queryClientProviderProps={{ client: queryClient }}
|
queryClientProviderProps={{ client: queryClient }}
|
||||||
|
publishableApiKey='pk_6c28ea35a3372ba52adabcd91a000151e139de469fd340743cc0d20fe3b9df97'
|
||||||
>
|
>
|
||||||
<CartProvider>{children}</CartProvider>
|
<CartProvider>{children}</CartProvider>
|
||||||
</MedusaProvider>
|
</MedusaProvider>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pillButton:hover {
|
.pillButton:hover {
|
||||||
@apply shadow-lg transform duration-300 translate-x-0 opacity-100 transition-transform ease-in-out;
|
@apply shadow-lg transform duration-300 transition-transform ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import styles from './PillButton.module.css';
|
import styles from './PillButton.module.css';
|
||||||
|
|
||||||
type PillButtonProps = {
|
type PillButtonProps = {
|
||||||
text: string; // text prop is now required
|
text: string; // text prop is now required
|
||||||
|
className?: string;
|
||||||
|
// Additional props if needed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
onClick?: () => void; // onClick is optional
|
onClick?: () => void; // onClick is optional
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PillButton({ text, onClick }: PillButtonProps) {
|
export function PillButton({
|
||||||
|
text,
|
||||||
|
className = '',
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: PillButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className={styles.pillButton}>
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${styles.pillButton} ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
<span className={styles.arrow}>→</span>
|
<span className={styles.arrow}>→</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
// src/components/AnimatedImage.tsx
|
// src/components/AnimatedImage.tsx
|
||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
|
||||||
import useInView from '@/hooks/useInView';
|
|
||||||
import Image, { StaticImageData } from 'next/image';
|
import Image, { StaticImageData } from 'next/image';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import useInView from '@/hooks/useInView';
|
||||||
|
|
||||||
interface AnimatedImageProps {
|
interface AnimatedImageProps {
|
||||||
src: string | StaticImageData;
|
src: string | StaticImageData;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import useInView from '@/hooks/useInView';
|
import useInView from '@/hooks/useInView';
|
||||||
|
|
||||||
interface AnimatedLiProps {
|
interface AnimatedLiProps {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import useInView from '@/hooks/useInView';
|
import useInView from '@/hooks/useInView';
|
||||||
|
|
||||||
interface AnimatedTextProps {
|
interface AnimatedTextProps {
|
||||||
|
|||||||
121
src/components/auth-page.tsx
Normal file
121
src/components/auth-page.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
export function AuthPageComponent() {
|
||||||
|
const [activeTab, setActiveTab] = useState('login');
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// Handle form submission here
|
||||||
|
console.log('Form submitted:', activeTab);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='min-h-screen flex items-center justify-center bg-gray-100 px-4 py-12 sm:px-6 lg:px-8'>
|
||||||
|
<Card className='w-full max-w-md'>
|
||||||
|
<CardHeader className='space-y-1'>
|
||||||
|
<CardTitle className='text-2xl font-bold'>
|
||||||
|
Welcome to the Mozimo Store
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Login or create an account to start shopping
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
<TabsList className='grid w-full grid-cols-2'>
|
||||||
|
<TabsTrigger value='login'>Login</TabsTrigger>
|
||||||
|
<TabsTrigger value='register'>Register</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value='login'>
|
||||||
|
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='login-email'>Email</Label>
|
||||||
|
<Input
|
||||||
|
id='login-email'
|
||||||
|
type='email'
|
||||||
|
placeholder='m@example.com'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='login-password'>Password</Label>
|
||||||
|
<Input id='login-password' type='password' required />
|
||||||
|
</div>
|
||||||
|
<Button type='submit' className='w-full'>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value='register'>
|
||||||
|
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-name'>Name</Label>
|
||||||
|
<Input
|
||||||
|
id='register-name'
|
||||||
|
type='text'
|
||||||
|
placeholder='John Doe'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-email'>Email</Label>
|
||||||
|
<Input
|
||||||
|
id='register-email'
|
||||||
|
type='email'
|
||||||
|
placeholder='m@example.com'
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='register-password'>Password</Label>
|
||||||
|
<Input id='register-password' type='password' required />
|
||||||
|
</div>
|
||||||
|
<Button type='submit' className='w-full'>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className='flex flex-wrap items-center justify-between gap-2'>
|
||||||
|
<div className='text-sm text-muted-foreground'>
|
||||||
|
<span className='mr-1 hidden sm:inline-block'>
|
||||||
|
{activeTab === 'login'
|
||||||
|
? "Don't have an account?"
|
||||||
|
: 'Already have an account?'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant='link'
|
||||||
|
className='p-0'
|
||||||
|
onClick={() =>
|
||||||
|
setActiveTab(activeTab === 'login' ? 'register' : 'login')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{activeTab === 'login' ? 'Sign up' : 'Log in'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{activeTab === 'login' && (
|
||||||
|
<Button variant='link' className='p-0'>
|
||||||
|
Forgot password?
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import styles from './category-slider.module.css';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { AnimatedLi } from './animated-link';
|
import { AnimatedLi } from './animated-link';
|
||||||
|
import styles from './category-slider.module.css';
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ name: 'Bars', image: '/images/categories/bars.jpg' },
|
{ name: 'Bars', image: '/images/categories/bars.jpg' },
|
||||||
@ -29,7 +30,7 @@ export function ChocolateCategories() {
|
|||||||
startClass={startClass}
|
startClass={startClass}
|
||||||
finishClass={finishClass}
|
finishClass={finishClass}
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
className={styles.productImage}
|
className={styles.productImage}
|
||||||
alt={category.name}
|
alt={category.name}
|
||||||
src={category.image}
|
src={category.image}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { FaMapLocationDot } from 'react-icons/fa6';
|
import Image from 'next/image';
|
||||||
import {
|
import {
|
||||||
FaPhoneAlt,
|
|
||||||
FaFacebook,
|
FaFacebook,
|
||||||
FaInstagram,
|
FaInstagram,
|
||||||
FaYoutube,
|
|
||||||
FaLinkedin,
|
FaLinkedin,
|
||||||
|
FaPhoneAlt,
|
||||||
|
FaYoutube,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import Image from 'next/image';
|
import { FaMapLocationDot } from 'react-icons/fa6';
|
||||||
|
|
||||||
import logoPic from '/public/images/logo-puce-red.svg';
|
import logoPic from '/public/images/logo-puce-red.svg';
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { InstagramPost, fetchInstagramPosts } from '@/lib/instagram';
|
import Image from 'next/image';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { fetchInstagramPosts, InstagramPost } from '@/lib/instagram';
|
||||||
|
|
||||||
export function InstagramFeed() {
|
export function InstagramFeed() {
|
||||||
const [posts, setPosts] = useState<InstagramPost[]>([]);
|
const [posts, setPosts] = useState<InstagramPost[]>([]);
|
||||||
|
|
||||||
@ -19,7 +21,7 @@ export function InstagramFeed() {
|
|||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<div key={post.id} className='flex flex-col items-center'>
|
<div key={post.id} className='flex flex-col items-center'>
|
||||||
<a href={post.permalink} target='_blank' rel='noopener noreferrer'>
|
<a href={post.permalink} target='_blank' rel='noopener noreferrer'>
|
||||||
<img
|
<Image
|
||||||
src={post.media_url}
|
src={post.media_url}
|
||||||
alt={post.caption}
|
alt={post.caption}
|
||||||
className='rounded-lg shadow-lg'
|
className='rounded-lg shadow-lg'
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
// components/Layout.js
|
// components/Layout.js
|
||||||
import { Footer } from './footer';
|
|
||||||
import { ReactNode } from 'react'; // Import ReactNode
|
import { ReactNode } from 'react'; // Import ReactNode
|
||||||
|
|
||||||
|
import { Footer } from './footer';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: ReactNode; // Define the type of children prop
|
children: ReactNode; // Define the type of children prop
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: hsl(0 0% 100% / .2);
|
background: hsl(0 0% 100% / .2);
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: blur(1rem);
|
||||||
font-size: 2rem;
|
font-size: 1.25rem;
|
||||||
line-height: 2.8rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -28,9 +28,9 @@
|
|||||||
.mobileNavToggle {
|
.mobileNavToggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
width: 3.2rem;
|
width: 2rem;
|
||||||
top: 3.2rem;
|
top: 2rem;
|
||||||
left: 3.2rem;
|
left: 2rem;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
background-image: url('/icons/menu.svg');
|
background-image: url('/icons/menu.svg');
|
||||||
@ -50,14 +50,14 @@
|
|||||||
.navBar {
|
.navBar {
|
||||||
right: 0;
|
right: 0;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 8rem;
|
height: 5rem;
|
||||||
border-radius:0 0 0.8rem 0.8rem;
|
border-radius:0 0 0.5rem 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navBar > div {
|
.navBar > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap, 3.2rem);
|
gap: var(--gap, 2rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .navBarInitial {
|
/* .navBarInitial {
|
||||||
@ -73,10 +73,6 @@
|
|||||||
backdrop-filter: blur(10px); /* Adds a blur effect */
|
backdrop-filter: blur(10px); /* Adds a blur effect */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cls-1, .cls-2 {
|
|
||||||
fill: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transparent / almost transparent to white
|
/* Transparent / almost transparent to white
|
||||||
.navBarInitial {
|
.navBarInitial {
|
||||||
@apply bg-gradient-to-b from-black/50 to-black/30 text-white absolute rounded-lg;
|
@apply bg-gradient-to-b from-black/50 to-black/30 text-white absolute rounded-lg;
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FiSearch, FiUser } from 'react-icons/fi';
|
import { FiSearch, FiUser } from 'react-icons/fi';
|
||||||
import { HiShoppingBag } from 'react-icons/hi2';
|
import { HiShoppingBag } from 'react-icons/hi2';
|
||||||
import styles from './navbar.module.css';
|
|
||||||
import Image from 'next/image';
|
|
||||||
// import logoWhite from '/public/images/logo-white.svg';
|
// import logoWhite from '/public/images/logo-white.svg';
|
||||||
import logoBlack from '/public/images/logo-black.svg';
|
import logoBlack from '/public/images/logo-black.svg';
|
||||||
|
|
||||||
|
import styles from './navbar.module.css';
|
||||||
|
|
||||||
// components/Navbar.tsx
|
// components/Navbar.tsx
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const prevScrollY = useRef(0);
|
const prevScrollY = useRef(0);
|
||||||
@ -94,9 +96,9 @@ export function Navbar() {
|
|||||||
<button aria-label='Profile'>
|
<button aria-label='Profile'>
|
||||||
<FiUser size={20} />
|
<FiUser size={20} />
|
||||||
</button>
|
</button>
|
||||||
<button aria-label='Shopping Cart'>
|
<a aria-label='Shopping Cart' href='/products'>
|
||||||
<HiShoppingBag size={20} />
|
<HiShoppingBag size={20} />
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
src/components/our-collection.module.css
Normal file
7
src/components/our-collection.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.ourCollection {
|
||||||
|
@apply w-full h-auto object-cover hover:scale-110;
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: 1400ms;
|
||||||
|
transition-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import styles from './our-collection.module.css';
|
||||||
|
|
||||||
const collections = [
|
const collections = [
|
||||||
{
|
{
|
||||||
title: 'Bestsellers',
|
title: 'Bestsellers',
|
||||||
@ -26,7 +28,7 @@ export function Collections() {
|
|||||||
alt={collection.title}
|
alt={collection.title}
|
||||||
width={500}
|
width={500}
|
||||||
height={300}
|
height={300}
|
||||||
className='w-full h-auto object-cover transition transform hover:scale-110 duration-[1400ms] linear'
|
className={styles.ourCollection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='p-4'>
|
<div className='p-4'>
|
||||||
|
|||||||
162
src/components/product-list.tsx
Normal file
162
src/components/product-list.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SearchIcon, SlidersHorizontal } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
|
||||||
|
// Mock product data
|
||||||
|
const products = [
|
||||||
|
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999.99 },
|
||||||
|
{ id: 2, name: 'Smartphone', category: 'Electronics', price: 699.99 },
|
||||||
|
{ id: 3, name: 'Headphones', category: 'Electronics', price: 199.99 },
|
||||||
|
{ id: 4, name: 'T-shirt', category: 'Clothing', price: 24.99 },
|
||||||
|
{ id: 5, name: 'Jeans', category: 'Clothing', price: 49.99 },
|
||||||
|
{ id: 6, name: 'Sneakers', category: 'Footwear', price: 89.99 },
|
||||||
|
{ id: 7, name: 'Watch', category: 'Accessories', price: 149.99 },
|
||||||
|
{ id: 8, name: 'Backpack', category: 'Accessories', price: 79.99 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const categories = ['Electronics', 'Clothing', 'Footwear', 'Accessories'];
|
||||||
|
|
||||||
|
export function ProductListComponent() {
|
||||||
|
const [priceRange, setPriceRange] = useState([0, 1000]);
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState('name');
|
||||||
|
|
||||||
|
const handleCategoryChange = (category: string) => {
|
||||||
|
setSelectedCategories((prev) =>
|
||||||
|
prev.includes(category)
|
||||||
|
? prev.filter((c) => c !== category)
|
||||||
|
: [...prev, category],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredProducts = products
|
||||||
|
.filter(
|
||||||
|
(product) =>
|
||||||
|
(selectedCategories.length === 0 ||
|
||||||
|
selectedCategories.includes(product.category)) &&
|
||||||
|
product.price >= priceRange[0] &&
|
||||||
|
product.price <= priceRange[1] &&
|
||||||
|
product.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (sortBy === 'price') return a.price - b.price;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container mx-auto px-4 py-8'>
|
||||||
|
<div className='flex flex-col md:flex-row gap-8'>
|
||||||
|
{/* Sidebar with filters */}
|
||||||
|
<aside className='w-full md:w-1/4 space-y-6'>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filters</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-6'>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Categories</h3>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div key={category} className='flex items-center space-x-2'>
|
||||||
|
<Checkbox
|
||||||
|
id={category}
|
||||||
|
checked={selectedCategories.includes(category)}
|
||||||
|
onCheckedChange={() => handleCategoryChange(category)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={category}
|
||||||
|
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Price Range</h3>
|
||||||
|
<Slider
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
step={10}
|
||||||
|
value={priceRange}
|
||||||
|
onValueChange={setPriceRange}
|
||||||
|
/>
|
||||||
|
<div className='flex justify-between mt-2'>
|
||||||
|
<span>${priceRange[0]}</span>
|
||||||
|
<span>${priceRange[1]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<main className='w-full md:w-3/4'>
|
||||||
|
<div className='flex flex-col sm:flex-row justify-between items-center mb-6 gap-4'>
|
||||||
|
<div className='relative w-full sm:w-auto'>
|
||||||
|
<SearchIcon className='absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400' />
|
||||||
|
<Input
|
||||||
|
type='search'
|
||||||
|
placeholder='Search products...'
|
||||||
|
className='pl-8'
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-2 w-full sm:w-auto'>
|
||||||
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
|
<SelectTrigger className='w-full sm:w-[180px]'>
|
||||||
|
<SelectValue placeholder='Sort by' />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value='name'>Name</SelectItem>
|
||||||
|
<SelectItem value='price'>Price</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant='outline' size='icon'>
|
||||||
|
<SlidersHorizontal className='h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'>
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<Card key={product.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{product.name}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Category: {product.category}</p>
|
||||||
|
<p className='font-bold mt-2'>${product.price.toFixed(2)}</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button className='w-full'>Add to Cart</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
216
src/components/product-page.tsx
Normal file
216
src/components/product-page.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChevronLeft, ChevronRight, ShoppingCart, Star } from 'lucide-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
|
||||||
|
interface Variant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
variants: Variant[];
|
||||||
|
images: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductPageComponent() {
|
||||||
|
const product: Product = {
|
||||||
|
name: 'Luxury Dark Chocolate Bar',
|
||||||
|
description:
|
||||||
|
'Indulge in the rich, intense flavor of our premium dark chocolate bar. Made with carefully selected cocoa beans, this bar offers a perfect balance of bitterness and sweetness that will satisfy even the most discerning chocolate connoisseurs.',
|
||||||
|
variants: [
|
||||||
|
{ id: '70', name: '70% Cocoa', price: 12.99 },
|
||||||
|
{ id: '85', name: '85% Cocoa', price: 14.99 },
|
||||||
|
{ id: '100', name: '100% Cocoa', price: 16.99 },
|
||||||
|
],
|
||||||
|
images: [
|
||||||
|
'/placeholder.svg?height=400&width=400',
|
||||||
|
'/placeholder.svg?height=400&width=400',
|
||||||
|
'/placeholder.svg?height=400&width=400',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
|
const [selectedVariant, setSelectedVariant] = useState<Variant>(
|
||||||
|
product.variants[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
setCurrentImage((currentImage + 1) % product.images.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevImage = () => {
|
||||||
|
setCurrentImage(
|
||||||
|
(currentImage - 1 + product.images.length) % product.images.length,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container mx-auto px-4 py-8'>
|
||||||
|
<div className='grid md:grid-cols-2 gap-8'>
|
||||||
|
<div className='relative'>
|
||||||
|
<Image
|
||||||
|
src={product.images[currentImage]}
|
||||||
|
alt={`${product.name} image ${currentImage + 1}`}
|
||||||
|
className='w-full h-auto rounded-lg shadow-lg'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='absolute left-2 top-1/2 transform -translate-y-1/2'
|
||||||
|
onClick={prevImage}
|
||||||
|
>
|
||||||
|
<ChevronLeft className='h-4 w-4' />
|
||||||
|
<span className='sr-only'>Previous image</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='absolute right-2 top-1/2 transform -translate-y-1/2'
|
||||||
|
onClick={nextImage}
|
||||||
|
>
|
||||||
|
<ChevronRight className='h-4 w-4' />
|
||||||
|
<span className='sr-only'>Next image</span>
|
||||||
|
</Button>
|
||||||
|
<div className='flex justify-center mt-4 space-x-2'>
|
||||||
|
{product.images.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-3 h-3 rounded-full ${
|
||||||
|
index === currentImage ? 'bg-primary' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className='text-3xl font-bold mb-4'>{product.name}</h1>
|
||||||
|
<div className='flex items-center mb-4'>
|
||||||
|
<div className='flex text-yellow-400'>
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star key={i} className='w-5 h-5 fill-current' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 text-sm text-gray-600'>(128 reviews)</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-2xl font-bold mb-4'>
|
||||||
|
${selectedVariant.price.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
<p className='text-gray-600 mb-6'>{product.description}</p>
|
||||||
|
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h3 className='text-lg font-semibold mb-2'>Choose Variant:</h3>
|
||||||
|
<RadioGroup
|
||||||
|
defaultValue={selectedVariant.id}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSelectedVariant(
|
||||||
|
product.variants.find((v) => v.id === value) ||
|
||||||
|
product.variants[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{product.variants.map((variant) => (
|
||||||
|
<div key={variant.id} className='flex items-center space-x-2'>
|
||||||
|
<RadioGroupItem value={variant.id} id={variant.id} />
|
||||||
|
<Label htmlFor={variant.id}>
|
||||||
|
{variant.name} - ${variant.price.toFixed(2)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center mb-6'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</Button>
|
||||||
|
<span className='mx-4'>{quantity}</span>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button className='w-full md:w-auto'>
|
||||||
|
<ShoppingCart className='mr-2 h-4 w-4' /> Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Product Details</h2>
|
||||||
|
<ul className='list-disc pl-5 space-y-2 text-gray-600'>
|
||||||
|
<li>Single-origin cocoa beans from Ecuador</li>
|
||||||
|
<li>Fair trade certified</li>
|
||||||
|
<li>No artificial flavors or preservatives</li>
|
||||||
|
<li>100g (3.5oz) bar</li>
|
||||||
|
<li>Available in different cocoa percentages</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Customer Reviews</h2>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className='p-4'>
|
||||||
|
<div className='flex items-center mb-2'>
|
||||||
|
<div className='flex text-yellow-400'>
|
||||||
|
{[...Array(5)].map((_, j) => (
|
||||||
|
<Star key={j} className='w-4 h-4 fill-current' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 text-sm font-medium'>John Doe</span>
|
||||||
|
</div>
|
||||||
|
<p className='text-gray-600'>
|
||||||
|
This chocolate is absolutely divine! The rich flavor and
|
||||||
|
smooth texture make it a perfect treat for any chocolate
|
||||||
|
lover. I especially love the 85% cocoa variant for its intense
|
||||||
|
flavor.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12'>
|
||||||
|
<h2 className='text-2xl font-bold mb-4'>Related Products</h2>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-6'>
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Card key={i}>
|
||||||
|
<CardContent className='p-4'>
|
||||||
|
<Image
|
||||||
|
src='/placeholder.svg?height=200&width=200'
|
||||||
|
alt={`Related product ${i + 1}`}
|
||||||
|
className='w-full h-48 object-cover mb-4 rounded'
|
||||||
|
/>
|
||||||
|
<h3 className='font-bold mb-2'>Milk Chocolate Truffles</h3>
|
||||||
|
<p className='text-gray-600 mb-2'>
|
||||||
|
Smooth, creamy milk chocolate truffles
|
||||||
|
</p>
|
||||||
|
<p className='font-bold'>$9.99</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,16 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
// import Swiper core and required modules
|
// import Swiper core and required modules
|
||||||
import { Autoplay, Pagination, EffectFade } from 'swiper/modules';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
||||||
|
|
||||||
// Import Swiper styles
|
// Import Swiper styles
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/effect-fade';
|
import 'swiper/css/effect-fade';
|
||||||
import 'swiper/css/navigation';
|
import 'swiper/css/navigation';
|
||||||
import 'swiper/css/pagination';
|
import 'swiper/css/pagination';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { Autoplay, EffectFade, Pagination } from 'swiper/modules';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
|
||||||
const slides = [
|
const slides = [
|
||||||
{ name: 'Bars', image: '/images/slider/slider-01.jpg' },
|
{ name: 'Bars', image: '/images/slider/slider-01.jpg' },
|
||||||
{ name: 'Barks', image: '/images/slider/slider-02.jpg' },
|
{ name: 'Barks', image: '/images/slider/slider-02.jpg' },
|
||||||
|
|||||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
|
destructive:
|
||||||
|
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
|
outline:
|
||||||
|
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
|
secondary:
|
||||||
|
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-9 px-4 py-2',
|
||||||
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
|
lg: 'h-10 rounded-md px-8',
|
||||||
|
icon: 'h-9 w-9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
83
src/components/ui/card.tsx
Normal file
83
src/components/ui/card.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'rounded-xl border bg-card text-card-foreground shadow',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn('font-semibold leading-none tracking-tight', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = 'CardTitle';
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = 'CardDescription';
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex items-center p-6 pt-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = 'CardFooter';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
};
|
||||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
|
import { CheckIcon } from '@radix-ui/react-icons';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn('flex items-center justify-center text-current')}
|
||||||
|
>
|
||||||
|
<CheckIcon className='h-4 w-4' />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
26
src/components/ui/input.tsx
Normal file
26
src/components/ui/input.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = 'Input';
|
||||||
|
|
||||||
|
export { Input };
|
||||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
|
);
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
||||||
44
src/components/ui/radio-group.tsx
Normal file
44
src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CheckIcon } from '@radix-ui/react-icons';
|
||||||
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn('grid gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className='flex items-center justify-center'>
|
||||||
|
<CheckIcon className='h-3.5 w-3.5 fill-primary' />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
164
src/components/ui/select.tsx
Normal file
164
src/components/ui/select.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CaretSortIcon,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<CaretSortIcon className='h-4 w-4 opacity-50' />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-default items-center justify-center py-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
));
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-default items-center justify-center py-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
));
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
'p-1',
|
||||||
|
position === 'popper' &&
|
||||||
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn('px-2 py-1.5 text-sm font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className='absolute right-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className='h-4 w-4' />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
));
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
28
src/components/ui/slider.tsx
Normal file
28
src/components/ui/slider.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex w-full touch-none select-none items-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className='relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20'>
|
||||||
|
<SliderPrimitive.Range className='absolute h-full bg-primary' />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className='block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
));
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Slider };
|
||||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||||
@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useRef, RefObject } from 'react';
|
import { RefObject, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useSharedIntersectionObserver } from './shared-intersection-observer';
|
import { useSharedIntersectionObserver } from './shared-intersection-observer';
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@ -42,7 +43,7 @@ const useInView = (
|
|||||||
return () => {
|
return () => {
|
||||||
observerManager.unobserve(element);
|
observerManager.unobserve(element);
|
||||||
};
|
};
|
||||||
}, [observerManager]);
|
}, [observerManager, options]);
|
||||||
|
|
||||||
return [elementRef, isInView];
|
return [elementRef, isInView];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, RefObject } from 'react';
|
import { RefObject, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface UseInViewOptions {
|
interface UseInViewOptions {
|
||||||
threshold?: number | number[];
|
threshold?: number | number[];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Medusa from '@medusajs/medusa-js';
|
import Medusa from '@medusajs/js-sdk';
|
||||||
|
|
||||||
// Defaults to standard port for Medusa server
|
// Defaults to standard port for Medusa server
|
||||||
let MEDUSA_BACKEND_URL = 'http://localhost:9000';
|
let MEDUSA_BACKEND_URL = 'http://localhost:9000';
|
||||||
@ -7,7 +7,8 @@ if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
|
|||||||
MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
|
MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const medusaClient = new Medusa({
|
export const sdk = new Medusa({
|
||||||
baseUrl: MEDUSA_BACKEND_URL,
|
baseUrl: MEDUSA_BACKEND_URL,
|
||||||
maxRetries: 3,
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import { cache } from 'react';
|
|
||||||
import { medusaClient } from '@/lib/config';
|
|
||||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
|
||||||
|
|
||||||
export const getProductByHandle = cache(async function (
|
|
||||||
handle: string,
|
|
||||||
): Promise<{ product: PricedProduct }> {
|
|
||||||
const product = await medusaClient.products
|
|
||||||
.list({ handle })
|
|
||||||
.then(({ products }) => products[0])
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { product };
|
|
||||||
});
|
|
||||||
82
src/lib/product-interface.tsx
Normal file
82
src/lib/product-interface.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string | null;
|
||||||
|
description: string | null;
|
||||||
|
handle: string;
|
||||||
|
is_giftcard: boolean;
|
||||||
|
discountable: boolean;
|
||||||
|
thumbnail: string;
|
||||||
|
collection_id: string | null;
|
||||||
|
type_id: string;
|
||||||
|
weight: number | null;
|
||||||
|
length: number | null;
|
||||||
|
height: number | null;
|
||||||
|
width: number | null;
|
||||||
|
hs_code: string | null;
|
||||||
|
origin_country: string | null;
|
||||||
|
mid_code: string | null;
|
||||||
|
material: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
type: ProductType;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
collection: any | null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
options: any[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
tags: any[];
|
||||||
|
images: ProductImage[];
|
||||||
|
variants: ProductVariant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductType {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
metadata: {
|
||||||
|
tax_id: string;
|
||||||
|
discount_limit: number;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductImage {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metadata: any | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductVariant {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
sku: string | null;
|
||||||
|
barcode: string | null;
|
||||||
|
ean: string | null;
|
||||||
|
upc: string | null;
|
||||||
|
allow_backorder: boolean;
|
||||||
|
manage_inventory: boolean;
|
||||||
|
hs_code: string | null;
|
||||||
|
origin_country: string | null;
|
||||||
|
mid_code: string | null;
|
||||||
|
material: string | null;
|
||||||
|
weight: number | null;
|
||||||
|
length: number | null;
|
||||||
|
height: number | null;
|
||||||
|
width: number | null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metadata: any | null;
|
||||||
|
variant_rank: number;
|
||||||
|
product_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
options: any[];
|
||||||
|
calculated_price: number | null;
|
||||||
|
}
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { cache } from 'react';
|
|
||||||
import { medusaClient } from '@/lib/config';
|
|
||||||
import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
|
|
||||||
|
|
||||||
import { StoreGetProductsParams } from '@medusajs/medusa';
|
|
||||||
|
|
||||||
export const getProductsList = cache(async function ({
|
|
||||||
pageParam = 0,
|
|
||||||
queryParams,
|
|
||||||
}: {
|
|
||||||
pageParam?: number;
|
|
||||||
queryParams?: StoreGetProductsParams;
|
|
||||||
}): Promise<{
|
|
||||||
response: { products: PricedProduct[]; count: number };
|
|
||||||
nextPage: number | null;
|
|
||||||
queryParams?: StoreGetProductsParams;
|
|
||||||
}> {
|
|
||||||
const limit = queryParams?.limit || 12;
|
|
||||||
const { products, count } = await medusaClient.products
|
|
||||||
.list(
|
|
||||||
{
|
|
||||||
limit,
|
|
||||||
offset: pageParam,
|
|
||||||
...queryParams,
|
|
||||||
},
|
|
||||||
{ next: { tags: ['products'] } },
|
|
||||||
)
|
|
||||||
.then((res) => res)
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextPage = count > pageParam + 1 ? pageParam + 1 : null;
|
|
||||||
return {
|
|
||||||
response: { products: products, count },
|
|
||||||
nextPage,
|
|
||||||
queryParams,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
import plugin from 'tailwindcss-animate';
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
|
darkMode: ['class'],
|
||||||
content: [
|
content: [
|
||||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
@ -9,16 +10,59 @@ const config: Config = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: 'var(--background)',
|
background: 'hsl(var(--background))',
|
||||||
foreground: 'var(--foreground)',
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
card: {
|
||||||
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
montera: ['Montera', 'sans-serif'],
|
montera: ['Montera', 'sans-serif'],
|
||||||
renner: ['Renner', 'sans-serif'],
|
renner: ['Renner', 'sans-serif'],
|
||||||
samantha: ['Samantha ', 'handwritten'],
|
samantha: ['Samantha ', 'handwritten'],
|
||||||
},
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
},
|
||||||
|
plugins: [plugin],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user