Spaces:
Runtime error
Runtime error
Hanzo Dev
commited on
Commit
·
fc743bc
0
Parent(s):
Initial commit for ecommerce template
Browse files- .gitignore +33 -0
- Dockerfile +21 -0
- LICENSE +21 -0
- README.md +132 -0
- app/globals.css +58 -0
- app/layout.tsx +22 -0
- app/page.tsx +293 -0
- components/ui/avatar.tsx +53 -0
- components/ui/badge.tsx +36 -0
- components/ui/button.tsx +68 -0
- components/ui/card.tsx +79 -0
- components/ui/dialog.tsx +143 -0
- components/ui/input.tsx +21 -0
- components/ui/progress.tsx +28 -0
- components/ui/select.tsx +185 -0
- components/ui/tabs.tsx +66 -0
- lib/utils.ts +6 -0
- next.config.js +9 -0
- package.json +56 -0
- postcss.config.js +6 -0
- src/App.tsx +293 -0
- tailwind.config.js +76 -0
- tsconfig.json +27 -0
.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dependencies
|
| 2 |
+
/node_modules
|
| 3 |
+
/.pnp
|
| 4 |
+
.pnp.js
|
| 5 |
+
|
| 6 |
+
# testing
|
| 7 |
+
/coverage
|
| 8 |
+
|
| 9 |
+
# next.js
|
| 10 |
+
/.next/
|
| 11 |
+
/out/
|
| 12 |
+
|
| 13 |
+
# production
|
| 14 |
+
/build
|
| 15 |
+
|
| 16 |
+
# misc
|
| 17 |
+
.DS_Store
|
| 18 |
+
*.pem
|
| 19 |
+
|
| 20 |
+
# debug
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
| 24 |
+
|
| 25 |
+
# local env files
|
| 26 |
+
.env*.local
|
| 27 |
+
|
| 28 |
+
# vercel
|
| 29 |
+
.vercel
|
| 30 |
+
|
| 31 |
+
# typescript
|
| 32 |
+
*.tsbuildinfo
|
| 33 |
+
next-env.d.ts
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package files
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN npm ci --only=production
|
| 10 |
+
|
| 11 |
+
# Copy application files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build the application
|
| 15 |
+
RUN npm run build
|
| 16 |
+
|
| 17 |
+
# Expose port
|
| 18 |
+
EXPOSE 3000
|
| 19 |
+
|
| 20 |
+
# Start the application
|
| 21 |
+
CMD ["npm", "start"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Hanzo AI
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ecommerce Storefront
|
| 2 |
+
|
| 3 |
+
Complete online store with cart and product management
|
| 4 |
+
|
| 5 |
+
Built with [@hanzo/ui](https://github.com/hanzoai/ui) components - a modern React component library based on Radix UI and Tailwind CSS.
|
| 6 |
+
|
| 7 |
+
## 🚀 Quick Start
|
| 8 |
+
|
| 9 |
+
### Deploy to Hanzo Cloud
|
| 10 |
+
|
| 11 |
+
[](https://hanzo.app/deploy?template=https://github.com/hanzoai/template-ecommerce-storefront)
|
| 12 |
+
|
| 13 |
+
**Instant deployment** - Click to deploy this template to Hanzo Cloud. If you're not signed in, we'll create a public repo for you and you can start editing immediately!
|
| 14 |
+
|
| 15 |
+
### Edit on Hanzo
|
| 16 |
+
|
| 17 |
+
[](https://hanzo.app/edit/github/hanzoai/template-ecommerce-storefront)
|
| 18 |
+
|
| 19 |
+
**Cloud IDE** - Click to open this template in Hanzo's cloud development environment. No local setup required!
|
| 20 |
+
|
| 21 |
+
### Local Development
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
# Clone this template
|
| 25 |
+
git clone https://github.com/hanzoai/template-ecommerce-storefront.git
|
| 26 |
+
cd ecommerce-storefront
|
| 27 |
+
|
| 28 |
+
# Install dependencies
|
| 29 |
+
npm install
|
| 30 |
+
# or
|
| 31 |
+
pnpm install
|
| 32 |
+
|
| 33 |
+
# Start development server
|
| 34 |
+
npm run dev
|
| 35 |
+
# or
|
| 36 |
+
pnpm dev
|
| 37 |
+
|
| 38 |
+
# Open http://localhost:3000
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## 🚢 Deploy to Hugging Face
|
| 42 |
+
|
| 43 |
+
This template includes a built-in publish option for Hugging Face Spaces:
|
| 44 |
+
|
| 45 |
+
1. **Login to Hugging Face** in your terminal:
|
| 46 |
+
```bash
|
| 47 |
+
huggingface-cli login
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
2. **Use the built-in publish command**:
|
| 51 |
+
```bash
|
| 52 |
+
npm run publish-hf
|
| 53 |
+
# or
|
| 54 |
+
pnpm publish-hf
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
This will automatically:
|
| 58 |
+
- Create a new Space in your HF account
|
| 59 |
+
- Configure it for Next.js deployment
|
| 60 |
+
- Push all necessary files
|
| 61 |
+
- Your app will be live at: `https://huggingface.co/spaces/YOUR_USERNAME/ecommerce-storefront`
|
| 62 |
+
|
| 63 |
+
3. **Or manually push** to an existing Space:
|
| 64 |
+
```bash
|
| 65 |
+
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/ecommerce-storefront
|
| 66 |
+
git push hf main
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## 🎨 Features
|
| 70 |
+
|
| 71 |
+
- **Product Grid**: Beautiful product showcase with filters
|
| 72 |
+
- **Shopping Cart**: Full cart functionality with quantity controls
|
| 73 |
+
- **Filters & Search**: Advanced product filtering
|
| 74 |
+
- **Responsive Design**: Works perfectly on all devices
|
| 75 |
+
- **Dark Mode**: Built-in dark mode support
|
| 76 |
+
- **TypeScript**: Full type safety
|
| 77 |
+
|
| 78 |
+
## 📦 What's Included
|
| 79 |
+
|
| 80 |
+
- Next.js 14 with App Router
|
| 81 |
+
- React 18 with Server Components
|
| 82 |
+
- TypeScript configuration
|
| 83 |
+
- Tailwind CSS with custom theme
|
| 84 |
+
- ESLint and Prettier configs
|
| 85 |
+
- @hanzo/ui component library
|
| 86 |
+
- Lucide React icons
|
| 87 |
+
- Hugging Face deployment config
|
| 88 |
+
|
| 89 |
+
## 🛠️ Customization
|
| 90 |
+
|
| 91 |
+
### Theme Colors
|
| 92 |
+
|
| 93 |
+
Edit `tailwind.config.js` to customize the color scheme:
|
| 94 |
+
|
| 95 |
+
```js
|
| 96 |
+
theme: {
|
| 97 |
+
extend: {
|
| 98 |
+
colors: {
|
| 99 |
+
primary: {
|
| 100 |
+
DEFAULT: "hsl(var(--primary))",
|
| 101 |
+
foreground: "hsl(var(--primary-foreground))",
|
| 102 |
+
},
|
| 103 |
+
// Add your custom colors
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### Components
|
| 110 |
+
|
| 111 |
+
All UI components are in `components/ui/`. They're built with:
|
| 112 |
+
- Radix UI primitives for accessibility
|
| 113 |
+
- Tailwind CSS for styling
|
| 114 |
+
- Full TypeScript support
|
| 115 |
+
|
| 116 |
+
## 📚 Documentation
|
| 117 |
+
|
| 118 |
+
- [Hanzo Documentation](https://hanzo.app/docs)
|
| 119 |
+
- [@hanzo/ui Components](https://github.com/hanzoai/ui)
|
| 120 |
+
- [Template Gallery](https://huggingface.co/spaces/hanzo-community/gallery)
|
| 121 |
+
|
| 122 |
+
## 🤝 Contributing
|
| 123 |
+
|
| 124 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
| 125 |
+
|
| 126 |
+
## 📄 License
|
| 127 |
+
|
| 128 |
+
MIT License - see [LICENSE](LICENSE) file for details.
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
Built with ❤️ by [Hanzo AI](https://hanzo.ai)
|
app/globals.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
:root {
|
| 7 |
+
--background: 0 0% 100%;
|
| 8 |
+
--foreground: 222.2 84% 4.9%;
|
| 9 |
+
--card: 0 0% 100%;
|
| 10 |
+
--card-foreground: 222.2 84% 4.9%;
|
| 11 |
+
--popover: 0 0% 100%;
|
| 12 |
+
--popover-foreground: 222.2 84% 4.9%;
|
| 13 |
+
--primary: 222.2 47.4% 11.2%;
|
| 14 |
+
--primary-foreground: 210 40% 98%;
|
| 15 |
+
--secondary: 210 40% 96.1%;
|
| 16 |
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
| 17 |
+
--muted: 210 40% 96.1%;
|
| 18 |
+
--muted-foreground: 215.4 16.3% 46.9%;
|
| 19 |
+
--accent: 210 40% 96.1%;
|
| 20 |
+
--accent-foreground: 222.2 47.4% 11.2%;
|
| 21 |
+
--destructive: 0 84.2% 60.2%;
|
| 22 |
+
--destructive-foreground: 210 40% 98%;
|
| 23 |
+
--border: 214.3 31.8% 91.4%;
|
| 24 |
+
--input: 214.3 31.8% 91.4%;
|
| 25 |
+
--ring: 222.2 84% 4.9%;
|
| 26 |
+
--radius: 0.5rem;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.dark {
|
| 30 |
+
--background: 222.2 84% 4.9%;
|
| 31 |
+
--foreground: 210 40% 98%;
|
| 32 |
+
--card: 222.2 84% 4.9%;
|
| 33 |
+
--card-foreground: 210 40% 98%;
|
| 34 |
+
--popover: 222.2 84% 4.9%;
|
| 35 |
+
--popover-foreground: 210 40% 98%;
|
| 36 |
+
--primary: 210 40% 98%;
|
| 37 |
+
--primary-foreground: 222.2 47.4% 11.2%;
|
| 38 |
+
--secondary: 217.2 32.6% 17.5%;
|
| 39 |
+
--secondary-foreground: 210 40% 98%;
|
| 40 |
+
--muted: 217.2 32.6% 17.5%;
|
| 41 |
+
--muted-foreground: 215 20.2% 65.1%;
|
| 42 |
+
--accent: 217.2 32.6% 17.5%;
|
| 43 |
+
--accent-foreground: 210 40% 98%;
|
| 44 |
+
--destructive: 0 62.8% 30.6%;
|
| 45 |
+
--destructive-foreground: 210 40% 98%;
|
| 46 |
+
--border: 217.2 32.6% 17.5%;
|
| 47 |
+
--input: 217.2 32.6% 17.5%;
|
| 48 |
+
--ring: 212.7 26.8% 83.9%;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
* {
|
| 53 |
+
@apply border-border;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
body {
|
| 57 |
+
@apply bg-background text-foreground;
|
| 58 |
+
}
|
app/layout.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Inter } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const inter = Inter({ subsets: ["latin"] });
|
| 6 |
+
|
| 7 |
+
export const metadata: Metadata = {
|
| 8 |
+
title: "Ecommerce Storefront - Hanzo UI Template",
|
| 9 |
+
description: "Complete online store with cart",
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
export default function RootLayout({
|
| 13 |
+
children,
|
| 14 |
+
}: {
|
| 15 |
+
children: React.ReactNode;
|
| 16 |
+
}) {
|
| 17 |
+
return (
|
| 18 |
+
<html lang="en" suppressHydrationWarning>
|
| 19 |
+
<body className={inter.className}>{children}</body>
|
| 20 |
+
</html>
|
| 21 |
+
);
|
| 22 |
+
}
|
app/page.tsx
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
| 5 |
+
import { Button } from "@/components/ui/button";
|
| 6 |
+
import { Badge } from "@/components/ui/badge";
|
| 7 |
+
import { Input } from "@/components/ui/input";
|
| 8 |
+
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
| 9 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
| 10 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 11 |
+
import { ShoppingCart, Search, Star, Filter, Heart, Share2 } from "lucide-react";
|
| 12 |
+
|
| 13 |
+
const products = [
|
| 14 |
+
{
|
| 15 |
+
id: "1",
|
| 16 |
+
name: "Premium Wireless Headphones",
|
| 17 |
+
price: 299.99,
|
| 18 |
+
image: "/api/placeholder/400/400",
|
| 19 |
+
rating: 4.5,
|
| 20 |
+
reviews: 234,
|
| 21 |
+
badge: "Best Seller",
|
| 22 |
+
variants: ["Black", "White", "Blue"]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: "2",
|
| 26 |
+
name: "Smart Watch Pro",
|
| 27 |
+
price: 399.99,
|
| 28 |
+
image: "/api/placeholder/400/400",
|
| 29 |
+
rating: 4.8,
|
| 30 |
+
reviews: 567,
|
| 31 |
+
badge: "New",
|
| 32 |
+
variants: ["Silver", "Gold", "Space Gray"]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: "3",
|
| 36 |
+
name: "Portable Speaker",
|
| 37 |
+
price: 149.99,
|
| 38 |
+
image: "/api/placeholder/400/400",
|
| 39 |
+
rating: 4.3,
|
| 40 |
+
reviews: 189,
|
| 41 |
+
badge: null,
|
| 42 |
+
variants: ["Red", "Black", "Green"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: "4",
|
| 46 |
+
name: "Laptop Stand",
|
| 47 |
+
price: 79.99,
|
| 48 |
+
image: "/api/placeholder/400/400",
|
| 49 |
+
rating: 4.6,
|
| 50 |
+
reviews: 432,
|
| 51 |
+
badge: "Sale",
|
| 52 |
+
variants: ["Aluminum", "Wood"]
|
| 53 |
+
}
|
| 54 |
+
];
|
| 55 |
+
|
| 56 |
+
const categories = ["All Products", "Electronics", "Accessories", "Audio", "Computing"];
|
| 57 |
+
|
| 58 |
+
export default function EcommerceStorefront() {
|
| 59 |
+
const [selectedCategory, setSelectedCategory] = useState("All Products");
|
| 60 |
+
const [cart, setCart] = useState<{ id: string; quantity: number }[]>([]);
|
| 61 |
+
|
| 62 |
+
const addToCart = (productId: string) => {
|
| 63 |
+
setCart(prev => {
|
| 64 |
+
const existing = prev.find(item => item.id === productId);
|
| 65 |
+
if (existing) {
|
| 66 |
+
return prev.map(item =>
|
| 67 |
+
item.id === productId
|
| 68 |
+
? { ...item, quantity: item.quantity + 1 }
|
| 69 |
+
: item
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
return [...prev, { id: productId, quantity: 1 }];
|
| 73 |
+
});
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const cartItemsCount = cart.reduce((sum, item) => sum + item.quantity, 0);
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div className="min-h-screen bg-background">
|
| 80 |
+
{/* Header */}
|
| 81 |
+
<header className="border-b sticky top-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 z-50">
|
| 82 |
+
<div className="container mx-auto px-6 py-4">
|
| 83 |
+
<div className="flex items-center justify-between">
|
| 84 |
+
<div className="flex items-center gap-8">
|
| 85 |
+
<h1 className="text-2xl font-bold">Store</h1>
|
| 86 |
+
<nav className="hidden md:flex items-center gap-6">
|
| 87 |
+
{categories.map(category => (
|
| 88 |
+
<button
|
| 89 |
+
key={category}
|
| 90 |
+
onClick={() => setSelectedCategory(category)}
|
| 91 |
+
className={`text-sm font-medium transition-colors hover:text-primary ${
|
| 92 |
+
selectedCategory === category
|
| 93 |
+
? "text-primary"
|
| 94 |
+
: "text-muted-foreground"
|
| 95 |
+
}`}
|
| 96 |
+
>
|
| 97 |
+
{category}
|
| 98 |
+
</button>
|
| 99 |
+
))}
|
| 100 |
+
</nav>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div className="flex items-center gap-4">
|
| 104 |
+
<div className="relative hidden md:block">
|
| 105 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
| 106 |
+
<Input
|
| 107 |
+
placeholder="Search products..."
|
| 108 |
+
className="pl-9 w-[200px] lg:w-[300px]"
|
| 109 |
+
/>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<Button variant="ghost" size="icon" className="relative">
|
| 113 |
+
<ShoppingCart className="w-5 h-5" />
|
| 114 |
+
{cartItemsCount > 0 && (
|
| 115 |
+
<Badge className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 flex items-center justify-center">
|
| 116 |
+
{cartItemsCount}
|
| 117 |
+
</Badge>
|
| 118 |
+
)}
|
| 119 |
+
</Button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</header>
|
| 124 |
+
|
| 125 |
+
{/* Hero Section - Orange/Pink Gradient Theme */}
|
| 126 |
+
<section className="bg-gradient-to-r from-orange-500 to-pink-500 text-white py-16">
|
| 127 |
+
<div className="container mx-auto px-6">
|
| 128 |
+
<div className="max-w-3xl">
|
| 129 |
+
<h2 className="text-4xl font-bold mb-4">
|
| 130 |
+
Summer Collection
|
| 131 |
+
</h2>
|
| 132 |
+
<p className="text-xl mb-6 opacity-90">
|
| 133 |
+
Discover our latest products built with @hanzo/ui components
|
| 134 |
+
</p>
|
| 135 |
+
<Button size="lg" variant="secondary">
|
| 136 |
+
Shop Now
|
| 137 |
+
</Button>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</section>
|
| 141 |
+
|
| 142 |
+
{/* Filters Bar */}
|
| 143 |
+
<div className="border-b">
|
| 144 |
+
<div className="container mx-auto px-6 py-4">
|
| 145 |
+
<div className="flex items-center justify-between">
|
| 146 |
+
<div className="flex items-center gap-4">
|
| 147 |
+
<Button variant="outline" className="gap-2">
|
| 148 |
+
<Filter className="w-4 h-4" />
|
| 149 |
+
Filters
|
| 150 |
+
</Button>
|
| 151 |
+
<Select defaultValue="featured">
|
| 152 |
+
<SelectTrigger className="w-[180px]">
|
| 153 |
+
<SelectValue />
|
| 154 |
+
</SelectTrigger>
|
| 155 |
+
<SelectContent>
|
| 156 |
+
<SelectItem value="featured">Featured</SelectItem>
|
| 157 |
+
<SelectItem value="price-low">Price: Low to High</SelectItem>
|
| 158 |
+
<SelectItem value="price-high">Price: High to Low</SelectItem>
|
| 159 |
+
<SelectItem value="rating">Highest Rated</SelectItem>
|
| 160 |
+
<SelectItem value="newest">Newest</SelectItem>
|
| 161 |
+
</SelectContent>
|
| 162 |
+
</Select>
|
| 163 |
+
</div>
|
| 164 |
+
<p className="text-sm text-muted-foreground">
|
| 165 |
+
Showing {products.length} products
|
| 166 |
+
</p>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Products Grid */}
|
| 172 |
+
<section className="py-12">
|
| 173 |
+
<div className="container mx-auto px-6">
|
| 174 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 175 |
+
{products.map(product => (
|
| 176 |
+
<Card key={product.id} className="overflow-hidden group">
|
| 177 |
+
<div className="relative">
|
| 178 |
+
<AspectRatio ratio={1}>
|
| 179 |
+
<img
|
| 180 |
+
src={product.image}
|
| 181 |
+
alt={product.name}
|
| 182 |
+
className="object-cover w-full h-full group-hover:scale-105 transition-transform"
|
| 183 |
+
/>
|
| 184 |
+
</AspectRatio>
|
| 185 |
+
{product.badge && (
|
| 186 |
+
<Badge className="absolute top-2 left-2">
|
| 187 |
+
{product.badge}
|
| 188 |
+
</Badge>
|
| 189 |
+
)}
|
| 190 |
+
<div className="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 191 |
+
<Button size="icon" variant="secondary" className="h-8 w-8">
|
| 192 |
+
<Heart className="w-4 h-4" />
|
| 193 |
+
</Button>
|
| 194 |
+
<Button size="icon" variant="secondary" className="h-8 w-8">
|
| 195 |
+
<Share2 className="w-4 h-4" />
|
| 196 |
+
</Button>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<CardContent className="p-4">
|
| 201 |
+
<h3 className="font-semibold mb-2">{product.name}</h3>
|
| 202 |
+
<div className="flex items-center gap-2 mb-2">
|
| 203 |
+
<div className="flex items-center">
|
| 204 |
+
{[...Array(5)].map((_, i) => (
|
| 205 |
+
<Star
|
| 206 |
+
key={i}
|
| 207 |
+
className={`w-4 h-4 ${
|
| 208 |
+
i < Math.floor(product.rating)
|
| 209 |
+
? "fill-yellow-400 text-yellow-400"
|
| 210 |
+
: "text-muted-foreground"
|
| 211 |
+
}`}
|
| 212 |
+
/>
|
| 213 |
+
))}
|
| 214 |
+
</div>
|
| 215 |
+
<span className="text-sm text-muted-foreground">
|
| 216 |
+
({product.reviews})
|
| 217 |
+
</span>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="flex items-center justify-between mb-3">
|
| 220 |
+
<span className="text-2xl font-bold">
|
| 221 |
+
${product.price}
|
| 222 |
+
</span>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
{/* Variant Selector */}
|
| 226 |
+
<div className="mb-3">
|
| 227 |
+
<Select defaultValue={product.variants[0]}>
|
| 228 |
+
<SelectTrigger className="w-full h-8 text-sm">
|
| 229 |
+
<SelectValue />
|
| 230 |
+
</SelectTrigger>
|
| 231 |
+
<SelectContent>
|
| 232 |
+
{product.variants.map(variant => (
|
| 233 |
+
<SelectItem key={variant} value={variant}>
|
| 234 |
+
{variant}
|
| 235 |
+
</SelectItem>
|
| 236 |
+
))}
|
| 237 |
+
</SelectContent>
|
| 238 |
+
</Select>
|
| 239 |
+
</div>
|
| 240 |
+
</CardContent>
|
| 241 |
+
|
| 242 |
+
<CardFooter className="p-4 pt-0">
|
| 243 |
+
<Button
|
| 244 |
+
className="w-full"
|
| 245 |
+
onClick={() => addToCart(product.id)}
|
| 246 |
+
>
|
| 247 |
+
<ShoppingCart className="w-4 h-4 mr-2" />
|
| 248 |
+
Add to Cart
|
| 249 |
+
</Button>
|
| 250 |
+
</CardFooter>
|
| 251 |
+
</Card>
|
| 252 |
+
))}
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
</section>
|
| 256 |
+
|
| 257 |
+
{/* Features */}
|
| 258 |
+
<section className="py-12 bg-muted">
|
| 259 |
+
<div className="container mx-auto px-6">
|
| 260 |
+
<div className="grid md:grid-cols-3 gap-8">
|
| 261 |
+
<div className="text-center">
|
| 262 |
+
<div className="w-12 h-12 rounded-full bg-orange-500/10 flex items-center justify-center mx-auto mb-3">
|
| 263 |
+
<ShoppingCart className="w-6 h-6 text-orange-500" />
|
| 264 |
+
</div>
|
| 265 |
+
<h3 className="font-semibold mb-1">Free Shipping</h3>
|
| 266 |
+
<p className="text-sm text-muted-foreground">
|
| 267 |
+
On orders over $100
|
| 268 |
+
</p>
|
| 269 |
+
</div>
|
| 270 |
+
<div className="text-center">
|
| 271 |
+
<div className="w-12 h-12 rounded-full bg-pink-500/10 flex items-center justify-center mx-auto mb-3">
|
| 272 |
+
<Star className="w-6 h-6 text-pink-500" />
|
| 273 |
+
</div>
|
| 274 |
+
<h3 className="font-semibold mb-1">Quality Products</h3>
|
| 275 |
+
<p className="text-sm text-muted-foreground">
|
| 276 |
+
100% authentic brands
|
| 277 |
+
</p>
|
| 278 |
+
</div>
|
| 279 |
+
<div className="text-center">
|
| 280 |
+
<div className="w-12 h-12 rounded-full bg-rose-500/10 flex items-center justify-center mx-auto mb-3">
|
| 281 |
+
<Heart className="w-6 h-6 text-rose-500" />
|
| 282 |
+
</div>
|
| 283 |
+
<h3 className="font-semibold mb-1">24/7 Support</h3>
|
| 284 |
+
<p className="text-sm text-muted-foreground">
|
| 285 |
+
Dedicated customer service
|
| 286 |
+
</p>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
</section>
|
| 291 |
+
</div>
|
| 292 |
+
);
|
| 293 |
+
}
|
components/ui/avatar.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Avatar({
|
| 9 |
+
className,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
| 12 |
+
return (
|
| 13 |
+
<AvatarPrimitive.Root
|
| 14 |
+
data-slot="avatar"
|
| 15 |
+
className={cn(
|
| 16 |
+
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function AvatarImage({
|
| 25 |
+
className,
|
| 26 |
+
...props
|
| 27 |
+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
| 28 |
+
return (
|
| 29 |
+
<AvatarPrimitive.Image
|
| 30 |
+
data-slot="avatar-image"
|
| 31 |
+
className={cn("aspect-square size-full", className)}
|
| 32 |
+
{...props}
|
| 33 |
+
/>
|
| 34 |
+
)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function AvatarFallback({
|
| 38 |
+
className,
|
| 39 |
+
...props
|
| 40 |
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
| 41 |
+
return (
|
| 42 |
+
<AvatarPrimitive.Fallback
|
| 43 |
+
data-slot="avatar-fallback"
|
| 44 |
+
className={cn(
|
| 45 |
+
"bg-muted flex size-full items-center justify-center rounded-full",
|
| 46 |
+
className
|
| 47 |
+
)}
|
| 48 |
+
{...props}
|
| 49 |
+
/>
|
| 50 |
+
)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
components/ui/badge.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const badgeVariants = cva(
|
| 7 |
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
| 8 |
+
{
|
| 9 |
+
variants: {
|
| 10 |
+
variant: {
|
| 11 |
+
default:
|
| 12 |
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
| 13 |
+
secondary:
|
| 14 |
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 15 |
+
destructive:
|
| 16 |
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
| 17 |
+
outline: "text-foreground",
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
defaultVariants: {
|
| 21 |
+
variant: "default",
|
| 22 |
+
},
|
| 23 |
+
}
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
export interface BadgeProps
|
| 27 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
| 28 |
+
VariantProps<typeof badgeVariants> {}
|
| 29 |
+
|
| 30 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 31 |
+
return (
|
| 32 |
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
| 33 |
+
)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export { Badge, badgeVariants }
|
components/ui/button.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react";
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot";
|
| 3 |
+
import { cva, type VariantProps } from "class-variance-authority";
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils";
|
| 6 |
+
|
| 7 |
+
const buttonVariants = cva(
|
| 8 |
+
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full text-sm font-sans font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 9 |
+
{
|
| 10 |
+
variants: {
|
| 11 |
+
variant: {
|
| 12 |
+
default:
|
| 13 |
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
| 14 |
+
destructive:
|
| 15 |
+
"bg-red-500 text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 [&_svg]:!text-white",
|
| 16 |
+
outline:
|
| 17 |
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
| 18 |
+
secondary:
|
| 19 |
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
| 20 |
+
ghost:
|
| 21 |
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
| 22 |
+
lightGray: "bg-neutral-200/60 hover:bg-neutral-200",
|
| 23 |
+
link: "text-primary underline-offset-4 hover:underline",
|
| 24 |
+
ghostDarker:
|
| 25 |
+
"text-white shadow-xs focus-visible:ring-black/40 bg-black/40 hover:bg-black/70",
|
| 26 |
+
black: "bg-neutral-950 text-neutral-300 hover:brightness-110",
|
| 27 |
+
sky: "bg-sky-500 text-white hover:brightness-110",
|
| 28 |
+
},
|
| 29 |
+
size: {
|
| 30 |
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
| 31 |
+
sm: "h-8 rounded-full text-[13px] gap-1.5 px-3",
|
| 32 |
+
lg: "h-10 rounded-full px-6 has-[>svg]:px-4",
|
| 33 |
+
icon: "size-9",
|
| 34 |
+
iconXs: "size-7",
|
| 35 |
+
iconXss: "size-6",
|
| 36 |
+
iconXsss: "size-5",
|
| 37 |
+
xs: "h-6 text-xs rounded-full pl-2 pr-2 gap-1",
|
| 38 |
+
},
|
| 39 |
+
},
|
| 40 |
+
defaultVariants: {
|
| 41 |
+
variant: "default",
|
| 42 |
+
size: "default",
|
| 43 |
+
},
|
| 44 |
+
}
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
function Button({
|
| 48 |
+
className,
|
| 49 |
+
variant,
|
| 50 |
+
size,
|
| 51 |
+
asChild = false,
|
| 52 |
+
...props
|
| 53 |
+
}: React.ComponentProps<"button"> &
|
| 54 |
+
VariantProps<typeof buttonVariants> & {
|
| 55 |
+
asChild?: boolean;
|
| 56 |
+
}) {
|
| 57 |
+
const Comp = asChild ? Slot : "button";
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<Comp
|
| 61 |
+
data-slot="button"
|
| 62 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 63 |
+
{...props}
|
| 64 |
+
/>
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
export { Button, buttonVariants };
|
components/ui/card.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
const Card = React.forwardRef<
|
| 6 |
+
HTMLDivElement,
|
| 7 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 8 |
+
>(({ className, ...props }, ref) => (
|
| 9 |
+
<div
|
| 10 |
+
ref={ref}
|
| 11 |
+
className={cn(
|
| 12 |
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
{...props}
|
| 16 |
+
/>
|
| 17 |
+
))
|
| 18 |
+
Card.displayName = "Card"
|
| 19 |
+
|
| 20 |
+
const CardHeader = React.forwardRef<
|
| 21 |
+
HTMLDivElement,
|
| 22 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 23 |
+
>(({ className, ...props }, ref) => (
|
| 24 |
+
<div
|
| 25 |
+
ref={ref}
|
| 26 |
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
| 27 |
+
{...props}
|
| 28 |
+
/>
|
| 29 |
+
))
|
| 30 |
+
CardHeader.displayName = "CardHeader"
|
| 31 |
+
|
| 32 |
+
const CardTitle = React.forwardRef<
|
| 33 |
+
HTMLParagraphElement,
|
| 34 |
+
React.HTMLAttributes<HTMLHeadingElement>
|
| 35 |
+
>(({ className, ...props }, ref) => (
|
| 36 |
+
<h3
|
| 37 |
+
ref={ref}
|
| 38 |
+
className={cn(
|
| 39 |
+
"text-2xl font-semibold leading-none tracking-tight",
|
| 40 |
+
className
|
| 41 |
+
)}
|
| 42 |
+
{...props}
|
| 43 |
+
/>
|
| 44 |
+
))
|
| 45 |
+
CardTitle.displayName = "CardTitle"
|
| 46 |
+
|
| 47 |
+
const CardDescription = React.forwardRef<
|
| 48 |
+
HTMLParagraphElement,
|
| 49 |
+
React.HTMLAttributes<HTMLParagraphElement>
|
| 50 |
+
>(({ className, ...props }, ref) => (
|
| 51 |
+
<p
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("text-sm text-muted-foreground", className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
))
|
| 57 |
+
CardDescription.displayName = "CardDescription"
|
| 58 |
+
|
| 59 |
+
const CardContent = React.forwardRef<
|
| 60 |
+
HTMLDivElement,
|
| 61 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 62 |
+
>(({ className, ...props }, ref) => (
|
| 63 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 64 |
+
))
|
| 65 |
+
CardContent.displayName = "CardContent"
|
| 66 |
+
|
| 67 |
+
const CardFooter = React.forwardRef<
|
| 68 |
+
HTMLDivElement,
|
| 69 |
+
React.HTMLAttributes<HTMLDivElement>
|
| 70 |
+
>(({ className, ...props }, ref) => (
|
| 71 |
+
<div
|
| 72 |
+
ref={ref}
|
| 73 |
+
className={cn("flex items-center p-6 pt-0", className)}
|
| 74 |
+
{...props}
|
| 75 |
+
/>
|
| 76 |
+
))
|
| 77 |
+
CardFooter.displayName = "CardFooter"
|
| 78 |
+
|
| 79 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
components/ui/dialog.tsx
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
| 5 |
+
import { XIcon } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
function Dialog({
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
| 12 |
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function DialogTrigger({
|
| 16 |
+
...props
|
| 17 |
+
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
| 18 |
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function DialogPortal({
|
| 22 |
+
...props
|
| 23 |
+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
| 24 |
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function DialogClose({
|
| 28 |
+
...props
|
| 29 |
+
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
| 30 |
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function DialogOverlay({
|
| 34 |
+
className,
|
| 35 |
+
...props
|
| 36 |
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
| 37 |
+
return (
|
| 38 |
+
<DialogPrimitive.Overlay
|
| 39 |
+
data-slot="dialog-overlay"
|
| 40 |
+
className={cn(
|
| 41 |
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
| 42 |
+
className
|
| 43 |
+
)}
|
| 44 |
+
{...props}
|
| 45 |
+
/>
|
| 46 |
+
)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function DialogContent({
|
| 50 |
+
className,
|
| 51 |
+
children,
|
| 52 |
+
showCloseButton = true,
|
| 53 |
+
...props
|
| 54 |
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
| 55 |
+
showCloseButton?: boolean
|
| 56 |
+
}) {
|
| 57 |
+
return (
|
| 58 |
+
<DialogPortal data-slot="dialog-portal">
|
| 59 |
+
<DialogOverlay />
|
| 60 |
+
<DialogPrimitive.Content
|
| 61 |
+
data-slot="dialog-content"
|
| 62 |
+
className={cn(
|
| 63 |
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
| 64 |
+
className
|
| 65 |
+
)}
|
| 66 |
+
{...props}
|
| 67 |
+
>
|
| 68 |
+
{children}
|
| 69 |
+
{showCloseButton && (
|
| 70 |
+
<DialogPrimitive.Close
|
| 71 |
+
data-slot="dialog-close"
|
| 72 |
+
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
| 73 |
+
>
|
| 74 |
+
<XIcon />
|
| 75 |
+
<span className="sr-only">Close</span>
|
| 76 |
+
</DialogPrimitive.Close>
|
| 77 |
+
)}
|
| 78 |
+
</DialogPrimitive.Content>
|
| 79 |
+
</DialogPortal>
|
| 80 |
+
)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 84 |
+
return (
|
| 85 |
+
<div
|
| 86 |
+
data-slot="dialog-header"
|
| 87 |
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
| 88 |
+
{...props}
|
| 89 |
+
/>
|
| 90 |
+
)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
| 94 |
+
return (
|
| 95 |
+
<div
|
| 96 |
+
data-slot="dialog-footer"
|
| 97 |
+
className={cn(
|
| 98 |
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
| 99 |
+
className
|
| 100 |
+
)}
|
| 101 |
+
{...props}
|
| 102 |
+
/>
|
| 103 |
+
)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function DialogTitle({
|
| 107 |
+
className,
|
| 108 |
+
...props
|
| 109 |
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
| 110 |
+
return (
|
| 111 |
+
<DialogPrimitive.Title
|
| 112 |
+
data-slot="dialog-title"
|
| 113 |
+
className={cn("text-lg leading-none font-semibold", className)}
|
| 114 |
+
{...props}
|
| 115 |
+
/>
|
| 116 |
+
)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function DialogDescription({
|
| 120 |
+
className,
|
| 121 |
+
...props
|
| 122 |
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
| 123 |
+
return (
|
| 124 |
+
<DialogPrimitive.Description
|
| 125 |
+
data-slot="dialog-description"
|
| 126 |
+
className={cn("text-muted-foreground text-sm", className)}
|
| 127 |
+
{...props}
|
| 128 |
+
/>
|
| 129 |
+
)
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export {
|
| 133 |
+
Dialog,
|
| 134 |
+
DialogClose,
|
| 135 |
+
DialogContent,
|
| 136 |
+
DialogDescription,
|
| 137 |
+
DialogFooter,
|
| 138 |
+
DialogHeader,
|
| 139 |
+
DialogOverlay,
|
| 140 |
+
DialogPortal,
|
| 141 |
+
DialogTitle,
|
| 142 |
+
DialogTrigger,
|
| 143 |
+
}
|
components/ui/input.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
| 6 |
+
return (
|
| 7 |
+
<input
|
| 8 |
+
type={type}
|
| 9 |
+
data-slot="input"
|
| 10 |
+
className={cn(
|
| 11 |
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
| 12 |
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
| 13 |
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 14 |
+
className
|
| 15 |
+
)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export { Input }
|
components/ui/progress.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const Progress = React.forwardRef<
|
| 7 |
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
| 8 |
+
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
| 9 |
+
indicatorClassName?: string
|
| 10 |
+
}
|
| 11 |
+
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
| 12 |
+
<ProgressPrimitive.Root
|
| 13 |
+
ref={ref}
|
| 14 |
+
className={cn(
|
| 15 |
+
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
|
| 16 |
+
className
|
| 17 |
+
)}
|
| 18 |
+
{...props}
|
| 19 |
+
>
|
| 20 |
+
<ProgressPrimitive.Indicator
|
| 21 |
+
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
| 22 |
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
| 23 |
+
/>
|
| 24 |
+
</ProgressPrimitive.Root>
|
| 25 |
+
))
|
| 26 |
+
Progress.displayName = ProgressPrimitive.Root.displayName
|
| 27 |
+
|
| 28 |
+
export { Progress }
|
components/ui/select.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
| 5 |
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
function Select({
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
| 12 |
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function SelectGroup({
|
| 16 |
+
...props
|
| 17 |
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
| 18 |
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function SelectValue({
|
| 22 |
+
...props
|
| 23 |
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
| 24 |
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function SelectTrigger({
|
| 28 |
+
className,
|
| 29 |
+
size = "default",
|
| 30 |
+
children,
|
| 31 |
+
...props
|
| 32 |
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
| 33 |
+
size?: "sm" | "default"
|
| 34 |
+
}) {
|
| 35 |
+
return (
|
| 36 |
+
<SelectPrimitive.Trigger
|
| 37 |
+
data-slot="select-trigger"
|
| 38 |
+
data-size={size}
|
| 39 |
+
className={cn(
|
| 40 |
+
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 41 |
+
className
|
| 42 |
+
)}
|
| 43 |
+
{...props}
|
| 44 |
+
>
|
| 45 |
+
{children}
|
| 46 |
+
<SelectPrimitive.Icon asChild>
|
| 47 |
+
<ChevronDownIcon className="size-4 opacity-50" />
|
| 48 |
+
</SelectPrimitive.Icon>
|
| 49 |
+
</SelectPrimitive.Trigger>
|
| 50 |
+
)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function SelectContent({
|
| 54 |
+
className,
|
| 55 |
+
children,
|
| 56 |
+
position = "popper",
|
| 57 |
+
...props
|
| 58 |
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
| 59 |
+
return (
|
| 60 |
+
<SelectPrimitive.Portal>
|
| 61 |
+
<SelectPrimitive.Content
|
| 62 |
+
data-slot="select-content"
|
| 63 |
+
className={cn(
|
| 64 |
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
| 65 |
+
position === "popper" &&
|
| 66 |
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
| 67 |
+
className
|
| 68 |
+
)}
|
| 69 |
+
position={position}
|
| 70 |
+
{...props}
|
| 71 |
+
>
|
| 72 |
+
<SelectScrollUpButton />
|
| 73 |
+
<SelectPrimitive.Viewport
|
| 74 |
+
className={cn(
|
| 75 |
+
"p-1",
|
| 76 |
+
position === "popper" &&
|
| 77 |
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
| 78 |
+
)}
|
| 79 |
+
>
|
| 80 |
+
{children}
|
| 81 |
+
</SelectPrimitive.Viewport>
|
| 82 |
+
<SelectScrollDownButton />
|
| 83 |
+
</SelectPrimitive.Content>
|
| 84 |
+
</SelectPrimitive.Portal>
|
| 85 |
+
)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function SelectLabel({
|
| 89 |
+
className,
|
| 90 |
+
...props
|
| 91 |
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
| 92 |
+
return (
|
| 93 |
+
<SelectPrimitive.Label
|
| 94 |
+
data-slot="select-label"
|
| 95 |
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
| 96 |
+
{...props}
|
| 97 |
+
/>
|
| 98 |
+
)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function SelectItem({
|
| 102 |
+
className,
|
| 103 |
+
children,
|
| 104 |
+
...props
|
| 105 |
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
| 106 |
+
return (
|
| 107 |
+
<SelectPrimitive.Item
|
| 108 |
+
data-slot="select-item"
|
| 109 |
+
className={cn(
|
| 110 |
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
| 111 |
+
className
|
| 112 |
+
)}
|
| 113 |
+
{...props}
|
| 114 |
+
>
|
| 115 |
+
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
| 116 |
+
<SelectPrimitive.ItemIndicator>
|
| 117 |
+
<CheckIcon className="size-4" />
|
| 118 |
+
</SelectPrimitive.ItemIndicator>
|
| 119 |
+
</span>
|
| 120 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 121 |
+
</SelectPrimitive.Item>
|
| 122 |
+
)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function SelectSeparator({
|
| 126 |
+
className,
|
| 127 |
+
...props
|
| 128 |
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
| 129 |
+
return (
|
| 130 |
+
<SelectPrimitive.Separator
|
| 131 |
+
data-slot="select-separator"
|
| 132 |
+
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
| 133 |
+
{...props}
|
| 134 |
+
/>
|
| 135 |
+
)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function SelectScrollUpButton({
|
| 139 |
+
className,
|
| 140 |
+
...props
|
| 141 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
| 142 |
+
return (
|
| 143 |
+
<SelectPrimitive.ScrollUpButton
|
| 144 |
+
data-slot="select-scroll-up-button"
|
| 145 |
+
className={cn(
|
| 146 |
+
"flex cursor-default items-center justify-center py-1",
|
| 147 |
+
className
|
| 148 |
+
)}
|
| 149 |
+
{...props}
|
| 150 |
+
>
|
| 151 |
+
<ChevronUpIcon className="size-4" />
|
| 152 |
+
</SelectPrimitive.ScrollUpButton>
|
| 153 |
+
)
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function SelectScrollDownButton({
|
| 157 |
+
className,
|
| 158 |
+
...props
|
| 159 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
| 160 |
+
return (
|
| 161 |
+
<SelectPrimitive.ScrollDownButton
|
| 162 |
+
data-slot="select-scroll-down-button"
|
| 163 |
+
className={cn(
|
| 164 |
+
"flex cursor-default items-center justify-center py-1",
|
| 165 |
+
className
|
| 166 |
+
)}
|
| 167 |
+
{...props}
|
| 168 |
+
>
|
| 169 |
+
<ChevronDownIcon className="size-4" />
|
| 170 |
+
</SelectPrimitive.ScrollDownButton>
|
| 171 |
+
)
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
export {
|
| 175 |
+
Select,
|
| 176 |
+
SelectContent,
|
| 177 |
+
SelectGroup,
|
| 178 |
+
SelectItem,
|
| 179 |
+
SelectLabel,
|
| 180 |
+
SelectScrollDownButton,
|
| 181 |
+
SelectScrollUpButton,
|
| 182 |
+
SelectSeparator,
|
| 183 |
+
SelectTrigger,
|
| 184 |
+
SelectValue,
|
| 185 |
+
}
|
components/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Tabs({
|
| 9 |
+
className,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
| 12 |
+
return (
|
| 13 |
+
<TabsPrimitive.Root
|
| 14 |
+
data-slot="tabs"
|
| 15 |
+
className={cn("flex flex-col gap-2", className)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function TabsList({
|
| 22 |
+
className,
|
| 23 |
+
...props
|
| 24 |
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
| 25 |
+
return (
|
| 26 |
+
<TabsPrimitive.List
|
| 27 |
+
data-slot="tabs-list"
|
| 28 |
+
className={cn(
|
| 29 |
+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
| 30 |
+
className
|
| 31 |
+
)}
|
| 32 |
+
{...props}
|
| 33 |
+
/>
|
| 34 |
+
)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function TabsTrigger({
|
| 38 |
+
className,
|
| 39 |
+
...props
|
| 40 |
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
| 41 |
+
return (
|
| 42 |
+
<TabsPrimitive.Trigger
|
| 43 |
+
data-slot="tabs-trigger"
|
| 44 |
+
className={cn(
|
| 45 |
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 46 |
+
className
|
| 47 |
+
)}
|
| 48 |
+
{...props}
|
| 49 |
+
/>
|
| 50 |
+
)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function TabsContent({
|
| 54 |
+
className,
|
| 55 |
+
...props
|
| 56 |
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
| 57 |
+
return (
|
| 58 |
+
<TabsPrimitive.Content
|
| 59 |
+
data-slot="tabs-content"
|
| 60 |
+
className={cn("flex-1 outline-none", className)}
|
| 61 |
+
{...props}
|
| 62 |
+
/>
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
lib/utils.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type ClassValue, clsx } from "clsx"
|
| 2 |
+
import { twMerge } from "tailwind-merge"
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs))
|
| 6 |
+
}
|
next.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
reactStrictMode: true,
|
| 4 |
+
images: {
|
| 5 |
+
domains: ['api.placeholder.com'],
|
| 6 |
+
},
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
module.exports = nextConfig
|
package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@hanzo/template-ecommerce-storefront",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Complete online store with cart",
|
| 5 |
+
"repository": {
|
| 6 |
+
"type": "git",
|
| 7 |
+
"url": "https://github.com/hanzoai/template-ecommerce-storefront"
|
| 8 |
+
},
|
| 9 |
+
"scripts": {
|
| 10 |
+
"dev": "next dev",
|
| 11 |
+
"build": "next build",
|
| 12 |
+
"start": "next start",
|
| 13 |
+
"lint": "next lint",
|
| 14 |
+
"preview": "vite preview"
|
| 15 |
+
},
|
| 16 |
+
"dependencies": {
|
| 17 |
+
"next": "14.2.5",
|
| 18 |
+
"react": "^18.3.1",
|
| 19 |
+
"react-dom": "^18.3.1",
|
| 20 |
+
"lucide-react": "^0.400.0",
|
| 21 |
+
"clsx": "^2.1.1",
|
| 22 |
+
"tailwind-merge": "^2.3.0",
|
| 23 |
+
"class-variance-authority": "^0.7.0",
|
| 24 |
+
"@radix-ui/react-accordion": "^1.1.2",
|
| 25 |
+
"@radix-ui/react-alert-dialog": "^1.0.5",
|
| 26 |
+
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
| 27 |
+
"@radix-ui/react-avatar": "^1.0.4",
|
| 28 |
+
"@radix-ui/react-checkbox": "^1.0.4",
|
| 29 |
+
"@radix-ui/react-dialog": "^1.0.5",
|
| 30 |
+
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
| 31 |
+
"@radix-ui/react-label": "^2.0.2",
|
| 32 |
+
"@radix-ui/react-popover": "^1.0.7",
|
| 33 |
+
"@radix-ui/react-progress": "^1.0.3",
|
| 34 |
+
"@radix-ui/react-scroll-area": "^1.0.5",
|
| 35 |
+
"@radix-ui/react-select": "^2.0.0",
|
| 36 |
+
"@radix-ui/react-separator": "^1.0.3",
|
| 37 |
+
"@radix-ui/react-slot": "^1.0.2",
|
| 38 |
+
"@radix-ui/react-switch": "^1.0.3",
|
| 39 |
+
"@radix-ui/react-tabs": "^1.0.4",
|
| 40 |
+
"@radix-ui/react-toggle": "^1.0.3",
|
| 41 |
+
"@radix-ui/react-toggle-group": "^1.0.4",
|
| 42 |
+
"@radix-ui/react-tooltip": "^1.0.7"
|
| 43 |
+
},
|
| 44 |
+
"devDependencies": {
|
| 45 |
+
"@types/node": "^20",
|
| 46 |
+
"@types/react": "^18",
|
| 47 |
+
"@types/react-dom": "^18",
|
| 48 |
+
"autoprefixer": "^10.4.19",
|
| 49 |
+
"eslint": "^8",
|
| 50 |
+
"eslint-config-next": "14.2.5",
|
| 51 |
+
"postcss": "^8.4.39",
|
| 52 |
+
"tailwindcss": "^3.4.4",
|
| 53 |
+
"typescript": "^5",
|
| 54 |
+
"vite": "^5.0.0"
|
| 55 |
+
}
|
| 56 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
src/App.tsx
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
| 5 |
+
import { Button } from "@/components/ui/button";
|
| 6 |
+
import { Badge } from "@/components/ui/badge";
|
| 7 |
+
import { Input } from "@/components/ui/input";
|
| 8 |
+
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
| 9 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
| 10 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 11 |
+
import { ShoppingCart, Search, Star, Filter, Heart, Share2 } from "lucide-react";
|
| 12 |
+
|
| 13 |
+
const products = [
|
| 14 |
+
{
|
| 15 |
+
id: "1",
|
| 16 |
+
name: "Premium Wireless Headphones",
|
| 17 |
+
price: 299.99,
|
| 18 |
+
image: "/api/placeholder/400/400",
|
| 19 |
+
rating: 4.5,
|
| 20 |
+
reviews: 234,
|
| 21 |
+
badge: "Best Seller",
|
| 22 |
+
variants: ["Black", "White", "Blue"]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: "2",
|
| 26 |
+
name: "Smart Watch Pro",
|
| 27 |
+
price: 399.99,
|
| 28 |
+
image: "/api/placeholder/400/400",
|
| 29 |
+
rating: 4.8,
|
| 30 |
+
reviews: 567,
|
| 31 |
+
badge: "New",
|
| 32 |
+
variants: ["Silver", "Gold", "Space Gray"]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
id: "3",
|
| 36 |
+
name: "Portable Speaker",
|
| 37 |
+
price: 149.99,
|
| 38 |
+
image: "/api/placeholder/400/400",
|
| 39 |
+
rating: 4.3,
|
| 40 |
+
reviews: 189,
|
| 41 |
+
badge: null,
|
| 42 |
+
variants: ["Red", "Black", "Green"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
id: "4",
|
| 46 |
+
name: "Laptop Stand",
|
| 47 |
+
price: 79.99,
|
| 48 |
+
image: "/api/placeholder/400/400",
|
| 49 |
+
rating: 4.6,
|
| 50 |
+
reviews: 432,
|
| 51 |
+
badge: "Sale",
|
| 52 |
+
variants: ["Aluminum", "Wood"]
|
| 53 |
+
}
|
| 54 |
+
];
|
| 55 |
+
|
| 56 |
+
const categories = ["All Products", "Electronics", "Accessories", "Audio", "Computing"];
|
| 57 |
+
|
| 58 |
+
export default function EcommerceStorefront() {
|
| 59 |
+
const [selectedCategory, setSelectedCategory] = useState("All Products");
|
| 60 |
+
const [cart, setCart] = useState<{ id: string; quantity: number }[]>([]);
|
| 61 |
+
|
| 62 |
+
const addToCart = (productId: string) => {
|
| 63 |
+
setCart(prev => {
|
| 64 |
+
const existing = prev.find(item => item.id === productId);
|
| 65 |
+
if (existing) {
|
| 66 |
+
return prev.map(item =>
|
| 67 |
+
item.id === productId
|
| 68 |
+
? { ...item, quantity: item.quantity + 1 }
|
| 69 |
+
: item
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
return [...prev, { id: productId, quantity: 1 }];
|
| 73 |
+
});
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const cartItemsCount = cart.reduce((sum, item) => sum + item.quantity, 0);
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div className="min-h-screen bg-background">
|
| 80 |
+
{/* Header */}
|
| 81 |
+
<header className="border-b sticky top-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 z-50">
|
| 82 |
+
<div className="container mx-auto px-6 py-4">
|
| 83 |
+
<div className="flex items-center justify-between">
|
| 84 |
+
<div className="flex items-center gap-8">
|
| 85 |
+
<h1 className="text-2xl font-bold">Store</h1>
|
| 86 |
+
<nav className="hidden md:flex items-center gap-6">
|
| 87 |
+
{categories.map(category => (
|
| 88 |
+
<button
|
| 89 |
+
key={category}
|
| 90 |
+
onClick={() => setSelectedCategory(category)}
|
| 91 |
+
className={`text-sm font-medium transition-colors hover:text-primary ${
|
| 92 |
+
selectedCategory === category
|
| 93 |
+
? "text-primary"
|
| 94 |
+
: "text-muted-foreground"
|
| 95 |
+
}`}
|
| 96 |
+
>
|
| 97 |
+
{category}
|
| 98 |
+
</button>
|
| 99 |
+
))}
|
| 100 |
+
</nav>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div className="flex items-center gap-4">
|
| 104 |
+
<div className="relative hidden md:block">
|
| 105 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
| 106 |
+
<Input
|
| 107 |
+
placeholder="Search products..."
|
| 108 |
+
className="pl-9 w-[200px] lg:w-[300px]"
|
| 109 |
+
/>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<Button variant="ghost" size="icon" className="relative">
|
| 113 |
+
<ShoppingCart className="w-5 h-5" />
|
| 114 |
+
{cartItemsCount > 0 && (
|
| 115 |
+
<Badge className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 flex items-center justify-center">
|
| 116 |
+
{cartItemsCount}
|
| 117 |
+
</Badge>
|
| 118 |
+
)}
|
| 119 |
+
</Button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</header>
|
| 124 |
+
|
| 125 |
+
{/* Hero Section - Orange/Pink Gradient Theme */}
|
| 126 |
+
<section className="bg-gradient-to-r from-orange-500 to-pink-500 text-white py-16">
|
| 127 |
+
<div className="container mx-auto px-6">
|
| 128 |
+
<div className="max-w-3xl">
|
| 129 |
+
<h2 className="text-4xl font-bold mb-4">
|
| 130 |
+
Summer Collection
|
| 131 |
+
</h2>
|
| 132 |
+
<p className="text-xl mb-6 opacity-90">
|
| 133 |
+
Discover our latest products built with @hanzo/ui components
|
| 134 |
+
</p>
|
| 135 |
+
<Button size="lg" variant="secondary">
|
| 136 |
+
Shop Now
|
| 137 |
+
</Button>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</section>
|
| 141 |
+
|
| 142 |
+
{/* Filters Bar */}
|
| 143 |
+
<div className="border-b">
|
| 144 |
+
<div className="container mx-auto px-6 py-4">
|
| 145 |
+
<div className="flex items-center justify-between">
|
| 146 |
+
<div className="flex items-center gap-4">
|
| 147 |
+
<Button variant="outline" className="gap-2">
|
| 148 |
+
<Filter className="w-4 h-4" />
|
| 149 |
+
Filters
|
| 150 |
+
</Button>
|
| 151 |
+
<Select defaultValue="featured">
|
| 152 |
+
<SelectTrigger className="w-[180px]">
|
| 153 |
+
<SelectValue />
|
| 154 |
+
</SelectTrigger>
|
| 155 |
+
<SelectContent>
|
| 156 |
+
<SelectItem value="featured">Featured</SelectItem>
|
| 157 |
+
<SelectItem value="price-low">Price: Low to High</SelectItem>
|
| 158 |
+
<SelectItem value="price-high">Price: High to Low</SelectItem>
|
| 159 |
+
<SelectItem value="rating">Highest Rated</SelectItem>
|
| 160 |
+
<SelectItem value="newest">Newest</SelectItem>
|
| 161 |
+
</SelectContent>
|
| 162 |
+
</Select>
|
| 163 |
+
</div>
|
| 164 |
+
<p className="text-sm text-muted-foreground">
|
| 165 |
+
Showing {products.length} products
|
| 166 |
+
</p>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Products Grid */}
|
| 172 |
+
<section className="py-12">
|
| 173 |
+
<div className="container mx-auto px-6">
|
| 174 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 175 |
+
{products.map(product => (
|
| 176 |
+
<Card key={product.id} className="overflow-hidden group">
|
| 177 |
+
<div className="relative">
|
| 178 |
+
<AspectRatio ratio={1}>
|
| 179 |
+
<img
|
| 180 |
+
src={product.image}
|
| 181 |
+
alt={product.name}
|
| 182 |
+
className="object-cover w-full h-full group-hover:scale-105 transition-transform"
|
| 183 |
+
/>
|
| 184 |
+
</AspectRatio>
|
| 185 |
+
{product.badge && (
|
| 186 |
+
<Badge className="absolute top-2 left-2">
|
| 187 |
+
{product.badge}
|
| 188 |
+
</Badge>
|
| 189 |
+
)}
|
| 190 |
+
<div className="absolute top-2 right-2 flex flex-col gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 191 |
+
<Button size="icon" variant="secondary" className="h-8 w-8">
|
| 192 |
+
<Heart className="w-4 h-4" />
|
| 193 |
+
</Button>
|
| 194 |
+
<Button size="icon" variant="secondary" className="h-8 w-8">
|
| 195 |
+
<Share2 className="w-4 h-4" />
|
| 196 |
+
</Button>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<CardContent className="p-4">
|
| 201 |
+
<h3 className="font-semibold mb-2">{product.name}</h3>
|
| 202 |
+
<div className="flex items-center gap-2 mb-2">
|
| 203 |
+
<div className="flex items-center">
|
| 204 |
+
{[...Array(5)].map((_, i) => (
|
| 205 |
+
<Star
|
| 206 |
+
key={i}
|
| 207 |
+
className={`w-4 h-4 ${
|
| 208 |
+
i < Math.floor(product.rating)
|
| 209 |
+
? "fill-yellow-400 text-yellow-400"
|
| 210 |
+
: "text-muted-foreground"
|
| 211 |
+
}`}
|
| 212 |
+
/>
|
| 213 |
+
))}
|
| 214 |
+
</div>
|
| 215 |
+
<span className="text-sm text-muted-foreground">
|
| 216 |
+
({product.reviews})
|
| 217 |
+
</span>
|
| 218 |
+
</div>
|
| 219 |
+
<div className="flex items-center justify-between mb-3">
|
| 220 |
+
<span className="text-2xl font-bold">
|
| 221 |
+
${product.price}
|
| 222 |
+
</span>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
{/* Variant Selector */}
|
| 226 |
+
<div className="mb-3">
|
| 227 |
+
<Select defaultValue={product.variants[0]}>
|
| 228 |
+
<SelectTrigger className="w-full h-8 text-sm">
|
| 229 |
+
<SelectValue />
|
| 230 |
+
</SelectTrigger>
|
| 231 |
+
<SelectContent>
|
| 232 |
+
{product.variants.map(variant => (
|
| 233 |
+
<SelectItem key={variant} value={variant}>
|
| 234 |
+
{variant}
|
| 235 |
+
</SelectItem>
|
| 236 |
+
))}
|
| 237 |
+
</SelectContent>
|
| 238 |
+
</Select>
|
| 239 |
+
</div>
|
| 240 |
+
</CardContent>
|
| 241 |
+
|
| 242 |
+
<CardFooter className="p-4 pt-0">
|
| 243 |
+
<Button
|
| 244 |
+
className="w-full"
|
| 245 |
+
onClick={() => addToCart(product.id)}
|
| 246 |
+
>
|
| 247 |
+
<ShoppingCart className="w-4 h-4 mr-2" />
|
| 248 |
+
Add to Cart
|
| 249 |
+
</Button>
|
| 250 |
+
</CardFooter>
|
| 251 |
+
</Card>
|
| 252 |
+
))}
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
</section>
|
| 256 |
+
|
| 257 |
+
{/* Features */}
|
| 258 |
+
<section className="py-12 bg-muted">
|
| 259 |
+
<div className="container mx-auto px-6">
|
| 260 |
+
<div className="grid md:grid-cols-3 gap-8">
|
| 261 |
+
<div className="text-center">
|
| 262 |
+
<div className="w-12 h-12 rounded-full bg-orange-500/10 flex items-center justify-center mx-auto mb-3">
|
| 263 |
+
<ShoppingCart className="w-6 h-6 text-orange-500" />
|
| 264 |
+
</div>
|
| 265 |
+
<h3 className="font-semibold mb-1">Free Shipping</h3>
|
| 266 |
+
<p className="text-sm text-muted-foreground">
|
| 267 |
+
On orders over $100
|
| 268 |
+
</p>
|
| 269 |
+
</div>
|
| 270 |
+
<div className="text-center">
|
| 271 |
+
<div className="w-12 h-12 rounded-full bg-pink-500/10 flex items-center justify-center mx-auto mb-3">
|
| 272 |
+
<Star className="w-6 h-6 text-pink-500" />
|
| 273 |
+
</div>
|
| 274 |
+
<h3 className="font-semibold mb-1">Quality Products</h3>
|
| 275 |
+
<p className="text-sm text-muted-foreground">
|
| 276 |
+
100% authentic brands
|
| 277 |
+
</p>
|
| 278 |
+
</div>
|
| 279 |
+
<div className="text-center">
|
| 280 |
+
<div className="w-12 h-12 rounded-full bg-rose-500/10 flex items-center justify-center mx-auto mb-3">
|
| 281 |
+
<Heart className="w-6 h-6 text-rose-500" />
|
| 282 |
+
</div>
|
| 283 |
+
<h3 className="font-semibold mb-1">24/7 Support</h3>
|
| 284 |
+
<p className="text-sm text-muted-foreground">
|
| 285 |
+
Dedicated customer service
|
| 286 |
+
</p>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
</section>
|
| 291 |
+
</div>
|
| 292 |
+
);
|
| 293 |
+
}
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
module.exports = {
|
| 3 |
+
darkMode: ["class"],
|
| 4 |
+
content: [
|
| 5 |
+
'./pages/**/*.{ts,tsx}',
|
| 6 |
+
'./components/**/*.{ts,tsx}',
|
| 7 |
+
'./app/**/*.{ts,tsx}',
|
| 8 |
+
'./src/**/*.{ts,tsx}',
|
| 9 |
+
],
|
| 10 |
+
theme: {
|
| 11 |
+
container: {
|
| 12 |
+
center: true,
|
| 13 |
+
padding: "2rem",
|
| 14 |
+
screens: {
|
| 15 |
+
"2xl": "1400px",
|
| 16 |
+
},
|
| 17 |
+
},
|
| 18 |
+
extend: {
|
| 19 |
+
colors: {
|
| 20 |
+
border: "hsl(var(--border))",
|
| 21 |
+
input: "hsl(var(--input))",
|
| 22 |
+
ring: "hsl(var(--ring))",
|
| 23 |
+
background: "hsl(var(--background))",
|
| 24 |
+
foreground: "hsl(var(--foreground))",
|
| 25 |
+
primary: {
|
| 26 |
+
DEFAULT: "hsl(var(--primary))",
|
| 27 |
+
foreground: "hsl(var(--primary-foreground))",
|
| 28 |
+
},
|
| 29 |
+
secondary: {
|
| 30 |
+
DEFAULT: "hsl(var(--secondary))",
|
| 31 |
+
foreground: "hsl(var(--secondary-foreground))",
|
| 32 |
+
},
|
| 33 |
+
destructive: {
|
| 34 |
+
DEFAULT: "hsl(var(--destructive))",
|
| 35 |
+
foreground: "hsl(var(--destructive-foreground))",
|
| 36 |
+
},
|
| 37 |
+
muted: {
|
| 38 |
+
DEFAULT: "hsl(var(--muted))",
|
| 39 |
+
foreground: "hsl(var(--muted-foreground))",
|
| 40 |
+
},
|
| 41 |
+
accent: {
|
| 42 |
+
DEFAULT: "hsl(var(--accent))",
|
| 43 |
+
foreground: "hsl(var(--accent-foreground))",
|
| 44 |
+
},
|
| 45 |
+
popover: {
|
| 46 |
+
DEFAULT: "hsl(var(--popover))",
|
| 47 |
+
foreground: "hsl(var(--popover-foreground))",
|
| 48 |
+
},
|
| 49 |
+
card: {
|
| 50 |
+
DEFAULT: "hsl(var(--card))",
|
| 51 |
+
foreground: "hsl(var(--card-foreground))",
|
| 52 |
+
},
|
| 53 |
+
},
|
| 54 |
+
borderRadius: {
|
| 55 |
+
lg: "var(--radius)",
|
| 56 |
+
md: "calc(var(--radius) - 2px)",
|
| 57 |
+
sm: "calc(var(--radius) - 4px)",
|
| 58 |
+
},
|
| 59 |
+
keyframes: {
|
| 60 |
+
"accordion-down": {
|
| 61 |
+
from: { height: 0 },
|
| 62 |
+
to: { height: "var(--radix-accordion-content-height)" },
|
| 63 |
+
},
|
| 64 |
+
"accordion-up": {
|
| 65 |
+
from: { height: "var(--radix-accordion-content-height)" },
|
| 66 |
+
to: { height: 0 },
|
| 67 |
+
},
|
| 68 |
+
},
|
| 69 |
+
animation: {
|
| 70 |
+
"accordion-down": "accordion-down 0.2s ease-out",
|
| 71 |
+
"accordion-up": "accordion-up 0.2s ease-out",
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
},
|
| 75 |
+
plugins: [],
|
| 76 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "preserve",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 26 |
+
"exclude": ["node_modules"]
|
| 27 |
+
}
|