diff --git a/.eslintrc.json b/.eslintrc.json
index 933f123..74b2c9c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -4,13 +4,34 @@
"next/typescript",
"eslint:recommended",
"plugin:react/recommended",
- "prettier"
+ "prettier",
+ "plugin:@next/next/recommended"
],
- "plugins": ["prettier"],
+ "plugins": ["prettier", "eslint-plugin-import"],
"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",
"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": {
"browser": true,
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..7a63543
--- /dev/null
+++ b/components.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index c87d15e..0f849e8 100644
--- a/package.json
+++ b/package.json
@@ -9,15 +9,35 @@
"lint": "next lint"
},
"dependencies": {
+ "@medusajs/js-sdk": "^0.0.2-preview-20241001060739",
"@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",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.447.0",
"medusa-react": "^9.0.18",
"next": "14.2.13",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0",
"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": {
"@types/node": "^20",
@@ -26,6 +46,7 @@
"eslint": "^8",
"eslint-config-next": "14.2.13",
"eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"postcss": "^8",
"prettier": "^3.3.3",
diff --git a/src/app/(static)/about-us/page.tsx b/src/app/(static)/about-us/page.tsx
index 02c98a0..70c5def 100644
--- a/src/app/(static)/about-us/page.tsx
+++ b/src/app/(static)/about-us/page.tsx
@@ -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 barkPic from '/public/images/about-us/05-bark.jpg';
import italyPic from '/public/images/about-us/06-italy.jpg';
+
import { AnimatedImage } from '@/components/animated-image';
export default function AboutUsPage() {
diff --git a/src/app/(static)/festival/page.tsx b/src/app/(static)/festival/page.tsx
index 15b88e3..bb05501 100644
--- a/src/app/(static)/festival/page.tsx
+++ b/src/app/(static)/festival/page.tsx
@@ -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 goldPralineBoxPic from '/public/images/diwali/05-gold-praline-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 { AnimatedText } from '@/components/animated-text';
// import blueSpreadPic from '/public/images/diwali/07-blue-spread.jpg';
-export default function AboutUsPage() {
+export default function FestivalPage() {
return (
-
+
-
+
diff --git a/src/app/(static)/layout.tsx b/src/app/(static)/layout.tsx
index ccffd14..9ba0b22 100644
--- a/src/app/(static)/layout.tsx
+++ b/src/app/(static)/layout.tsx
@@ -1,7 +1,7 @@
import '../globals.css';
-// import './globalicons.css';
import { ReactNode } from 'react';
+
import { Footer } from '@/components/footer';
import { Navbar } from '@/components/navbar';
diff --git a/src/app/(static)/page.tsx b/src/app/(static)/page.tsx
index 9e512df..bad1bab 100644
--- a/src/app/(static)/page.tsx
+++ b/src/app/(static)/page.tsx
@@ -1,21 +1,22 @@
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 { 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() {
return (
{/* First Two-Column Layout */}
-
+
{/* Left Column - Text with Header */}
-
+
@@ -43,7 +44,7 @@ export default function HomePage() {
{/* Right Column - Image */}
-
+
) => {
+ 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,
+ ) => {
+ 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 Loading...
;
+ }
+
+ return (
+
+
+
+
+ Welcome to Our E-commerce Store
+
+
+ Login or create an account to start shopping
+
+
+
+
+
+ Login
+ Register
+
+
+
+
+
+
+
+
+
+
+
+
+ {activeTab === 'login'
+ ? "Don't have an account?"
+ : 'Already have an account?'}
+
+
+ setActiveTab(activeTab === 'login' ? 'register' : 'login')
+ }
+ >
+ {activeTab === 'login' ? 'Sign up' : 'Log in'}
+
+
+ {activeTab === 'login' && (
+
+ Forgot password?
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(storefront)/products/[handle]/ProductDetails.tsx b/src/app/(storefront)/products/[handle]/ProductDetails.tsx
index 7569e92..d7d79f9 100644
--- a/src/app/(storefront)/products/[handle]/ProductDetails.tsx
+++ b/src/app/(storefront)/products/[handle]/ProductDetails.tsx
@@ -1,28 +1,75 @@
'use client';
-import { FC } from 'react';
-import { useEffect, useState } from 'react';
-import { getProductByHandle } from '@/lib/product-by-handle';
-import { PricedProduct } from '@medusajs/medusa/dist/types/pricing';
+import { ChevronLeft, ChevronRight, ShoppingCart, Star } from 'lucide-react';
+import Image from 'next/image';
+import { FC, useEffect, 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';
+import { Product, ProductVariant } from '@/lib/product-interface';
interface ProductDetailsProps {
handle: string;
}
const ProductDetails: FC = ({ handle }) => {
- const [product, setProduct] = useState(null);
+ const [quantity, setQuantity] = useState(1);
+ const [currentImage, setCurrentImage] = useState(0);
+ const [product, setProduct] = useState();
+ const [selectedVariant, setSelectedVariant] = useState(
+ null,
+ );
+
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(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(() => {
const fetchProduct = async () => {
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);
if (!product) {
setProduct(null);
} else {
setProduct(product);
+ setSelectedVariant(product.variants[0]);
}
} catch (err: unknown) {
setError(err);
@@ -48,29 +95,163 @@ const ProductDetails: FC = ({ handle }) => {
}
return (
-
-
{product.title}
-
- {/* Product Images */}
-
- {!!product.images?.length && (
-
- )}
+
+
+
+
+
+
+ Previous image
+
+
+
+ Next image
+
+
+ {product.images.map((_, index) => (
+
+ ))}
+
-
- {/* Product Info */}
-
-
- Price: ${product.variants[0].prices[0].amount / 100}
+
+
{product.title}
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
(128 reviews)
+
+
+ ${selectedVariant?.calculated_price?.toFixed(2)}
-
{product.description}
-
- Add to Cart
-
+
{product.description}
+
+
+
Choose Variant:
+
+ setSelectedVariant(
+ product.variants.find((v) => v.id === value) ||
+ product.variants[0],
+ )
+ }
+ >
+ {product.variants.map((variant) => (
+
+
+
+ {variant.title} - ${variant?.calculated_price?.toFixed(2)}
+
+
+ ))}
+
+
+
+
+ setQuantity(Math.max(1, quantity - 1))}
+ >
+ -
+
+ {quantity}
+ setQuantity(quantity + 1)}
+ >
+ +
+
+
+
+ Add to Cart
+
+
+
+
+
+
Product Details
+
+ Single-origin cocoa beans from Ecuador
+ Fair trade certified
+ No artificial flavors or preservatives
+ 100g (3.5oz) bar
+ Available in different cocoa percentages
+
+
+
+
+
Customer Reviews
+
+ {[...Array(3)].map((_, i) => (
+
+
+
+
+ {[...Array(5)].map((_, j) => (
+
+ ))}
+
+
John Doe
+
+
+ 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.
+
+
+
+ ))}
+
+
+
+
+
Related Products
+
+ {[...Array(3)].map((_, i) => (
+
+
+
+ Milk Chocolate Truffles
+
+ Smooth, creamy milk chocolate truffles
+
+ $9.99
+
+
+ ))}
diff --git a/src/app/(storefront)/products/[handle]/page.tsx b/src/app/(storefront)/products/[handle]/page.tsx
index a2f945c..1c42a9a 100644
--- a/src/app/(storefront)/products/[handle]/page.tsx
+++ b/src/app/(storefront)/products/[handle]/page.tsx
@@ -1,5 +1,5 @@
import { FC } from 'react';
-import { getProductsList } from '@/lib/product';
+
import ProductDetails from './ProductDetails';
interface ProductPageProps {
@@ -7,17 +7,30 @@ interface ProductPageProps {
handle: string;
};
}
+interface Product {
+ handle: string;
+}
export async function generateStaticParams() {
// Fetch the list of products
- const products = await getProductsList({}).then(
- ({ response }) => response.products,
- );
+ const products: Product[] = await fetch(
+ `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) {
return [];
}
-
// Generate static params based on product handles
const staticParams = products.map((product) => ({
handle: product.handle,
diff --git a/src/app/(storefront)/products/page.tsx b/src/app/(storefront)/products/page.tsx
index e9a17af..0f3fe34 100644
--- a/src/app/(storefront)/products/page.tsx
+++ b/src/app/(storefront)/products/page.tsx
@@ -1,16 +1,39 @@
'use client';
-import { useProducts } from 'medusa-react';
+import { HttpTypes } from '@medusajs/types';
import Image from 'next/image';
import Link from 'next/link';
+import { useEffect, useState } from 'react';
export default function ProductsPage() {
- const { products, isLoading, error } = useProducts();
+ const [loading, setLoading] = useState(true);
- if (isLoading) {
- return
Loading...
;
- }
- if (error) return
Error: {error.message}
;
+ const [products, setProducts] = useState
([]);
+
+ useEffect(() => {
+ if (!loading) {
+ return;
+ }
+ 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 (
@@ -29,7 +52,7 @@ export default function ProductsPage() {
{product.title}
{product.description}
- ${product.variants[0]?.prices[0].amount / 100}
+ {/* ${product.variants?[0]?.calculated_price / 100} */}
diff --git a/src/app/globals.css b/src/app/globals.css
index 36a7eb6..236d649 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -3,29 +3,15 @@
@tailwind utilities;
:root {
- --background: #f8f6f5;
- --foreground: #703133;
- --normal-font: 2rem;
- --title-font: 5rem;
- --headline-font: 8rem;
- --items-font: 10rem;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #f8f6f5;
- --foreground: #703133;
- }
-}
-
-html {
- font-size: 62.5%;
+ --normal-font: 1.25rem;
+ --title-font: 3rem;
+ --headline-font: 5rem;
+ --items-font: 6.25rem;
}
body {
margin: 0;
padding: 0;
- color: var(--foreground);
background-color: var(--background);
font-family: Renner, sans-serif;
font-weight: 300;
@@ -133,3 +119,88 @@ animation-range-end: contain; */
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;
+}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index ed075bc..2d2729c 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -1,9 +1,9 @@
// src/app/providers.tsx
'use client';
-import { ReactNode, useState } from 'react';
-import { MedusaProvider, CartProvider } from 'medusa-react';
import { QueryClient } from '@tanstack/react-query';
+import { CartProvider, MedusaProvider } from 'medusa-react';
+import { ReactNode, useState } from 'react';
interface ProvidersProps {
children: ReactNode;
@@ -16,6 +16,7 @@ export function Providers({ children }: ProvidersProps) {
{children}
diff --git a/src/components/PillButton.module.css b/src/components/PillButton.module.css
index 28ff9c0..fffdc24 100644
--- a/src/components/PillButton.module.css
+++ b/src/components/PillButton.module.css
@@ -15,7 +15,7 @@
}
.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;
}
diff --git a/src/components/PillButton.tsx b/src/components/PillButton.tsx
index cf32d0c..185ad33 100644
--- a/src/components/PillButton.tsx
+++ b/src/components/PillButton.tsx
@@ -1,14 +1,28 @@
import React from 'react';
+
import styles from './PillButton.module.css';
type PillButtonProps = {
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
};
-export function PillButton({ text, onClick }: PillButtonProps) {
+export function PillButton({
+ text,
+ className = '',
+ onClick,
+ ...props
+}: PillButtonProps) {
return (
-
+
{text}
→
diff --git a/src/components/animated-image.tsx b/src/components/animated-image.tsx
index 40c314e..9c5c9b6 100644
--- a/src/components/animated-image.tsx
+++ b/src/components/animated-image.tsx
@@ -1,8 +1,9 @@
// src/components/AnimatedImage.tsx
'use client';
-import React from 'react';
-import useInView from '@/hooks/useInView';
import Image, { StaticImageData } from 'next/image';
+import React from 'react';
+
+import useInView from '@/hooks/useInView';
interface AnimatedImageProps {
src: string | StaticImageData;
diff --git a/src/components/animated-link.tsx b/src/components/animated-link.tsx
index 73ea92b..bbb8bcd 100644
--- a/src/components/animated-link.tsx
+++ b/src/components/animated-link.tsx
@@ -1,6 +1,6 @@
'use client';
-import React from 'react';
-import { ReactNode } from 'react';
+import React, { ReactNode } from 'react';
+
import useInView from '@/hooks/useInView';
interface AnimatedLiProps {
diff --git a/src/components/animated-text.tsx b/src/components/animated-text.tsx
index 0896d94..87a87e6 100644
--- a/src/components/animated-text.tsx
+++ b/src/components/animated-text.tsx
@@ -1,5 +1,6 @@
'use client';
import { ReactNode } from 'react';
+
import useInView from '@/hooks/useInView';
interface AnimatedTextProps {
diff --git a/src/components/auth-page.tsx b/src/components/auth-page.tsx
new file mode 100644
index 0000000..be3a9e5
--- /dev/null
+++ b/src/components/auth-page.tsx
@@ -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) => {
+ event.preventDefault();
+ // Handle form submission here
+ console.log('Form submitted:', activeTab);
+ };
+
+ return (
+
+
+
+
+ Welcome to the Mozimo Store
+
+
+ Login or create an account to start shopping
+
+
+
+
+
+ Login
+ Register
+
+
+
+
+
+
+
+
+
+
+
+
+ {activeTab === 'login'
+ ? "Don't have an account?"
+ : 'Already have an account?'}
+
+
+ setActiveTab(activeTab === 'login' ? 'register' : 'login')
+ }
+ >
+ {activeTab === 'login' ? 'Sign up' : 'Log in'}
+
+
+ {activeTab === 'login' && (
+
+ Forgot password?
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/category-slider.tsx b/src/components/category-slider.tsx
index b9cb3c7..fdef6cb 100644
--- a/src/components/category-slider.tsx
+++ b/src/components/category-slider.tsx
@@ -1,8 +1,9 @@
'use client';
-import { useState } from 'react';
import Image from 'next/image';
-import styles from './category-slider.module.css';
+import { useState } from 'react';
+
import { AnimatedLi } from './animated-link';
+import styles from './category-slider.module.css';
const categories = [
{ name: 'Bars', image: '/images/categories/bars.jpg' },
@@ -29,7 +30,7 @@ export function ChocolateCategories() {
startClass={startClass}
finishClass={finishClass}
>
- ([]);
@@ -19,7 +21,7 @@ export function InstagramFeed() {
{posts.map((post) => (
- div {
display: flex;
align-items: center;
- gap: var(--gap, 3.2rem);
+ gap: var(--gap, 2rem);
}
/* .navBarInitial {
@@ -73,10 +73,6 @@
backdrop-filter: blur(10px); /* Adds a blur effect */
}
- .cls-1, .cls-2 {
- fill: #ffffff;
- }
-
/* Transparent / almost transparent to white
.navBarInitial {
@apply bg-gradient-to-b from-black/50 to-black/30 text-white absolute rounded-lg;
diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx
index 4b40a07..0aef59b 100644
--- a/src/components/navbar.tsx
+++ b/src/components/navbar.tsx
@@ -1,13 +1,15 @@
'use client';
-import React, { useState, useEffect, useRef } from 'react';
+import Image from 'next/image';
import Link from 'next/link';
+import React, { useEffect, useRef, useState } from 'react';
import { FiSearch, FiUser } from 'react-icons/fi';
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 logoBlack from '/public/images/logo-black.svg';
+import styles from './navbar.module.css';
+
// components/Navbar.tsx
export function Navbar() {
const prevScrollY = useRef(0);
@@ -94,9 +96,9 @@ export function Navbar() {
-
+
-
+
diff --git a/src/components/our-collection.module.css b/src/components/our-collection.module.css
new file mode 100644
index 0000000..d8a6f09
--- /dev/null
+++ b/src/components/our-collection.module.css
@@ -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;
+ }
+
\ No newline at end of file
diff --git a/src/components/our-collection.tsx b/src/components/our-collection.tsx
index 33ba7cf..4136f0f 100644
--- a/src/components/our-collection.tsx
+++ b/src/components/our-collection.tsx
@@ -1,5 +1,7 @@
import Image from 'next/image';
+import styles from './our-collection.module.css';
+
const collections = [
{
title: 'Bestsellers',
@@ -26,7 +28,7 @@ export function Collections() {
alt={collection.title}
width={500}
height={300}
- className='w-full h-auto object-cover transition transform hover:scale-110 duration-[1400ms] linear'
+ className={styles.ourCollection}
/>
diff --git a/src/components/product-list.tsx b/src/components/product-list.tsx
new file mode 100644
index 0000000..b2da88f
--- /dev/null
+++ b/src/components/product-list.tsx
@@ -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
([]);
+ 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 (
+
+
+ {/* Sidebar with filters */}
+
+
+
+ Filters
+
+
+
+
Categories
+ {categories.map((category) => (
+
+ handleCategoryChange(category)}
+ />
+
+ {category}
+
+
+ ))}
+
+
+
Price Range
+
+
+ ${priceRange[0]}
+ ${priceRange[1]}
+
+
+
+
+
+
+ {/* Main content */}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+
+ Name
+ Price
+
+
+
+
+
+
+
+
+
+ {filteredProducts.map((product) => (
+
+
+ {product.name}
+
+
+ Category: {product.category}
+ ${product.price.toFixed(2)}
+
+
+ Add to Cart
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/components/product-page.tsx b/src/components/product-page.tsx
new file mode 100644
index 0000000..36016fa
--- /dev/null
+++ b/src/components/product-page.tsx
@@ -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(
+ product.variants[0],
+ );
+
+ const nextImage = () => {
+ setCurrentImage((currentImage + 1) % product.images.length);
+ };
+
+ const prevImage = () => {
+ setCurrentImage(
+ (currentImage - 1 + product.images.length) % product.images.length,
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ Previous image
+
+
+
+ Next image
+
+
+ {product.images.map((_, index) => (
+
+ ))}
+
+
+
+
{product.name}
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
(128 reviews)
+
+
+ ${selectedVariant.price.toFixed(2)}
+
+
{product.description}
+
+
+
Choose Variant:
+
+ setSelectedVariant(
+ product.variants.find((v) => v.id === value) ||
+ product.variants[0],
+ )
+ }
+ >
+ {product.variants.map((variant) => (
+
+
+
+ {variant.name} - ${variant.price.toFixed(2)}
+
+
+ ))}
+
+
+
+
+ setQuantity(Math.max(1, quantity - 1))}
+ >
+ -
+
+ {quantity}
+ setQuantity(quantity + 1)}
+ >
+ +
+
+
+
+ Add to Cart
+
+
+
+
+
+
Product Details
+
+ Single-origin cocoa beans from Ecuador
+ Fair trade certified
+ No artificial flavors or preservatives
+ 100g (3.5oz) bar
+ Available in different cocoa percentages
+
+
+
+
+
Customer Reviews
+
+ {[...Array(3)].map((_, i) => (
+
+
+
+
+ {[...Array(5)].map((_, j) => (
+
+ ))}
+
+
John Doe
+
+
+ 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.
+
+
+
+ ))}
+
+
+
+
+
Related Products
+
+ {[...Array(3)].map((_, i) => (
+
+
+
+ Milk Chocolate Truffles
+
+ Smooth, creamy milk chocolate truffles
+
+ $9.99
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/swiper.tsx b/src/components/swiper.tsx
index 27d4202..2354dad 100644
--- a/src/components/swiper.tsx
+++ b/src/components/swiper.tsx
@@ -1,16 +1,15 @@
'use client';
// 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/css';
import 'swiper/css/effect-fade';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
+import Image from 'next/image';
+import { Autoplay, EffectFade, Pagination } from 'swiper/modules';
+import { Swiper, SwiperSlide } from 'swiper/react';
+
const slides = [
{ name: 'Bars', image: '/images/slider/slider-01.jpg' },
{ name: 'Barks', image: '/images/slider/slider-02.jpg' },
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..35a75ae
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -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,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ },
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..d26087e
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..59137da
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..28a28a2
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -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 {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..9575458
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -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,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..0fdc799
--- /dev/null
+++ b/src/components/ui/radio-group.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
+
+const RadioGroupItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ return (
+
+
+
+
+
+ );
+});
+RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
+
+export { RadioGroup, RadioGroupItem };
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000..f95be74
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName;
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = 'popper', ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+};
diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx
new file mode 100644
index 0000000..aaa6a59
--- /dev/null
+++ b/src/components/ui/slider.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+));
+Slider.displayName = SliderPrimitive.Root.displayName;
+
+export { Slider };
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..86a170f
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsContent, TabsList, TabsTrigger };
diff --git a/src/hooks/useInView.tsx b/src/hooks/useInView.tsx
index 030d8a0..920dd52 100644
--- a/src/hooks/useInView.tsx
+++ b/src/hooks/useInView.tsx
@@ -1,5 +1,6 @@
'use client';
-import { useState, useEffect, useRef, RefObject } from 'react';
+import { RefObject, useEffect, useRef, useState } from 'react';
+
import { useSharedIntersectionObserver } from './shared-intersection-observer';
// eslint-disable-next-line no-undef
@@ -42,7 +43,7 @@ const useInView = (
return () => {
observerManager.unobserve(element);
};
- }, [observerManager]);
+ }, [observerManager, options]);
return [elementRef, isInView];
};
diff --git a/src/hooks/useInViewsingle.tsx b/src/hooks/useInViewsingle.tsx
index 88372c8..4464050 100644
--- a/src/hooks/useInViewsingle.tsx
+++ b/src/hooks/useInViewsingle.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, RefObject } from 'react';
+import { RefObject, useEffect, useState } from 'react';
interface UseInViewOptions {
threshold?: number | number[];
diff --git a/src/lib/config.ts b/src/lib/config.ts
index d629d32..b09b299 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -1,4 +1,4 @@
-import Medusa from '@medusajs/medusa-js';
+import Medusa from '@medusajs/js-sdk';
// Defaults to standard port for Medusa server
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;
}
-export const medusaClient = new Medusa({
+export const sdk = new Medusa({
baseUrl: MEDUSA_BACKEND_URL,
- maxRetries: 3,
+ debug: process.env.NODE_ENV === 'development',
+ publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
});
diff --git a/src/lib/product-by-handle.tsx b/src/lib/product-by-handle.tsx
deleted file mode 100644
index 396dfea..0000000
--- a/src/lib/product-by-handle.tsx
+++ /dev/null
@@ -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 };
-});
diff --git a/src/lib/product-interface.tsx b/src/lib/product-interface.tsx
new file mode 100644
index 0000000..18231ca
--- /dev/null
+++ b/src/lib/product-interface.tsx
@@ -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;
+}
diff --git a/src/lib/product.tsx b/src/lib/product.tsx
deleted file mode 100644
index 6e69bb0..0000000
--- a/src/lib/product.tsx
+++ /dev/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,
- };
-});
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..9ad0df4
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 4d7c70e..9755f3a 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,6 +1,7 @@
import type { Config } from 'tailwindcss';
-
+import plugin from 'tailwindcss-animate';
const config: Config = {
+ darkMode: ['class'],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
@@ -9,16 +10,59 @@ const config: Config = {
theme: {
extend: {
colors: {
- background: 'var(--background)',
- foreground: 'var(--foreground)',
+ background: 'hsl(var(--background))',
+ 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: {
montera: ['Montera', 'sans-serif'],
renner: ['Renner', 'sans-serif'],
samantha: ['Samantha ', 'handwritten'],
},
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
},
},
- plugins: [],
+ plugins: [plugin],
};
export default config;