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 ( -
+
-
+
+ + Catalog + +

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 + + +
+
+ + setEmail(e.target.value)} + name='email' + required + /> +
+
+ + setPassword(e.target.value)} + name='password' + required + /> +
+ +
+
+ +
+
+ + setFirstName(e.target.value)} + required + /> +
+
+ + setLastName(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+
+
+ +
+ + {activeTab === 'login' + ? "Don't have an account?" + : 'Already have an account?'} + + +
+ {activeTab === 'login' && ( + + )} +
+
+
+ ); +} 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 && ( - {product.title} - )} +
+
+
+ {`${product.title} + + +
+ {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}

- +

{product.description}

+ +
+

Choose Variant:

+ + setSelectedVariant( + product.variants.find((v) => v.id === value) || + product.variants[0], + ) + } + > + {product.variants.map((variant) => ( +
+ + +
+ ))} +
+
+ +
+ + {quantity} + +
+ +
+
+ +
+

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) => ( + + + {`Related +

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 ( - 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?'} + + +
+ {activeTab === 'login' && ( + + )} +
+
+
+ ); +} 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} > - {category.name}([]); @@ -19,7 +21,7 @@ export function InstagramFeed() { {posts.map((post) => (
- {post.caption} 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 */} + + + {/* Main content */} +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ + +
+
+ +
+ {filteredProducts.map((product) => ( + + + {product.name} + + +

Category: {product.category}

+

${product.price.toFixed(2)}

+
+ + + +
+ ))} +
+
+
+
+ ); +} 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 ( +
+
+
+ {`${product.name} + + +
+ {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) => ( +
+ + +
+ ))} +
+
+ +
+ + {quantity} + +
+ +
+
+ +
+

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) => ( + + + {`Related +

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;