create-ecom-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +101 -0
- package/package.json +25 -0
- package/templates/ecommerce/app/about/page.tsx +152 -0
- package/templates/ecommerce/app/cart/page.tsx +103 -0
- package/templates/ecommerce/app/checkout/page.tsx +126 -0
- package/templates/ecommerce/app/contact/page.tsx +124 -0
- package/templates/ecommerce/app/globals.css +137 -0
- package/templates/ecommerce/app/layout.tsx +31 -0
- package/templates/ecommerce/app/login/page.tsx +224 -0
- package/templates/ecommerce/app/not-found.tsx +14 -0
- package/templates/ecommerce/app/orders/[id]/page.tsx +138 -0
- package/templates/ecommerce/app/page.tsx +215 -0
- package/templates/ecommerce/app/product/[id]/page.tsx +417 -0
- package/templates/ecommerce/app/products/page.tsx +244 -0
- package/templates/ecommerce/app/profile/page.tsx +273 -0
- package/templates/ecommerce/app/register/page.tsx +221 -0
- package/templates/ecommerce/app/wishlist/page.tsx +89 -0
- package/templates/ecommerce/components/CartItem.tsx +74 -0
- package/templates/ecommerce/components/CheckoutForm.tsx +109 -0
- package/templates/ecommerce/components/Filters.tsx +142 -0
- package/templates/ecommerce/components/Footer.tsx +100 -0
- package/templates/ecommerce/components/Header.tsx +194 -0
- package/templates/ecommerce/components/ProductCard.tsx +158 -0
- package/templates/ecommerce/components/SearchBar.tsx +41 -0
- package/templates/ecommerce/data/products.json +427 -0
- package/templates/ecommerce/lib/localStorage.ts +27 -0
- package/templates/ecommerce/lib/utils.ts +25 -0
- package/templates/ecommerce/next-env.d.ts +5 -0
- package/templates/ecommerce/next.config.js +13 -0
- package/templates/ecommerce/next.config.ts +14 -0
- package/templates/ecommerce/package.json +30 -0
- package/templates/ecommerce/postcss.config.js +7 -0
- package/templates/ecommerce/public/images/about-2.jpg +0 -0
- package/templates/ecommerce/public/images/contact-bg.jpg +0 -0
- package/templates/ecommerce/public/images/hero-bg.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-1.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-10.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-11.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-12.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-13.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-14.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-15.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-16.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-17.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-18.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-19.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-2.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-20.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-21.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-22.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-23.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-24.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-25.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-3.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-4.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-5.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-6.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-7.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-8.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-9.jpg +0 -0
- package/templates/ecommerce/public/service-worker.js +1 -0
- package/templates/ecommerce/store/useAuthStore.ts +56 -0
- package/templates/ecommerce/store/useCartStore.ts +63 -0
- package/templates/ecommerce/store/useOrderStore.ts +43 -0
- package/templates/ecommerce/store/useReviewStore.ts +60 -0
- package/templates/ecommerce/store/useWishlistStore.ts +43 -0
- package/templates/ecommerce/tailwind.config.ts +44 -0
- package/templates/ecommerce/tsconfig.json +22 -0
- package/templates/ecommerce/types/index.ts +69 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--font-inter: 'Inter', sans-serif;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html {
|
|
18
|
+
scroll-behavior: smooth;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
font-family: var(--font-inter), system-ui, sans-serif;
|
|
23
|
+
background-color: #f9fafb;
|
|
24
|
+
color: #111827;
|
|
25
|
+
-webkit-font-smoothing: antialiased;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@layer components {
|
|
29
|
+
.btn-primary {
|
|
30
|
+
@apply bg-gray-900 text-white px-5 py-2.5 rounded-lg font-medium
|
|
31
|
+
hover:bg-gray-700 active:scale-95 transition-all duration-200
|
|
32
|
+
disabled:opacity-50 disabled:cursor-not-allowed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.btn-secondary {
|
|
36
|
+
@apply border border-gray-300 text-gray-700 px-5 py-2.5 rounded-lg font-medium
|
|
37
|
+
hover:bg-gray-50 active:scale-95 transition-all duration-200;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.btn-danger {
|
|
41
|
+
@apply bg-red-500 text-white px-5 py-2.5 rounded-lg font-medium
|
|
42
|
+
hover:bg-red-600 active:scale-95 transition-all duration-200;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.card {
|
|
46
|
+
@apply bg-white rounded-2xl shadow-sm hover:shadow-lg transition-shadow duration-300;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.input {
|
|
50
|
+
@apply w-full px-4 py-2.5 border border-gray-200 rounded-lg text-sm
|
|
51
|
+
focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent
|
|
52
|
+
transition-all duration-200 bg-white;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.container-main {
|
|
56
|
+
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.badge {
|
|
60
|
+
@apply inline-flex items-center justify-center w-5 h-5 text-xs font-bold
|
|
61
|
+
bg-gray-900 text-white rounded-full;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.page-title {
|
|
65
|
+
@apply text-3xl font-bold text-gray-900 mb-2;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.section-title {
|
|
69
|
+
@apply text-2xl font-bold text-gray-900;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Scrollbar styling */
|
|
74
|
+
::-webkit-scrollbar {
|
|
75
|
+
width: 6px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
::-webkit-scrollbar-track {
|
|
79
|
+
background: #f1f1f1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
::-webkit-scrollbar-thumb {
|
|
83
|
+
background: #d1d5db;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
::-webkit-scrollbar-thumb:hover {
|
|
88
|
+
background: #9ca3af;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Star rating colors */
|
|
92
|
+
.star-filled {
|
|
93
|
+
color: #f59e0b;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.star-empty {
|
|
97
|
+
color: #d1d5db;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Floating bubble animations ── */
|
|
101
|
+
@keyframes bubble-float {
|
|
102
|
+
0% { transform: translateY(0px) scale(1); opacity: 0.7; }
|
|
103
|
+
50% { opacity: 0.5; }
|
|
104
|
+
100% { transform: translateY(-110vh) scale(0.85); opacity: 0; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@keyframes bubble-x-drift {
|
|
108
|
+
0% { margin-left: 0px; }
|
|
109
|
+
50% { margin-left: 18px; }
|
|
110
|
+
100% { margin-left: 0px; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.bubble {
|
|
114
|
+
position: absolute;
|
|
115
|
+
bottom: -10%;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
animation: bubble-float linear infinite, bubble-x-drift ease-in-out infinite;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@keyframes animate-fade-in {
|
|
122
|
+
from { opacity: 0; transform: translateY(16px); }
|
|
123
|
+
to { opacity: 1; transform: translateY(0); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.animate-fade-in {
|
|
127
|
+
animation: animate-fade-in 0.5s ease forwards;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@keyframes pulse-slow {
|
|
131
|
+
0%, 100% { opacity: 1; }
|
|
132
|
+
50% { opacity: 0.4; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.animate-pulse-slow {
|
|
136
|
+
animation: pulse-slow 2.5s ease-in-out infinite;
|
|
137
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
import Header from '@/components/Header';
|
|
4
|
+
import Footer from '@/components/Footer';
|
|
5
|
+
|
|
6
|
+
export const metadata: Metadata = {
|
|
7
|
+
title: {
|
|
8
|
+
template: '%s | GenZ Shop',
|
|
9
|
+
default: 'GenZ Shop — Shop Different. Shop Smarter.',
|
|
10
|
+
},
|
|
11
|
+
description:
|
|
12
|
+
'GenZ Shop — Curated fashion, electronics, and lifestyle products for the next generation. Shop trending styles at unbeatable prices.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<link
|
|
20
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
|
|
21
|
+
rel="stylesheet"
|
|
22
|
+
/>
|
|
23
|
+
</head>
|
|
24
|
+
<body className="min-h-screen flex flex-col bg-gray-50">
|
|
25
|
+
<Header />
|
|
26
|
+
<main className="flex-1">{children}</main>
|
|
27
|
+
<Footer />
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { Eye, EyeOff, Zap, ShoppingBag, ArrowRight, Mail, Lock } from 'lucide-react';
|
|
7
|
+
import { useAuthStore } from '@/store/useAuthStore';
|
|
8
|
+
|
|
9
|
+
// ⚠️ Defined outside to prevent focus loss
|
|
10
|
+
interface InputFieldProps {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
type: string;
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (v: string) => void;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
icon: React.ElementType;
|
|
18
|
+
suffix?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
function InputField({ id, label, type, value, onChange, placeholder, icon: Icon, suffix }: InputFieldProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1.5">{label}</label>
|
|
24
|
+
<div className="relative">
|
|
25
|
+
<div className="absolute inset-y-0 left-3.5 flex items-center pointer-events-none">
|
|
26
|
+
<Icon className="w-4 h-4 text-gray-400" />
|
|
27
|
+
</div>
|
|
28
|
+
<input
|
|
29
|
+
id={id}
|
|
30
|
+
type={type}
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={(e) => onChange(e.target.value)}
|
|
33
|
+
placeholder={placeholder}
|
|
34
|
+
className="input pl-10 pr-10"
|
|
35
|
+
required
|
|
36
|
+
/>
|
|
37
|
+
{suffix && (
|
|
38
|
+
<div className="absolute inset-y-0 right-3 flex items-center">{suffix}</div>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function LoginPage() {
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const { login } = useAuthStore();
|
|
48
|
+
const [email, setEmail] = useState('');
|
|
49
|
+
const [password, setPassword] = useState('');
|
|
50
|
+
const [showPass, setShowPass] = useState(false);
|
|
51
|
+
const [error, setError] = useState('');
|
|
52
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
53
|
+
|
|
54
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
setError('');
|
|
57
|
+
setIsLoading(true);
|
|
58
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
59
|
+
login(email, password);
|
|
60
|
+
const authed = useAuthStore.getState().isAuthenticated;
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
if (authed) {
|
|
63
|
+
router.push('/');
|
|
64
|
+
} else {
|
|
65
|
+
setError('Invalid email or password. Please try again.');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="min-h-screen flex">
|
|
71
|
+
{/* ── LEFT PANEL — gradient + brand ── */}
|
|
72
|
+
<div className="hidden lg:flex lg:w-1/2 text-white flex-col justify-between p-12 relative overflow-hidden" style={{ background: 'linear-gradient(160deg, #020617 0%, #0f172a 45%, #1e3a8a 100%)' }}>
|
|
73
|
+
|
|
74
|
+
{/* Blue glow spots */}
|
|
75
|
+
<div className="absolute inset-0 pointer-events-none" style={{
|
|
76
|
+
backgroundImage: `
|
|
77
|
+
radial-gradient(circle at 15% 70%, rgba(59,130,246,0.2) 0%, transparent 50%),
|
|
78
|
+
radial-gradient(circle at 85% 20%, rgba(96,165,250,0.15) 0%, transparent 45%),
|
|
79
|
+
radial-gradient(circle at 50% 95%, rgba(37,99,235,0.18) 0%, transparent 40%)
|
|
80
|
+
`,
|
|
81
|
+
}} />
|
|
82
|
+
|
|
83
|
+
{/* Floating Bubbles */}
|
|
84
|
+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
85
|
+
{[
|
|
86
|
+
{ w: 32, h: 32, left: '10%', dur: '8s', delay: '0s', color: 'rgba(59,130,246,0.15)', border: 'rgba(59,130,246,0.3)' },
|
|
87
|
+
{ w: 18, h: 18, left: '25%', dur: '6s', delay: '2s', color: 'rgba(96,165,250,0.15)', border: 'rgba(96,165,250,0.35)' },
|
|
88
|
+
{ w: 44, h: 44, left: '45%', dur: '10s', delay: '0.8s', color: 'rgba(37,99,235,0.1)', border: 'rgba(37,99,235,0.2)' },
|
|
89
|
+
{ w: 22, h: 22, left: '65%', dur: '7s', delay: '3.5s', color: 'rgba(59,130,246,0.15)', border: 'rgba(59,130,246,0.35)'},
|
|
90
|
+
{ w: 14, h: 14, left: '80%', dur: '5s', delay: '1s', color: 'rgba(96,165,250,0.2)', border: 'rgba(96,165,250,0.4)' },
|
|
91
|
+
{ w: 36, h: 36, left: '55%', dur: '9s', delay: '4.5s', color: 'rgba(59,130,246,0.1)', border: 'rgba(59,130,246,0.25)' },
|
|
92
|
+
].map((b, i) => (
|
|
93
|
+
<div
|
|
94
|
+
key={i}
|
|
95
|
+
className="bubble"
|
|
96
|
+
style={{
|
|
97
|
+
width: b.w,
|
|
98
|
+
height: b.h,
|
|
99
|
+
left: b.left,
|
|
100
|
+
background: b.color,
|
|
101
|
+
border: `1px solid ${b.border}`,
|
|
102
|
+
backdropFilter: 'blur(2px)',
|
|
103
|
+
animationDuration: `${b.dur}, ${parseFloat(b.dur) * 1.4}s`,
|
|
104
|
+
animationDelay: `${b.delay}, ${b.delay}`,
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Logo */}
|
|
111
|
+
<div className="relative flex items-center gap-2">
|
|
112
|
+
<div className="w-9 h-9 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #3b82f6, #2563eb)' }}>
|
|
113
|
+
<Zap className="w-5 h-5 text-white" />
|
|
114
|
+
</div>
|
|
115
|
+
<span className="text-xl font-black tracking-tight">GenZ Shop</span>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Center content */}
|
|
119
|
+
<div className="relative">
|
|
120
|
+
<div className="w-20 h-20 rounded-3xl flex items-center justify-center mb-8" style={{ background: 'rgba(59,130,246,0.15)', border: '1px solid rgba(59,130,246,0.3)' }}>
|
|
121
|
+
<ShoppingBag className="w-10 h-10 text-blue-400" />
|
|
122
|
+
</div>
|
|
123
|
+
<h2 className="text-4xl font-black leading-tight mb-4">
|
|
124
|
+
Your Style,<br />
|
|
125
|
+
<span className="text-transparent bg-clip-text" style={{ backgroundImage: 'linear-gradient(90deg, #93c5fd, #3b82f6, #60a5fa)' }}>
|
|
126
|
+
Your Rules.
|
|
127
|
+
</span>
|
|
128
|
+
</h2>
|
|
129
|
+
<p className="text-blue-100/80 leading-relaxed text-lg">
|
|
130
|
+
Discover trending fashion & lifestyle products curated for the GenZ generation.
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
{/* Trust badges */}
|
|
134
|
+
<div className="mt-10 grid grid-cols-3 gap-4">
|
|
135
|
+
{[{ val: '50K+', label: 'Customers' }, { val: '500+', label: 'Products' }, { val: '4.9★', label: 'Rating' }].map(({ val, label }) => (
|
|
136
|
+
<div key={label} className="rounded-2xl p-4 text-center" style={{ background: 'rgba(59,130,246,0.12)', border: '1px solid rgba(59,130,246,0.25)' }}>
|
|
137
|
+
<p className="text-xl font-black text-blue-300">{val}</p>
|
|
138
|
+
<p className="text-xs text-blue-200/60 mt-0.5">{label}</p>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<p className="relative text-blue-200/50 text-sm">© 2024 GenZ Shop</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
{/* ── RIGHT PANEL — form ── */}
|
|
149
|
+
<div className="w-full lg:w-1/2 flex flex-col items-center justify-center p-8 bg-white">
|
|
150
|
+
<div className="w-full max-w-md">
|
|
151
|
+
{/* Mobile logo */}
|
|
152
|
+
<div className="flex items-center gap-2 mb-10 lg:hidden">
|
|
153
|
+
<div className="w-8 h-8 bg-gray-900 rounded-lg flex items-center justify-center">
|
|
154
|
+
<Zap className="w-4 h-4 text-white" />
|
|
155
|
+
</div>
|
|
156
|
+
<span className="text-lg font-black text-gray-900">GenZ Shop</span>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div className="mb-8">
|
|
160
|
+
<h1 className="text-3xl font-black text-gray-900 mb-1">Welcome back!</h1>
|
|
161
|
+
<p className="text-gray-500">Sign in to your account to continue shopping</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
165
|
+
<InputField
|
|
166
|
+
id="login-email"
|
|
167
|
+
label="Email Address"
|
|
168
|
+
type="email"
|
|
169
|
+
value={email}
|
|
170
|
+
onChange={setEmail}
|
|
171
|
+
placeholder="you@example.com"
|
|
172
|
+
icon={Mail}
|
|
173
|
+
/>
|
|
174
|
+
<InputField
|
|
175
|
+
id="login-password"
|
|
176
|
+
label="Password"
|
|
177
|
+
type={showPass ? 'text' : 'password'}
|
|
178
|
+
value={password}
|
|
179
|
+
onChange={setPassword}
|
|
180
|
+
placeholder="Enter your password"
|
|
181
|
+
icon={Lock}
|
|
182
|
+
suffix={
|
|
183
|
+
<button type="button" onClick={() => setShowPass(!showPass)} className="text-gray-400 hover:text-gray-700 transition-colors">
|
|
184
|
+
{showPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
185
|
+
</button>
|
|
186
|
+
}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
{error && (
|
|
190
|
+
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3">
|
|
191
|
+
<p className="text-sm text-red-600">{error}</p>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
<button
|
|
196
|
+
type="submit"
|
|
197
|
+
disabled={isLoading}
|
|
198
|
+
className="w-full btn-primary py-3.5 text-base flex items-center justify-center gap-2"
|
|
199
|
+
id="login-submit-btn"
|
|
200
|
+
>
|
|
201
|
+
{isLoading ? (
|
|
202
|
+
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
203
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
204
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
205
|
+
</svg>
|
|
206
|
+
) : (
|
|
207
|
+
<>Sign In <ArrowRight className="w-4 h-4" /></>
|
|
208
|
+
)}
|
|
209
|
+
</button>
|
|
210
|
+
</form>
|
|
211
|
+
|
|
212
|
+
<div className="mt-6 text-center">
|
|
213
|
+
<p className="text-gray-500 text-sm">
|
|
214
|
+
Don't have an account?{' '}
|
|
215
|
+
<Link href="/register" className="font-bold text-gray-900 hover:underline">
|
|
216
|
+
Create Account
|
|
217
|
+
</Link>
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="container-main py-32 text-center animate-fade-in">
|
|
6
|
+
<p className="text-9xl font-bold text-gray-100">404</p>
|
|
7
|
+
<h1 className="text-3xl font-bold text-gray-900 mt-4 mb-3">Page not found</h1>
|
|
8
|
+
<p className="text-gray-500 mb-8">The page you're looking for doesn't exist.</p>
|
|
9
|
+
<Link href="/" className="btn-primary inline-block">
|
|
10
|
+
Back to Home
|
|
11
|
+
</Link>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { notFound } from 'next/navigation';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { motion } from 'framer-motion';
|
|
6
|
+
import { CheckCircle, Clock, Package, Truck, Home, ArrowLeft } from 'lucide-react';
|
|
7
|
+
import { useOrderStore } from '@/store/useOrderStore';
|
|
8
|
+
import { formatPrice } from '@/lib/utils';
|
|
9
|
+
import type { OrderStatus } from '@/types';
|
|
10
|
+
|
|
11
|
+
const STEPS: { status: OrderStatus; label: string; desc: string; icon: React.ElementType }[] = [
|
|
12
|
+
{ status: 'confirmed', label: 'Order Confirmed', desc: 'Your order has been placed successfully.', icon: CheckCircle },
|
|
13
|
+
{ status: 'processing', label: 'Processing', desc: 'We are preparing your items.', icon: Package },
|
|
14
|
+
{ status: 'shipped', label: 'Shipped', desc: 'Your order is on its way!', icon: Truck },
|
|
15
|
+
{ status: 'delivered', label: 'Delivered', desc: 'Enjoy your purchase!', icon: Home },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const STATUS_ORDER: OrderStatus[] = ['confirmed', 'processing', 'shipped', 'delivered'];
|
|
19
|
+
|
|
20
|
+
export default function OrderDetailPage({ params }: { params: { id: string } }) {
|
|
21
|
+
const { getOrder } = useOrderStore();
|
|
22
|
+
const order = getOrder(params.id);
|
|
23
|
+
|
|
24
|
+
if (!order) notFound();
|
|
25
|
+
|
|
26
|
+
const currentStep = STATUS_ORDER.indexOf(order.status);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<motion.div
|
|
30
|
+
className="container-main py-10 max-w-3xl"
|
|
31
|
+
initial={{ opacity: 0, y: 16 }}
|
|
32
|
+
animate={{ opacity: 1, y: 0 }}
|
|
33
|
+
transition={{ duration: 0.4 }}
|
|
34
|
+
>
|
|
35
|
+
<Link href="/profile?tab=orders" className="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-900 transition-colors mb-6">
|
|
36
|
+
<ArrowLeft className="w-4 h-4" /> Back to Orders
|
|
37
|
+
</Link>
|
|
38
|
+
|
|
39
|
+
<div className="flex items-center justify-between mb-6">
|
|
40
|
+
<div>
|
|
41
|
+
<h1 className="page-title mb-1">Order Details</h1>
|
|
42
|
+
<p className="font-mono text-gray-400 text-sm">{order.id}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<span className="text-xs font-semibold px-3 py-1.5 rounded-full bg-blue-100 text-blue-700 capitalize">
|
|
45
|
+
{order.status}
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Status Stepper */}
|
|
50
|
+
<div className="bg-white rounded-2xl shadow-sm p-6 mb-6">
|
|
51
|
+
<h2 className="font-bold text-gray-900 mb-6">Order Status</h2>
|
|
52
|
+
<div className="relative">
|
|
53
|
+
{/* Progress line */}
|
|
54
|
+
<div className="absolute top-5 left-5 right-5 h-0.5 bg-gray-100">
|
|
55
|
+
<motion.div
|
|
56
|
+
className="h-full bg-gray-900 rounded-full"
|
|
57
|
+
initial={{ width: 0 }}
|
|
58
|
+
animate={{ width: `${(currentStep / (STEPS.length - 1)) * 100}%` }}
|
|
59
|
+
transition={{ duration: 0.8, ease: 'easeOut' }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="relative flex justify-between">
|
|
64
|
+
{STEPS.map((step, i) => {
|
|
65
|
+
const Icon = step.icon;
|
|
66
|
+
const isDone = i <= currentStep;
|
|
67
|
+
return (
|
|
68
|
+
<div key={step.status} className="flex flex-col items-center max-w-[80px]">
|
|
69
|
+
<motion.div
|
|
70
|
+
initial={{ scale: 0.8 }}
|
|
71
|
+
animate={{ scale: 1 }}
|
|
72
|
+
transition={{ delay: i * 0.1 }}
|
|
73
|
+
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all z-10
|
|
74
|
+
${isDone ? 'bg-gray-900 border-gray-900' : 'bg-white border-gray-200'}`}
|
|
75
|
+
>
|
|
76
|
+
<Icon className={`w-4 h-4 ${isDone ? 'text-white' : 'text-gray-300'}`} />
|
|
77
|
+
</motion.div>
|
|
78
|
+
<p className={`text-xs font-semibold mt-2 text-center ${isDone ? 'text-gray-900' : 'text-gray-400'}`}>
|
|
79
|
+
{step.label}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<p className="text-sm text-gray-500 mt-6 text-center">
|
|
87
|
+
{STEPS[currentStep].desc}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Order Items */}
|
|
92
|
+
<div className="bg-white rounded-2xl shadow-sm p-6 mb-6">
|
|
93
|
+
<h2 className="font-bold text-gray-900 mb-4">Items Ordered</h2>
|
|
94
|
+
<div className="space-y-3">
|
|
95
|
+
{order.items.map((item) => (
|
|
96
|
+
<div key={item.product.id} className="flex justify-between items-center py-2 border-b border-gray-50 last:border-0">
|
|
97
|
+
<div>
|
|
98
|
+
<p className="font-medium text-gray-900 text-sm">{item.product.name}</p>
|
|
99
|
+
<p className="text-xs text-gray-400">{item.product.category} · Qty: {item.quantity}</p>
|
|
100
|
+
</div>
|
|
101
|
+
<p className="font-semibold text-gray-900">{formatPrice(item.product.price * item.quantity)}</p>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
<div className="border-t border-gray-100 pt-4 mt-4 space-y-2">
|
|
106
|
+
<div className="flex justify-between text-sm text-gray-600">
|
|
107
|
+
<span>Subtotal</span>
|
|
108
|
+
<span>{formatPrice(order.subtotal)}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex justify-between text-sm text-gray-600">
|
|
111
|
+
<span>Shipping</span>
|
|
112
|
+
<span className={order.shipping === 0 ? 'text-green-500 font-medium' : ''}>{order.shipping === 0 ? 'FREE' : formatPrice(order.shipping)}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex justify-between font-bold text-gray-900 pt-2 border-t border-gray-100">
|
|
115
|
+
<span>Total</span>
|
|
116
|
+
<span>{formatPrice(order.total)}</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Delivery Address */}
|
|
122
|
+
<div className="bg-white rounded-2xl shadow-sm p-6">
|
|
123
|
+
<h2 className="font-bold text-gray-900 mb-4">Delivery Address</h2>
|
|
124
|
+
<div className="text-sm text-gray-600 space-y-1">
|
|
125
|
+
<p className="font-semibold text-gray-900">{order.address.name}</p>
|
|
126
|
+
<p>{order.address.address}</p>
|
|
127
|
+
<p>{order.address.city}, {order.address.state} — {order.address.pincode}</p>
|
|
128
|
+
<p>{order.address.phone}</p>
|
|
129
|
+
<p>{order.address.email}</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="mt-6 flex justify-center">
|
|
134
|
+
<Link href="/products" className="btn-primary">Continue Shopping</Link>
|
|
135
|
+
</div>
|
|
136
|
+
</motion.div>
|
|
137
|
+
);
|
|
138
|
+
}
|