mirror of
https://github.com/harivansh-afk/GymSupps.git
synced 2026-04-15 07:04:47 +00:00
initial commit
This commit is contained in:
commit
357979d0af
31 changed files with 6645 additions and 0 deletions
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"template": "bolt-vite-react-ts"
|
||||||
|
}
|
||||||
8
.bolt/prompt
Normal file
8
.bolt/prompt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
||||||
|
|
||||||
|
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
||||||
|
|
||||||
|
Use icons from lucide-react for logos.
|
||||||
|
|
||||||
|
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
|
||||||
|
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4847
package-lock.json
generated
Normal file
4847
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
package.json
Normal file
46
package.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "vite-react-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"lucide-react": "^0.344.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.22.3",
|
||||||
|
"tailwind-merge": "^2.2.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zustand": "^4.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@types/node": "^20.11.24",
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
79
src/App.tsx
Normal file
79
src/App.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Header } from './components/Header';
|
||||||
|
import { ProductGrid } from './components/ProductGrid';
|
||||||
|
import { ProductDetails } from './components/ProductDetails';
|
||||||
|
import { products } from './data/products';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<string | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const categories = Array.from(new Set(products.map((p) => p.category)));
|
||||||
|
const currentProduct = products.find((p) => p.id === selectedProduct);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<Header />
|
||||||
|
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
{!selectedProduct ? (
|
||||||
|
<>
|
||||||
|
<section className="text-center mb-12">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">
|
||||||
|
Premium Supplements for Your Fitness Journey
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Discover our range of high-quality supplements designed to help you achieve your fitness goals.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex flex-wrap gap-4 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCategory(null)}
|
||||||
|
className={`px-4 py-2 rounded-full ${
|
||||||
|
!selectedCategory
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-secondary text-secondary-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
All Products
|
||||||
|
</button>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
className={`px-4 py-2 rounded-full ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-secondary text-secondary-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProductGrid
|
||||||
|
products={products}
|
||||||
|
category={selectedCategory ?? undefined}
|
||||||
|
onProductSelect={setSelectedProduct}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedProduct(null)}
|
||||||
|
className="mb-8 text-primary hover:underline flex items-center gap-2"
|
||||||
|
>
|
||||||
|
← Back to Products
|
||||||
|
</button>
|
||||||
|
{currentProduct && <ProductDetails product={currentProduct} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
105
src/components/Cart.tsx
Normal file
105
src/components/Cart.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { X, Plus, Minus, ShoppingBag } from 'lucide-react';
|
||||||
|
import { useCartStore } from '../store/cartStore';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from './ui/sheet';
|
||||||
|
|
||||||
|
export const Cart: React.FC = () => {
|
||||||
|
const { items, removeItem, updateQuantity, total } = useCartStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="relative">
|
||||||
|
<ShoppingBag className="w-5 h-5" />
|
||||||
|
{items.length > 0 && (
|
||||||
|
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
||||||
|
{items.reduce((sum, item) => sum + item.quantity, 0)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="w-full sm:max-w-lg">
|
||||||
|
<SheetHeader>
|
||||||
|
<SheetTitle>Shopping Cart</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="mt-8">
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<ShoppingBag className="w-12 h-12 mx-auto text-muted-foreground" />
|
||||||
|
<p className="mt-4 text-lg font-medium">Your cart is empty</p>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Start adding some supplements to your cart
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.product.id} className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={item.product.image}
|
||||||
|
alt={item.product.name}
|
||||||
|
className="w-20 h-20 object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-medium">{item.product.name}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
${item.product.price.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() =>
|
||||||
|
updateQuantity(item.product.id, Math.max(0, item.quantity - 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<span className="w-8 text-center">{item.quantity}</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() =>
|
||||||
|
updateQuantity(item.product.id, item.quantity + 1)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeItem(item.product.id)}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Separator className="my-6" />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Total</span>
|
||||||
|
<span className="font-medium">${total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<Button className="w-full">Proceed to Checkout</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
src/components/FeaturedProducts.tsx
Normal file
16
src/components/FeaturedProducts.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ProductCard } from './ProductCard';
|
||||||
|
import { products } from '../data/products';
|
||||||
|
|
||||||
|
export const FeaturedProducts: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<section className="py-12 px-4 max-w-7xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-8">Featured Products</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{products.map((product) => (
|
||||||
|
<ProductCard key={product.id} product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
src/components/Header.tsx
Normal file
38
src/components/Header.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ShoppingCart, Dumbbell, Search, User } from 'lucide-react';
|
||||||
|
import { useCartStore } from '../store/cartStore';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Cart } from './Cart';
|
||||||
|
|
||||||
|
export const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<header className="bg-background border-b">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Dumbbell className="w-8 h-8 text-primary" />
|
||||||
|
<span className="text-2xl font-bold">GymSupps</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:flex flex-1 max-w-lg mx-8">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search products..."
|
||||||
|
className="w-full px-4 py-2 rounded-md border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<Search className="absolute right-3 top-2.5 w-5 h-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
<Cart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
62
src/components/ProductCard.tsx
Normal file
62
src/components/ProductCard.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Star, ShoppingCart } from 'lucide-react';
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
import { useCartStore } from '../store/cartStore';
|
||||||
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
|
interface ProductCardProps {
|
||||||
|
product: Product;
|
||||||
|
onSelect: (productId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductCard: React.FC<ProductCardProps> = ({ product, onSelect }) => {
|
||||||
|
const addItem = useCartStore((state) => state.addItem);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="cursor-pointer" onClick={() => onSelect(product.id)}>
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-48 object-cover rounded-t-xl"
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<CardTitle className="text-lg">{product.name}</CardTitle>
|
||||||
|
<span className="text-lg font-bold text-primary">
|
||||||
|
${product.price.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Star className="w-4 h-4 text-yellow-400 fill-current" />
|
||||||
|
<span className="ml-1 text-sm text-muted-foreground">
|
||||||
|
{product.rating} ({product.reviews} reviews)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded-full">
|
||||||
|
{product.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addItem(product);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
205
src/components/ProductDetails.tsx
Normal file
205
src/components/ProductDetails.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
import { Beaker, Clock, Award, BookOpen } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
|
||||||
|
interface ProductDetailsProps {
|
||||||
|
product: Product;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductDetails: React.FC<ProductDetailsProps> = ({ product }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-[400px] object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Scientific Research</h2>
|
||||||
|
{product.studies.map((study, index) => (
|
||||||
|
<Card key={index} className="mb-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">{study.title}</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{study.authors} ({study.year}) - {study.journal}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm">{study.summary}</p>
|
||||||
|
<a
|
||||||
|
href={`https://doi.org/${study.doi}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline text-sm mt-2 inline-block"
|
||||||
|
>
|
||||||
|
View Study
|
||||||
|
</a>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold mb-2">{product.name}</h1>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<span className="text-lg font-semibold text-primary">
|
||||||
|
${product.price.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{product.category} / {product.subCategory}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-lg mb-8">{product.description}</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Beaker className="w-5 h-5" />
|
||||||
|
Dosage
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm">{product.dosage}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Clock className="w-5 h-5" />
|
||||||
|
Timing
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm">{product.timing}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Key Benefits</h2>
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{product.benefits.map((benefit, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-2">
|
||||||
|
<Award className="w-5 h-5 text-primary mt-1" />
|
||||||
|
<span>{benefit}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Nutritional Information</h2>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Serving Size</span>
|
||||||
|
<span>{product.nutritionalFacts.servingSize}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Servings Per Container</span>
|
||||||
|
<span>{product.nutritionalFacts.servingsPerContainer}</span>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Calories</span>
|
||||||
|
<span>{product.nutritionalFacts.calories}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Protein</span>
|
||||||
|
<span>{product.nutritionalFacts.protein}g</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Carbohydrates</span>
|
||||||
|
<span>{product.nutritionalFacts.carbs}g</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Fat</span>
|
||||||
|
<span>{product.nutritionalFacts.fat}g</span>
|
||||||
|
</div>
|
||||||
|
{product.nutritionalFacts.additionalInfo && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
{Object.entries(product.nutritionalFacts.additionalInfo).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex justify-between">
|
||||||
|
<span>{key}</span>
|
||||||
|
<span>{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Available Options</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Sizes</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{product.sizes.map((size) => (
|
||||||
|
<span
|
||||||
|
key={size}
|
||||||
|
className="px-3 py-1 rounded-full bg-secondary text-secondary-foreground text-sm"
|
||||||
|
>
|
||||||
|
{size}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-2">Flavors</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{product.flavors.map((flavor) => (
|
||||||
|
<span
|
||||||
|
key={flavor}
|
||||||
|
className="px-3 py-1 rounded-full bg-secondary text-secondary-foreground text-sm"
|
||||||
|
>
|
||||||
|
{flavor}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Certifications</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{product.certifications.map((cert, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-5 h-5" />
|
||||||
|
{cert.name}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm mb-2">Issued by: {cert.issuer}</p>
|
||||||
|
<a
|
||||||
|
href={cert.verificationUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline text-sm"
|
||||||
|
>
|
||||||
|
Verify Certification
|
||||||
|
</a>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
27
src/components/ProductGrid.tsx
Normal file
27
src/components/ProductGrid.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
import { ProductCard } from './ProductCard';
|
||||||
|
|
||||||
|
interface ProductGridProps {
|
||||||
|
products: Product[];
|
||||||
|
category?: string;
|
||||||
|
onProductSelect: (productId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductGrid: React.FC<ProductGridProps> = ({ products, category, onProductSelect }) => {
|
||||||
|
const filteredProducts = category
|
||||||
|
? products.filter((product) => product.category === category)
|
||||||
|
: products;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{filteredProducts.map((product) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
onSelect={onProductSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
76
src/components/ui/card.tsx
Normal file
76
src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border bg-card text-card-foreground shadow transition-all hover:shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
29
src/components/ui/separator.tsx
Normal file
29
src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
138
src/components/ui/sheet.tsx
Normal file
138
src/components/ui/sheet.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root
|
||||||
|
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger
|
||||||
|
|
||||||
|
const SheetClose = SheetPrimitive.Close
|
||||||
|
|
||||||
|
const SheetPortal = SheetPrimitive.Portal
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const sheetVariants = cva(
|
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
|
bottom:
|
||||||
|
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||||
|
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||||
|
right:
|
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SheetContentProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
|
VariantProps<typeof sheetVariants> {}
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(sheetVariants({ side }), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
))
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SheetHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
SheetHeader.displayName = "SheetHeader"
|
||||||
|
|
||||||
|
const SheetFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
SheetFooter.displayName = "SheetFooter"
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetPortal,
|
||||||
|
SheetOverlay,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
521
src/data/products.ts
Normal file
521
src/data/products.ts
Normal file
|
|
@ -0,0 +1,521 @@
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
|
||||||
|
export const products: Product[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Elite Whey Protein Isolate',
|
||||||
|
description: 'Premium micro-filtered whey protein isolate with 27g protein per serving and minimal fat content. Enhanced with digestive enzymes for optimal absorption.',
|
||||||
|
price: 59.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1593095948071-474c5cc2989d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Protein',
|
||||||
|
subCategory: 'Whey Isolate',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 245,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '30g',
|
||||||
|
servingsPerContainer: 30,
|
||||||
|
calories: 120,
|
||||||
|
protein: 27,
|
||||||
|
carbs: 1,
|
||||||
|
fat: 0.5,
|
||||||
|
additionalInfo: {
|
||||||
|
'Leucine': '2.7g',
|
||||||
|
'BCAA': '5.5g',
|
||||||
|
'Glutamine': '4.5g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '1-2 scoops post-workout or between meals',
|
||||||
|
timing: 'Immediately post-workout or anytime to meet daily protein requirements',
|
||||||
|
benefits: [
|
||||||
|
'Rapid protein synthesis activation',
|
||||||
|
'Enhanced muscle recovery',
|
||||||
|
'Improved strength gains',
|
||||||
|
'Supports lean muscle growth'
|
||||||
|
],
|
||||||
|
sizes: ['2lbs', '5lbs', '10lbs'],
|
||||||
|
flavors: ['Chocolate', 'Vanilla', 'Strawberry', 'Cookie & Cream'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Informed Choice Certified',
|
||||||
|
issuer: 'LGC Group',
|
||||||
|
verificationUrl: 'https://informed-choice.org'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Whey protein supplementation and muscle mass gains',
|
||||||
|
authors: 'Smith et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Journal of Sports Nutrition',
|
||||||
|
doi: '10.1000/jsn.2023.001',
|
||||||
|
summary: 'Study demonstrated 40% greater muscle protein synthesis with whey isolate vs concentrate'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Advanced Creatine Monohydrate',
|
||||||
|
description: 'Pharmaceutical grade creatine monohydrate for maximum strength and power output. Micronized for superior mixability and absorption.',
|
||||||
|
price: 29.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Essential Supplements',
|
||||||
|
subCategory: 'Creatine',
|
||||||
|
rating: 4.9,
|
||||||
|
reviews: 789,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '5g',
|
||||||
|
servingsPerContainer: 60,
|
||||||
|
calories: 0,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'Creatine Monohydrate': '5g',
|
||||||
|
'Purity': '99.9%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '5g daily',
|
||||||
|
timing: 'Any time of day, consistent daily intake',
|
||||||
|
benefits: [
|
||||||
|
'Increased strength and power output',
|
||||||
|
'Enhanced muscle volumization',
|
||||||
|
'Improved high-intensity performance',
|
||||||
|
'Accelerated recovery between sets'
|
||||||
|
],
|
||||||
|
sizes: ['300g', '600g', '1kg'],
|
||||||
|
flavors: ['Unflavored'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Creapure® Certified',
|
||||||
|
issuer: 'AlzChem Trostberg GmbH',
|
||||||
|
verificationUrl: 'https://creapure.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Effects of creatine supplementation on exercise performance',
|
||||||
|
authors: 'Johnson et al.',
|
||||||
|
year: 2022,
|
||||||
|
journal: 'International Journal of Sport Nutrition',
|
||||||
|
doi: '10.1000/ijsn.2022.005',
|
||||||
|
summary: 'Meta-analysis showing 8-14% increase in strength and power output with creatine supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Pre-Workout Maximum Performance',
|
||||||
|
description: 'Advanced pre-workout formula with clinically dosed ingredients for explosive energy, focus, and muscle pumps.',
|
||||||
|
price: 45.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1579722820308-d74e571900a9?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Performance Enhancers',
|
||||||
|
subCategory: 'Pre-Workout',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 456,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '16g',
|
||||||
|
servingsPerContainer: 30,
|
||||||
|
calories: 5,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 1,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'Citrulline Malate': '8g',
|
||||||
|
'Beta-Alanine': '3.2g',
|
||||||
|
'Caffeine': '300mg',
|
||||||
|
'L-Theanine': '200mg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '1 scoop 20-30 minutes before workout',
|
||||||
|
timing: 'Pre-workout only, avoid late evening use',
|
||||||
|
benefits: [
|
||||||
|
'Explosive energy and focus',
|
||||||
|
'Enhanced muscle pumps',
|
||||||
|
'Increased strength and endurance',
|
||||||
|
'Improved mental alertness'
|
||||||
|
],
|
||||||
|
sizes: ['30 servings', '60 servings'],
|
||||||
|
flavors: ['Fruit Punch', 'Blue Raspberry', 'Watermelon'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Informed Sport Certified',
|
||||||
|
issuer: 'LGC Group',
|
||||||
|
verificationUrl: 'https://informed-sport.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Citrulline malate supplementation and exercise performance',
|
||||||
|
authors: 'Williams et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Journal of Exercise Science',
|
||||||
|
doi: '10.1000/jes.2023.002',
|
||||||
|
summary: 'Study showed 15% increase in training volume with citrulline malate supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Omega-3 Fish Oil Ultra',
|
||||||
|
description: 'High-potency fish oil supplement with concentrated EPA and DHA for optimal muscle recovery and joint health.',
|
||||||
|
price: 34.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1584017911766-d451b3d0e843?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Essential Supplements',
|
||||||
|
subCategory: 'Fish Oil',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 342,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '2 softgels',
|
||||||
|
servingsPerContainer: 60,
|
||||||
|
calories: 20,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 2,
|
||||||
|
additionalInfo: {
|
||||||
|
'EPA': '1200mg',
|
||||||
|
'DHA': '900mg',
|
||||||
|
'Total Omega-3s': '2200mg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '2 softgels daily with meals',
|
||||||
|
timing: 'With meals for optimal absorption',
|
||||||
|
benefits: [
|
||||||
|
'Reduced inflammation',
|
||||||
|
'Enhanced joint mobility',
|
||||||
|
'Improved muscle recovery',
|
||||||
|
'Supports cardiovascular health'
|
||||||
|
],
|
||||||
|
sizes: ['60 softgels', '120 softgels', '180 softgels'],
|
||||||
|
flavors: ['Lemon'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'IFOS 5-Star Certified',
|
||||||
|
issuer: 'Nutrasource',
|
||||||
|
verificationUrl: 'https://certifications.nutrasource.ca/ifos'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Omega-3 supplementation and muscle protein synthesis',
|
||||||
|
authors: 'Anderson et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Sports Medicine Research',
|
||||||
|
doi: '10.1000/smr.2023.003',
|
||||||
|
summary: 'Research showed 20% improvement in muscle recovery markers with high-dose omega-3 supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Mass Gainer Supreme',
|
||||||
|
description: 'High-calorie mass gainer with premium proteins, complex carbohydrates, and essential nutrients for maximum muscle growth.',
|
||||||
|
price: 64.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1612532774276-cfa70ca7ed2b?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Protein',
|
||||||
|
subCategory: 'Mass Gainer',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 289,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '165g',
|
||||||
|
servingsPerContainer: 20,
|
||||||
|
calories: 650,
|
||||||
|
protein: 50,
|
||||||
|
carbs: 85,
|
||||||
|
fat: 8,
|
||||||
|
additionalInfo: {
|
||||||
|
'BCAAs': '11g',
|
||||||
|
'Creatine': '5g',
|
||||||
|
'Glutamine': '5g',
|
||||||
|
'MCTs': '3g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '1-2 servings daily',
|
||||||
|
timing: 'Post-workout or between meals',
|
||||||
|
benefits: [
|
||||||
|
'Rapid weight gain',
|
||||||
|
'Enhanced muscle growth',
|
||||||
|
'Improved recovery',
|
||||||
|
'Complete nutrient profile'
|
||||||
|
],
|
||||||
|
sizes: ['6lbs', '12lbs'],
|
||||||
|
flavors: ['Chocolate Blast', 'Vanilla Ice Cream', 'Strawberry Milkshake'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Informed Choice Certified',
|
||||||
|
issuer: 'LGC Group',
|
||||||
|
verificationUrl: 'https://informed-choice.org'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Effects of high-calorie supplementation on muscle mass',
|
||||||
|
authors: 'Thompson et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Journal of Sports Nutrition',
|
||||||
|
doi: '10.1000/jsn.2023.004',
|
||||||
|
summary: 'Study demonstrated significant muscle mass gains with structured mass gainer supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
name: 'BCAA Recovery Complex',
|
||||||
|
description: 'Premium 2:1:1 ratio BCAA formula enhanced with electrolytes and B-vitamins for optimal muscle recovery and hydration.',
|
||||||
|
price: 39.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Essential Supplements',
|
||||||
|
subCategory: 'BCAAs',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 567,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '14g',
|
||||||
|
servingsPerContainer: 30,
|
||||||
|
calories: 5,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 1,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'L-Leucine': '5g',
|
||||||
|
'L-Isoleucine': '2.5g',
|
||||||
|
'L-Valine': '2.5g',
|
||||||
|
'Electrolyte Blend': '1g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '1 scoop during training',
|
||||||
|
timing: 'During workout or throughout the day',
|
||||||
|
benefits: [
|
||||||
|
'Reduced muscle breakdown',
|
||||||
|
'Enhanced recovery',
|
||||||
|
'Improved hydration',
|
||||||
|
'Decreased soreness'
|
||||||
|
],
|
||||||
|
sizes: ['30 servings', '60 servings'],
|
||||||
|
flavors: ['Tropical Punch', 'Green Apple', 'Grape'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Informed Sport Certified',
|
||||||
|
issuer: 'LGC Group',
|
||||||
|
verificationUrl: 'https://informed-sport.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'BCAA supplementation and muscle protein breakdown',
|
||||||
|
authors: 'Martinez et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Amino Acids Research',
|
||||||
|
doi: '10.1000/aar.2023.005',
|
||||||
|
summary: 'Research showed 30% reduction in muscle protein breakdown during training with BCAA supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
name: 'Beta-Alanine Performance',
|
||||||
|
description: 'Pure beta-alanine supplement for enhanced muscular endurance and reduced fatigue during high-intensity training.',
|
||||||
|
price: 29.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Performance Enhancers',
|
||||||
|
subCategory: 'Beta-Alanine',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 234,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '3.2g',
|
||||||
|
servingsPerContainer: 60,
|
||||||
|
calories: 0,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'Beta-Alanine': '3.2g',
|
||||||
|
'CarnoSyn®': '3.2g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '3.2g daily',
|
||||||
|
timing: 'Any time of day, consistent daily intake',
|
||||||
|
benefits: [
|
||||||
|
'Increased muscular endurance',
|
||||||
|
'Delayed muscle fatigue',
|
||||||
|
'Enhanced workout capacity',
|
||||||
|
'Improved performance in high-rep sets'
|
||||||
|
],
|
||||||
|
sizes: ['200g', '400g'],
|
||||||
|
flavors: ['Unflavored'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'CarnoSyn® Certified',
|
||||||
|
issuer: 'Natural Alternatives International',
|
||||||
|
verificationUrl: 'https://carnosyn.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'Beta-alanine supplementation and exercise performance',
|
||||||
|
authors: 'Rodriguez et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Exercise Science Quarterly',
|
||||||
|
doi: '10.1000/esq.2023.006',
|
||||||
|
summary: 'Meta-analysis showed significant improvements in high-intensity exercise performance with beta-alanine supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
name: 'ZMA Night Recovery',
|
||||||
|
description: 'Synergistic blend of Zinc, Magnesium, and Vitamin B6 for enhanced recovery and testosterone support during sleep.',
|
||||||
|
price: 24.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Essential Supplements',
|
||||||
|
subCategory: 'Recovery',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 456,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '3 capsules',
|
||||||
|
servingsPerContainer: 30,
|
||||||
|
calories: 0,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'Zinc': '30mg',
|
||||||
|
'Magnesium': '450mg',
|
||||||
|
'Vitamin B6': '10.5mg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '3 capsules before bed',
|
||||||
|
timing: '30-60 minutes before sleep on an empty stomach',
|
||||||
|
benefits: [
|
||||||
|
'Enhanced sleep quality',
|
||||||
|
'Improved recovery',
|
||||||
|
'Supports testosterone levels',
|
||||||
|
'Optimized mineral status'
|
||||||
|
],
|
||||||
|
sizes: ['90 capsules', '180 capsules'],
|
||||||
|
flavors: ['Unflavored'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'NSF Certified for Sport',
|
||||||
|
issuer: 'NSF International',
|
||||||
|
verificationUrl: 'https://www.nsfsport.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'ZMA supplementation and recovery in athletes',
|
||||||
|
authors: 'Wilson et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Sleep and Recovery Research',
|
||||||
|
doi: '10.1000/srr.2023.007',
|
||||||
|
summary: 'Study showed improved sleep quality and recovery markers in trained athletes using ZMA supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9',
|
||||||
|
name: 'L-Carnitine Pure',
|
||||||
|
description: 'High-purity L-Carnitine supplement for enhanced fat metabolism and improved exercise performance.',
|
||||||
|
price: 34.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Performance Enhancers',
|
||||||
|
subCategory: 'Fat Metabolism',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 312,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '2g',
|
||||||
|
servingsPerContainer: 50,
|
||||||
|
calories: 0,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'L-Carnitine L-Tartrate': '2g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '2g daily',
|
||||||
|
timing: 'Pre-workout or with meals',
|
||||||
|
benefits: [
|
||||||
|
'Enhanced fat utilization',
|
||||||
|
'Improved exercise performance',
|
||||||
|
'Reduced muscle fatigue',
|
||||||
|
'Better recovery'
|
||||||
|
],
|
||||||
|
sizes: ['100g', '200g'],
|
||||||
|
flavors: ['Unflavored'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'Carnipure® Certified',
|
||||||
|
issuer: 'Lonza',
|
||||||
|
verificationUrl: 'https://www.carnipure.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'L-Carnitine and exercise metabolism',
|
||||||
|
authors: 'Garcia et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Metabolism Research',
|
||||||
|
doi: '10.1000/mr.2023.008',
|
||||||
|
summary: 'Research demonstrated improved fat oxidation and exercise performance with L-Carnitine supplementation'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '10',
|
||||||
|
name: 'HMB Muscle Defender',
|
||||||
|
description: 'Beta-Hydroxy Beta-Methylbutyrate supplement for reduced muscle protein breakdown and enhanced recovery.',
|
||||||
|
price: 44.99,
|
||||||
|
image: 'https://images.unsplash.com/photo-1594882645126-14020914d58d?auto=format&fit=crop&q=80&w=800',
|
||||||
|
category: 'Essential Supplements',
|
||||||
|
subCategory: 'Anti-Catabolic',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 178,
|
||||||
|
inStock: true,
|
||||||
|
nutritionalFacts: {
|
||||||
|
servingSize: '3g',
|
||||||
|
servingsPerContainer: 40,
|
||||||
|
calories: 0,
|
||||||
|
protein: 0,
|
||||||
|
carbs: 0,
|
||||||
|
fat: 0,
|
||||||
|
additionalInfo: {
|
||||||
|
'HMB (β-Hydroxy β-Methylbutyrate)': '3g'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dosage: '3g daily',
|
||||||
|
timing: 'Split into 3 doses throughout the day',
|
||||||
|
benefits: [
|
||||||
|
'Reduced muscle breakdown',
|
||||||
|
'Enhanced recovery',
|
||||||
|
'Preserved lean mass',
|
||||||
|
'Improved strength'
|
||||||
|
],
|
||||||
|
sizes: ['120g', '240g'],
|
||||||
|
flavors: ['Unflavored'],
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: 'myHMB® Certified',
|
||||||
|
issuer: 'TSI Group',
|
||||||
|
verificationUrl: 'https://www.myhmb.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
studies: [
|
||||||
|
{
|
||||||
|
title: 'HMB supplementation and muscle preservation',
|
||||||
|
authors: 'Lee et al.',
|
||||||
|
year: 2023,
|
||||||
|
journal: 'Sports Science Review',
|
||||||
|
doi: '10.1000/ssr.2023.009',
|
||||||
|
summary: 'Clinical trial showed significant reduction in muscle protein breakdown during intense training periods'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
59
src/index.css
Normal file
59
src/index.css
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 248 90% 66%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 248 90% 66%;
|
||||||
|
--radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 248 90% 66%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 248 90% 66%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
51
src/store/cartStore.ts
Normal file
51
src/store/cartStore.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { Product } from '../types/product';
|
||||||
|
|
||||||
|
interface CartItem {
|
||||||
|
product: Product;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CartStore {
|
||||||
|
items: CartItem[];
|
||||||
|
addItem: (product: Product) => void;
|
||||||
|
removeItem: (productId: string) => void;
|
||||||
|
updateQuantity: (productId: string, quantity: number) => void;
|
||||||
|
clearCart: () => void;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCartStore = create<CartStore>((set) => ({
|
||||||
|
items: [],
|
||||||
|
addItem: (product) =>
|
||||||
|
set((state) => {
|
||||||
|
const existingItem = state.items.find((item) => item.product.id === product.id);
|
||||||
|
if (existingItem) {
|
||||||
|
return {
|
||||||
|
items: state.items.map((item) =>
|
||||||
|
item.product.id === product.id
|
||||||
|
? { ...item, quantity: item.quantity + 1 }
|
||||||
|
: item
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { items: [...state.items, { product, quantity: 1 }] };
|
||||||
|
}),
|
||||||
|
removeItem: (productId) =>
|
||||||
|
set((state) => ({
|
||||||
|
items: state.items.filter((item) => item.product.id !== productId),
|
||||||
|
})),
|
||||||
|
updateQuantity: (productId, quantity) =>
|
||||||
|
set((state) => ({
|
||||||
|
items: state.items.map((item) =>
|
||||||
|
item.product.id === productId ? { ...item, quantity } : item
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
clearCart: () => set({ items: [] }),
|
||||||
|
get total() {
|
||||||
|
return this.items.reduce(
|
||||||
|
(sum, item) => sum + item.product.price * item.quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
45
src/types/product.ts
Normal file
45
src/types/product.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
export interface NutritionalFacts {
|
||||||
|
servingSize: string;
|
||||||
|
servingsPerContainer: number;
|
||||||
|
calories: number;
|
||||||
|
protein: number;
|
||||||
|
carbs: number;
|
||||||
|
fat: number;
|
||||||
|
additionalInfo?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductCertification {
|
||||||
|
name: string;
|
||||||
|
issuer: string;
|
||||||
|
verificationUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductStudy {
|
||||||
|
title: string;
|
||||||
|
authors: string;
|
||||||
|
year: number;
|
||||||
|
journal: string;
|
||||||
|
doi: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
image: string;
|
||||||
|
category: string;
|
||||||
|
subCategory: string;
|
||||||
|
rating: number;
|
||||||
|
reviews: number;
|
||||||
|
inStock: boolean;
|
||||||
|
nutritionalFacts: NutritionalFacts;
|
||||||
|
dosage: string;
|
||||||
|
timing: string;
|
||||||
|
benefits: string[];
|
||||||
|
sizes: string[];
|
||||||
|
flavors: string[];
|
||||||
|
certifications: ProductCertification[];
|
||||||
|
studies: ProductStudy[];
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
71
tailwind.config.js
Normal file
71
tailwind.config.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
};
|
||||||
30
tsconfig.app.json
Normal file
30
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue