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,56 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import type { User } from '@/types';
|
|
4
|
+
import { generateId } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface AuthStore {
|
|
7
|
+
user: User | null;
|
|
8
|
+
isAuthenticated: boolean;
|
|
9
|
+
register: (name: string, email: string, password: string) => void;
|
|
10
|
+
login: (email: string, password: string) => void;
|
|
11
|
+
logout: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useAuthStore = create<AuthStore>()(
|
|
15
|
+
persist(
|
|
16
|
+
(set) => ({
|
|
17
|
+
user: null,
|
|
18
|
+
isAuthenticated: false,
|
|
19
|
+
|
|
20
|
+
register: (name, email, password) => {
|
|
21
|
+
const usersRaw =
|
|
22
|
+
typeof window !== 'undefined'
|
|
23
|
+
? localStorage.getItem('ecom-users')
|
|
24
|
+
: null;
|
|
25
|
+
const users: User[] = usersRaw ? JSON.parse(usersRaw) : [];
|
|
26
|
+
|
|
27
|
+
const exists = users.find((u) => u.email === email);
|
|
28
|
+
if (exists) throw new Error('Email already registered');
|
|
29
|
+
|
|
30
|
+
const newUser: User = { id: generateId(), name, email, password };
|
|
31
|
+
const updated = [...users, newUser];
|
|
32
|
+
|
|
33
|
+
localStorage.setItem('ecom-users', JSON.stringify(updated));
|
|
34
|
+
set({ user: newUser, isAuthenticated: true });
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
login: (email, password) => {
|
|
38
|
+
const usersRaw =
|
|
39
|
+
typeof window !== 'undefined'
|
|
40
|
+
? localStorage.getItem('ecom-users')
|
|
41
|
+
: null;
|
|
42
|
+
const users: User[] = usersRaw ? JSON.parse(usersRaw) : [];
|
|
43
|
+
|
|
44
|
+
const user = users.find(
|
|
45
|
+
(u) => u.email === email && u.password === password
|
|
46
|
+
);
|
|
47
|
+
if (!user) throw new Error('Invalid email or password');
|
|
48
|
+
|
|
49
|
+
set({ user, isAuthenticated: true });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
logout: () => set({ user: null, isAuthenticated: false }),
|
|
53
|
+
}),
|
|
54
|
+
{ name: 'auth-storage' }
|
|
55
|
+
)
|
|
56
|
+
);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import type { CartItem, Product } from '@/types';
|
|
4
|
+
|
|
5
|
+
interface CartStore {
|
|
6
|
+
items: CartItem[];
|
|
7
|
+
addItem: (product: Product) => void;
|
|
8
|
+
removeItem: (id: string) => void;
|
|
9
|
+
updateQuantity: (id: string, quantity: number) => void;
|
|
10
|
+
clearCart: () => void;
|
|
11
|
+
itemCount: () => number;
|
|
12
|
+
total: () => number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useCartStore = create<CartStore>()(
|
|
16
|
+
persist(
|
|
17
|
+
(set, get) => ({
|
|
18
|
+
items: [],
|
|
19
|
+
|
|
20
|
+
addItem: (product) => {
|
|
21
|
+
const existing = get().items.find((i) => i.product.id === product.id);
|
|
22
|
+
if (existing) {
|
|
23
|
+
set({
|
|
24
|
+
items: get().items.map((i) =>
|
|
25
|
+
i.product.id === product.id
|
|
26
|
+
? { ...i, quantity: i.quantity + 1 }
|
|
27
|
+
: i
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
set({ items: [...get().items, { product, quantity: 1 }] });
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
removeItem: (id) => {
|
|
36
|
+
set({ items: get().items.filter((i) => i.product.id !== id) });
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
updateQuantity: (id, quantity) => {
|
|
40
|
+
if (quantity <= 0) {
|
|
41
|
+
get().removeItem(id);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
set({
|
|
45
|
+
items: get().items.map((i) =>
|
|
46
|
+
i.product.id === id ? { ...i, quantity } : i
|
|
47
|
+
),
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
clearCart: () => set({ items: [] }),
|
|
52
|
+
|
|
53
|
+
itemCount: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
|
|
54
|
+
|
|
55
|
+
total: () =>
|
|
56
|
+
get().items.reduce(
|
|
57
|
+
(sum, i) => sum + i.product.price * i.quantity,
|
|
58
|
+
0
|
|
59
|
+
),
|
|
60
|
+
}),
|
|
61
|
+
{ name: 'cart-storage' }
|
|
62
|
+
)
|
|
63
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import type { CartItem, Address, Order, OrderStatus } from '@/types';
|
|
4
|
+
import { generateId } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface OrderStore {
|
|
7
|
+
orders: Order[];
|
|
8
|
+
addOrder: (data: {
|
|
9
|
+
items: CartItem[];
|
|
10
|
+
address: Address;
|
|
11
|
+
subtotal: number;
|
|
12
|
+
shipping: number;
|
|
13
|
+
total: number;
|
|
14
|
+
}) => Order;
|
|
15
|
+
getOrder: (id: string) => Order | undefined;
|
|
16
|
+
getUserOrders: (userEmail: string) => Order[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useOrderStore = create<OrderStore>()(
|
|
20
|
+
persist(
|
|
21
|
+
(set, get) => ({
|
|
22
|
+
orders: [],
|
|
23
|
+
|
|
24
|
+
addOrder: (data) => {
|
|
25
|
+
const statuses: OrderStatus[] = ['confirmed', 'processing', 'shipped', 'delivered'];
|
|
26
|
+
const order: Order = {
|
|
27
|
+
...data,
|
|
28
|
+
id: `ORD-${generateId().toUpperCase().slice(0, 8)}`,
|
|
29
|
+
status: statuses[0],
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
};
|
|
32
|
+
set({ orders: [order, ...get().orders] });
|
|
33
|
+
return order;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
getOrder: (id) => get().orders.find((o) => o.id === id),
|
|
37
|
+
|
|
38
|
+
getUserOrders: (userEmail) =>
|
|
39
|
+
get().orders.filter((o) => o.address.email === userEmail),
|
|
40
|
+
}),
|
|
41
|
+
{ name: 'orders-storage' }
|
|
42
|
+
)
|
|
43
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import type { Review } from '@/types';
|
|
4
|
+
import { generateId } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface ReviewStore {
|
|
7
|
+
reviews: Review[];
|
|
8
|
+
addReview: (data: Omit<Review, 'id' | 'date' | 'helpful' | 'helpfulBy'>) => void;
|
|
9
|
+
markHelpful: (reviewId: string, userId: string) => void;
|
|
10
|
+
getProductReviews: (productId: string) => Review[];
|
|
11
|
+
hasReviewed: (productId: string, userId: string) => boolean;
|
|
12
|
+
getAverageRating: (productId: string) => number;
|
|
13
|
+
getUserReviews: (userId: string) => Review[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useReviewStore = create<ReviewStore>()(
|
|
17
|
+
persist(
|
|
18
|
+
(set, get) => ({
|
|
19
|
+
reviews: [],
|
|
20
|
+
|
|
21
|
+
addReview: (data) => {
|
|
22
|
+
const review: Review = {
|
|
23
|
+
...data,
|
|
24
|
+
id: generateId(),
|
|
25
|
+
date: new Date().toISOString(),
|
|
26
|
+
helpful: 0,
|
|
27
|
+
helpfulBy: [],
|
|
28
|
+
};
|
|
29
|
+
set({ reviews: [review, ...get().reviews] });
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
markHelpful: (reviewId, userId) => {
|
|
33
|
+
set({
|
|
34
|
+
reviews: get().reviews.map((r) => {
|
|
35
|
+
if (r.id !== reviewId) return r;
|
|
36
|
+
const alreadyMarked = r.helpfulBy.includes(userId);
|
|
37
|
+
return alreadyMarked
|
|
38
|
+
? { ...r, helpful: r.helpful - 1, helpfulBy: r.helpfulBy.filter((id) => id !== userId) }
|
|
39
|
+
: { ...r, helpful: r.helpful + 1, helpfulBy: [...r.helpfulBy, userId] };
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getProductReviews: (productId) =>
|
|
45
|
+
get().reviews.filter((r) => r.productId === productId),
|
|
46
|
+
|
|
47
|
+
hasReviewed: (productId, userId) =>
|
|
48
|
+
get().reviews.some((r) => r.productId === productId && r.userId === userId),
|
|
49
|
+
|
|
50
|
+
getAverageRating: (productId) => {
|
|
51
|
+
const reviews = get().reviews.filter((r) => r.productId === productId);
|
|
52
|
+
if (reviews.length === 0) return 0;
|
|
53
|
+
return reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getUserReviews: (userId) => get().reviews.filter((r) => r.userId === userId),
|
|
57
|
+
}),
|
|
58
|
+
{ name: 'reviews-storage' }
|
|
59
|
+
)
|
|
60
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import type { Product } from '@/types';
|
|
4
|
+
|
|
5
|
+
interface WishlistStore {
|
|
6
|
+
items: Product[];
|
|
7
|
+
addItem: (product: Product) => void;
|
|
8
|
+
removeItem: (id: string) => void;
|
|
9
|
+
isWishlisted: (id: string) => boolean;
|
|
10
|
+
toggle: (product: Product) => void;
|
|
11
|
+
itemCount: () => number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useWishlistStore = create<WishlistStore>()(
|
|
15
|
+
persist(
|
|
16
|
+
(set, get) => ({
|
|
17
|
+
items: [],
|
|
18
|
+
|
|
19
|
+
addItem: (product) => {
|
|
20
|
+
if (!get().isWishlisted(product.id)) {
|
|
21
|
+
set({ items: [...get().items, product] });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
removeItem: (id) => {
|
|
26
|
+
set({ items: get().items.filter((p) => p.id !== id) });
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
isWishlisted: (id) => get().items.some((p) => p.id === id),
|
|
30
|
+
|
|
31
|
+
toggle: (product) => {
|
|
32
|
+
if (get().isWishlisted(product.id)) {
|
|
33
|
+
get().removeItem(product.id);
|
|
34
|
+
} else {
|
|
35
|
+
get().addItem(product);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
itemCount: () => get().items.length,
|
|
40
|
+
}),
|
|
41
|
+
{ name: 'wishlist-storage' }
|
|
42
|
+
)
|
|
43
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
content: [
|
|
5
|
+
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
6
|
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
7
|
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
8
|
+
],
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
colors: {
|
|
12
|
+
brand: {
|
|
13
|
+
50: '#fdf4ff',
|
|
14
|
+
100: '#fae8ff',
|
|
15
|
+
500: '#a855f7',
|
|
16
|
+
600: '#9333ea',
|
|
17
|
+
700: '#7e22ce',
|
|
18
|
+
900: '#581c87',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
fontFamily: {
|
|
22
|
+
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
|
|
23
|
+
},
|
|
24
|
+
animation: {
|
|
25
|
+
'fade-in': 'fadeIn 0.4s ease-out',
|
|
26
|
+
'slide-up': 'slideUp 0.4s ease-out',
|
|
27
|
+
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
28
|
+
},
|
|
29
|
+
keyframes: {
|
|
30
|
+
fadeIn: {
|
|
31
|
+
'0%': { opacity: '0' },
|
|
32
|
+
'100%': { opacity: '1' },
|
|
33
|
+
},
|
|
34
|
+
slideUp: {
|
|
35
|
+
'0%': { opacity: '0', transform: 'translateY(20px)' },
|
|
36
|
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
plugins: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default config;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [{ "name": "next" }],
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["./*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type Product = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
price: number;
|
|
6
|
+
originalPrice?: number;
|
|
7
|
+
category: string;
|
|
8
|
+
image: string;
|
|
9
|
+
rating: number;
|
|
10
|
+
reviewCount: number;
|
|
11
|
+
inStock: boolean;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type User = {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
password: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CartItem = {
|
|
23
|
+
product: Product;
|
|
24
|
+
quantity: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type Address = {
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
phone: string;
|
|
31
|
+
address: string;
|
|
32
|
+
city: string;
|
|
33
|
+
state: string;
|
|
34
|
+
pincode: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type OrderStatus = 'confirmed' | 'processing' | 'shipped' | 'delivered';
|
|
38
|
+
|
|
39
|
+
export type Order = {
|
|
40
|
+
id: string;
|
|
41
|
+
items: CartItem[];
|
|
42
|
+
address: Address;
|
|
43
|
+
subtotal: number;
|
|
44
|
+
shipping: number;
|
|
45
|
+
total: number;
|
|
46
|
+
status: OrderStatus;
|
|
47
|
+
createdAt: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type Review = {
|
|
51
|
+
id: string;
|
|
52
|
+
productId: string;
|
|
53
|
+
userId: string;
|
|
54
|
+
userName: string;
|
|
55
|
+
rating: number;
|
|
56
|
+
title: string;
|
|
57
|
+
body: string;
|
|
58
|
+
date: string;
|
|
59
|
+
helpful: number;
|
|
60
|
+
helpfulBy: string[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type FilterState = {
|
|
64
|
+
search: string;
|
|
65
|
+
category: string;
|
|
66
|
+
minPrice: number;
|
|
67
|
+
maxPrice: number;
|
|
68
|
+
minRating: number;
|
|
69
|
+
};
|