mirror of
https://github.com/harivansh-afk/GymSupps.git
synced 2026-04-15 05:02:10 +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