initial commit

This commit is contained in:
Harivansh Rathi 2025-02-20 19:36:21 -05:00
commit 357979d0af
31 changed files with 6645 additions and 0 deletions

3
.bolt/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

8
.bolt/prompt Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

46
package.json Normal file
View 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
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

79
src/App.tsx Normal file
View 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
View 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>
);
};

View 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
View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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 }

View 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 }

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

71
tailwind.config.js Normal file
View 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
View 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
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View 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
View 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'),
},
},
});