create-fsd-architecture 1.0.1 → 1.0.2
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/.claude/settings.local.json +9 -0
- package/README.md +84 -0
- package/ashraf/README.md +122 -0
- package/ashraf/bun.lock +1347 -0
- package/ashraf/components.json +23 -0
- package/ashraf/eslint.config.js +23 -0
- package/ashraf/index.html +13 -0
- package/ashraf/package-lock.json +9886 -0
- package/ashraf/package.json +43 -0
- package/ashraf/src/app/App.tsx +12 -0
- package/ashraf/src/app/providers/axios-interceptor.tsx +0 -0
- package/ashraf/src/app/providers.tsx +0 -0
- package/ashraf/src/app/routing/auth-routes.tsx +0 -0
- package/ashraf/src/app/routing/dashboard-routes.tsx +0 -0
- package/ashraf/src/app/routing/guards/auth-guard.tsx +0 -0
- package/ashraf/src/app/routing/guards/dashboard-guard.tsx +0 -0
- package/ashraf/src/app/styles/index.css +190 -0
- package/ashraf/src/assets/index.ts +0 -0
- package/ashraf/src/entities/products/api/get-product-by-id.ts +8 -0
- package/ashraf/src/entities/products/api/get-products.ts +8 -0
- package/ashraf/src/entities/products/index.ts +5 -0
- package/ashraf/src/entities/products/model/query-keys.ts +5 -0
- package/ashraf/src/entities/products/model/types.ts +18 -0
- package/ashraf/src/entities/products/ui/product-card.tsx +25 -0
- package/ashraf/src/features/products/create-product/api/create-product.ts +7 -0
- package/ashraf/src/features/products/create-product/index.ts +2 -0
- package/ashraf/src/features/products/create-product/model/create-product-schema.ts +11 -0
- package/ashraf/src/features/products/create-product/model/use-create-product.ts +15 -0
- package/ashraf/src/features/products/create-product/ui/create-product-form.tsx +85 -0
- package/ashraf/src/features/products/update-product/api/update-product.ts +12 -0
- package/ashraf/src/features/products/update-product/index.ts +2 -0
- package/ashraf/src/features/products/update-product/model/update-product.schema.ts +11 -0
- package/ashraf/src/features/products/update-product/model/use-update-product.ts +17 -0
- package/ashraf/src/features/products/update-product/ui/update-product-form.tsx +69 -0
- package/ashraf/src/main.tsx +10 -0
- package/ashraf/src/pages/products/all-products.tsx +23 -0
- package/ashraf/src/pages/products/create-product-page.tsx +10 -0
- package/ashraf/src/pages/products/update-product-page.tsx +24 -0
- package/ashraf/src/shared/config/env.ts +1 -0
- package/ashraf/src/shared/lib/utils.ts +6 -0
- package/ashraf/src/shared/types/types.d.ts +0 -0
- package/ashraf/src/shared/ui/input.tsx +21 -0
- package/ashraf/src/shared/ui/label.tsx +22 -0
- package/ashraf/src/widgets/dashboard-header.tsx +0 -0
- package/ashraf/src/widgets/index.ts +0 -0
- package/ashraf/src/widgets/sidebar.tsx +0 -0
- package/ashraf/tsconfig.app.json +32 -0
- package/ashraf/tsconfig.json +21 -0
- package/ashraf/tsconfig.node.json +26 -0
- package/ashraf/vite.config.ts +14 -0
- package/bin/index.mjs +180 -39
- package/package.json +3 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mudular-structure-v2",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
14
|
+
"@tanstack/react-query": "^5.90.21",
|
|
15
|
+
"axios": "^1.15.0",
|
|
16
|
+
"class-variance-authority": "^0.7.1",
|
|
17
|
+
"clsx": "^2.1.1",
|
|
18
|
+
"lucide-react": "^0.575.0",
|
|
19
|
+
"radix-ui": "^1.4.3",
|
|
20
|
+
"react": "^19.2.0",
|
|
21
|
+
"react-dom": "^19.2.0",
|
|
22
|
+
"react-hook-form": "^7.72.1",
|
|
23
|
+
"react-router": "^7.13.1",
|
|
24
|
+
"tailwind-merge": "^3.5.0",
|
|
25
|
+
"tailwindcss": "^4.2.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^9.39.1",
|
|
29
|
+
"@types/node": "^24.10.1",
|
|
30
|
+
"@types/react": "^19.2.7",
|
|
31
|
+
"@types/react-dom": "^19.2.3",
|
|
32
|
+
"@vitejs/plugin-react-swc": "^4.2.2",
|
|
33
|
+
"eslint": "^9.39.1",
|
|
34
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
35
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
36
|
+
"globals": "^16.5.0",
|
|
37
|
+
"shadcn": "^3.8.5",
|
|
38
|
+
"tw-animate-css": "^1.4.0",
|
|
39
|
+
"typescript": "~5.9.3",
|
|
40
|
+
"typescript-eslint": "^8.48.0",
|
|
41
|
+
"vite": "^7.3.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
9
|
+
line-height: 1.5;
|
|
10
|
+
font-weight: 400;
|
|
11
|
+
|
|
12
|
+
color-scheme: light dark;
|
|
13
|
+
color: rgba(255, 255, 255, 0.87);
|
|
14
|
+
background-color: #242424;
|
|
15
|
+
|
|
16
|
+
font-synthesis: none;
|
|
17
|
+
text-rendering: optimizeLegibility;
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
-moz-osx-font-smoothing: grayscale;
|
|
20
|
+
--radius: 0.625rem;
|
|
21
|
+
--background: oklch(1 0 0);
|
|
22
|
+
--foreground: oklch(0.145 0 0);
|
|
23
|
+
--card: oklch(1 0 0);
|
|
24
|
+
--card-foreground: oklch(0.145 0 0);
|
|
25
|
+
--popover: oklch(1 0 0);
|
|
26
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
27
|
+
--primary: oklch(0.205 0 0);
|
|
28
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
29
|
+
--secondary: oklch(0.97 0 0);
|
|
30
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
31
|
+
--muted: oklch(0.97 0 0);
|
|
32
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
33
|
+
--accent: oklch(0.97 0 0);
|
|
34
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
35
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
36
|
+
--border: oklch(0.922 0 0);
|
|
37
|
+
--input: oklch(0.922 0 0);
|
|
38
|
+
--ring: oklch(0.708 0 0);
|
|
39
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
40
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
41
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
42
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
43
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
44
|
+
--sidebar: oklch(0.985 0 0);
|
|
45
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
46
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
47
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
48
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
49
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
50
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
51
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
a {
|
|
55
|
+
font-weight: 500;
|
|
56
|
+
color: #646cff;
|
|
57
|
+
text-decoration: inherit;
|
|
58
|
+
}
|
|
59
|
+
a:hover {
|
|
60
|
+
color: #535bf2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
body {
|
|
64
|
+
margin: 0;
|
|
65
|
+
display: flex;
|
|
66
|
+
place-items: center;
|
|
67
|
+
min-width: 320px;
|
|
68
|
+
min-height: 100vh;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
h1 {
|
|
72
|
+
font-size: 3.2em;
|
|
73
|
+
line-height: 1.1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button {
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
border: 1px solid transparent;
|
|
79
|
+
padding: 0.6em 1.2em;
|
|
80
|
+
font-size: 1em;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
font-family: inherit;
|
|
83
|
+
background-color: #1a1a1a;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
transition: border-color 0.25s;
|
|
86
|
+
}
|
|
87
|
+
button:hover {
|
|
88
|
+
border-color: #646cff;
|
|
89
|
+
}
|
|
90
|
+
button:focus,
|
|
91
|
+
button:focus-visible {
|
|
92
|
+
outline: 4px auto -webkit-focus-ring-color;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (prefers-color-scheme: light) {
|
|
96
|
+
:root {
|
|
97
|
+
color: #213547;
|
|
98
|
+
background-color: #ffffff;
|
|
99
|
+
}
|
|
100
|
+
a:hover {
|
|
101
|
+
color: #747bff;
|
|
102
|
+
}
|
|
103
|
+
button {
|
|
104
|
+
background-color: #f9f9f9;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@theme inline {
|
|
109
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
110
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
111
|
+
--radius-lg: var(--radius);
|
|
112
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
113
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
114
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
115
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
116
|
+
--color-background: var(--background);
|
|
117
|
+
--color-foreground: var(--foreground);
|
|
118
|
+
--color-card: var(--card);
|
|
119
|
+
--color-card-foreground: var(--card-foreground);
|
|
120
|
+
--color-popover: var(--popover);
|
|
121
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
122
|
+
--color-primary: var(--primary);
|
|
123
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
124
|
+
--color-secondary: var(--secondary);
|
|
125
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
126
|
+
--color-muted: var(--muted);
|
|
127
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
128
|
+
--color-accent: var(--accent);
|
|
129
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
130
|
+
--color-destructive: var(--destructive);
|
|
131
|
+
--color-border: var(--border);
|
|
132
|
+
--color-input: var(--input);
|
|
133
|
+
--color-ring: var(--ring);
|
|
134
|
+
--color-chart-1: var(--chart-1);
|
|
135
|
+
--color-chart-2: var(--chart-2);
|
|
136
|
+
--color-chart-3: var(--chart-3);
|
|
137
|
+
--color-chart-4: var(--chart-4);
|
|
138
|
+
--color-chart-5: var(--chart-5);
|
|
139
|
+
--color-sidebar: var(--sidebar);
|
|
140
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
141
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
142
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
143
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
144
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
145
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
146
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.dark {
|
|
150
|
+
--background: oklch(0.145 0 0);
|
|
151
|
+
--foreground: oklch(0.985 0 0);
|
|
152
|
+
--card: oklch(0.205 0 0);
|
|
153
|
+
--card-foreground: oklch(0.985 0 0);
|
|
154
|
+
--popover: oklch(0.205 0 0);
|
|
155
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
156
|
+
--primary: oklch(0.922 0 0);
|
|
157
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
158
|
+
--secondary: oklch(0.269 0 0);
|
|
159
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
160
|
+
--muted: oklch(0.269 0 0);
|
|
161
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
162
|
+
--accent: oklch(0.269 0 0);
|
|
163
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
164
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
165
|
+
--border: oklch(1 0 0 / 10%);
|
|
166
|
+
--input: oklch(1 0 0 / 15%);
|
|
167
|
+
--ring: oklch(0.556 0 0);
|
|
168
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
169
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
170
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
171
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
172
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
173
|
+
--sidebar: oklch(0.205 0 0);
|
|
174
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
175
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
176
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
177
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
178
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
179
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
180
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@layer base {
|
|
184
|
+
* {
|
|
185
|
+
@apply border-border outline-ring/50;
|
|
186
|
+
}
|
|
187
|
+
body {
|
|
188
|
+
@apply bg-background text-foreground;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Product = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
price: number;
|
|
6
|
+
imageUrl: string;
|
|
7
|
+
stock: number;
|
|
8
|
+
createdAt?: string;
|
|
9
|
+
updatedAt?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ProductPayload = {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
price: number;
|
|
16
|
+
imageUrl: string;
|
|
17
|
+
stock: number;
|
|
18
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Product } from "../model/types";
|
|
2
|
+
|
|
3
|
+
type ProductCardProps = {
|
|
4
|
+
product: Product;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const ProductCard = ({ product }: ProductCardProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="rounded-2xl border p-4 shadow-sm">
|
|
10
|
+
<img
|
|
11
|
+
src={product.imageUrl}
|
|
12
|
+
alt={product.title}
|
|
13
|
+
className="mb-3 h-48 w-full rounded-xl object-cover"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<h3 className="text-lg font-bold">{product.title}</h3>
|
|
17
|
+
<p className="mt-2 text-sm text-gray-600">{product.description}</p>
|
|
18
|
+
|
|
19
|
+
<div className="mt-3 flex items-center justify-between">
|
|
20
|
+
<span className="font-semibold">{product.price} EGP</span>
|
|
21
|
+
<span className="text-sm text-gray-500">Stock: {product.stock}</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Product, ProductPayload } from "@/entities/products";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
export const createProduct = async (payload: ProductPayload): Promise<Product> => {
|
|
5
|
+
const response = await axios.post("/products", payload);
|
|
6
|
+
return response.data;
|
|
7
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const createProductSchema = z.object({
|
|
4
|
+
title: z.string().min(2, "Title is required"),
|
|
5
|
+
description: z.string().min(5, "Description is required"),
|
|
6
|
+
price: z.coerce.number().min(1, "Price must be greater than 0"),
|
|
7
|
+
imageUrl: z.string().url("Invalid image url"),
|
|
8
|
+
stock: z.coerce.number().min(0, "Stock cannot be negative"),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type CreateProductFormValues = z.infer<typeof createProductSchema>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { productQueryKeys } from "@/entities/products";
|
|
3
|
+
|
|
4
|
+
import { createProduct } from "../api/create-product";
|
|
5
|
+
|
|
6
|
+
export const useCreateProduct = () => {
|
|
7
|
+
const queryClient = useQueryClient();
|
|
8
|
+
|
|
9
|
+
return useMutation({
|
|
10
|
+
mutationFn: createProduct,
|
|
11
|
+
onSuccess: () => {
|
|
12
|
+
queryClient.invalidateQueries({ queryKey: productQueryKeys.all });
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useForm } from "react-hook-form";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
|
|
5
|
+
import { createProductSchema, type CreateProductFormValues } from "../model/create-product-schema";
|
|
6
|
+
import { useCreateProduct } from "../model/use-create-product";
|
|
7
|
+
|
|
8
|
+
export const CreateProductForm = () => {
|
|
9
|
+
const { mutate, isPending } = useCreateProduct();
|
|
10
|
+
|
|
11
|
+
const form = useForm<CreateProductFormValues>({
|
|
12
|
+
resolver: zodResolver(createProductSchema),
|
|
13
|
+
defaultValues: {
|
|
14
|
+
title: "",
|
|
15
|
+
description: "",
|
|
16
|
+
price: 0,
|
|
17
|
+
imageUrl: "",
|
|
18
|
+
stock: 0,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const onSubmit = (values: CreateProductFormValues) => {
|
|
23
|
+
mutate(values, {
|
|
24
|
+
onSuccess: () => {
|
|
25
|
+
form.reset();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<form
|
|
32
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
33
|
+
className="space-y-4 rounded-2xl border p-5"
|
|
34
|
+
>
|
|
35
|
+
<input
|
|
36
|
+
placeholder="Title"
|
|
37
|
+
className="w-full rounded-xl border p-3"
|
|
38
|
+
{...form.register("title")}
|
|
39
|
+
/>
|
|
40
|
+
<p className="text-sm text-red-500">{form.formState.errors.title?.message}</p>
|
|
41
|
+
|
|
42
|
+
<textarea
|
|
43
|
+
placeholder="Description"
|
|
44
|
+
className="w-full rounded-xl border p-3"
|
|
45
|
+
{...form.register("description")}
|
|
46
|
+
/>
|
|
47
|
+
<p className="text-sm text-red-500">
|
|
48
|
+
{form.formState.errors.description?.message}
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<input
|
|
52
|
+
type="number"
|
|
53
|
+
placeholder="Price"
|
|
54
|
+
className="w-full rounded-xl border p-3"
|
|
55
|
+
{...form.register("price")}
|
|
56
|
+
/>
|
|
57
|
+
<p className="text-sm text-red-500">{form.formState.errors.price?.message}</p>
|
|
58
|
+
|
|
59
|
+
<input
|
|
60
|
+
placeholder="Image URL"
|
|
61
|
+
className="w-full rounded-xl border p-3"
|
|
62
|
+
{...form.register("imageUrl")}
|
|
63
|
+
/>
|
|
64
|
+
<p className="text-sm text-red-500">
|
|
65
|
+
{form.formState.errors.imageUrl?.message}
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<input
|
|
69
|
+
type="number"
|
|
70
|
+
placeholder="Stock"
|
|
71
|
+
className="w-full rounded-xl border p-3"
|
|
72
|
+
{...form.register("stock")}
|
|
73
|
+
/>
|
|
74
|
+
<p className="text-sm text-red-500">{form.formState.errors.stock?.message}</p>
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="submit"
|
|
78
|
+
disabled={isPending}
|
|
79
|
+
className="rounded-xl bg-black px-4 py-2 text-white disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{isPending ? "Creating..." : "Create Product"}
|
|
82
|
+
</button>
|
|
83
|
+
</form>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Product, ProductPayload } from "@/entities/products";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
|
|
4
|
+
type UpdateProductParams = {
|
|
5
|
+
id: string;
|
|
6
|
+
payload: ProductPayload;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const updateProduct = async ({ id, payload }: UpdateProductParams): Promise<Product> => {
|
|
10
|
+
const response = await axios.put(`/products/${id}`, payload);
|
|
11
|
+
return response.data;
|
|
12
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const updateProductSchema = z.object({
|
|
4
|
+
title: z.string().min(2, "Title is required"),
|
|
5
|
+
description: z.string().min(5, "Description is required"),
|
|
6
|
+
price: z.coerce.number().min(1, "Price must be greater than 0"),
|
|
7
|
+
imageUrl: z.string().url("Invalid image url"),
|
|
8
|
+
stock: z.coerce.number().min(0, "Stock cannot be negative"),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type UpdateProductFormValues = z.infer<typeof updateProductSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { updateProduct } from "../api/update-product";
|
|
3
|
+
import { productQueryKeys } from "@/entities/products";
|
|
4
|
+
|
|
5
|
+
export const useUpdateProduct = () => {
|
|
6
|
+
const queryClient = useQueryClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutationFn: updateProduct,
|
|
10
|
+
onSuccess: (updatedProduct) => {
|
|
11
|
+
queryClient.invalidateQueries({ queryKey: productQueryKeys.all });
|
|
12
|
+
queryClient.invalidateQueries({
|
|
13
|
+
queryKey: productQueryKeys.detail(updatedProduct.id),
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useForm } from "react-hook-form";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import type { Product } from "@/entities/products";
|
|
4
|
+
import {
|
|
5
|
+
updateProductSchema,
|
|
6
|
+
type UpdateProductFormValues,
|
|
7
|
+
} from "../model/update-product.schema";
|
|
8
|
+
import { useUpdateProduct } from "../model/use-update-product";
|
|
9
|
+
|
|
10
|
+
type UpdateProductFormProps = {
|
|
11
|
+
product: Product;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const UpdateProductForm = ({ product }: UpdateProductFormProps) => {
|
|
15
|
+
const { mutate, isPending } = useUpdateProduct();
|
|
16
|
+
|
|
17
|
+
const form = useForm<UpdateProductFormValues>({
|
|
18
|
+
resolver: zodResolver(updateProductSchema),
|
|
19
|
+
defaultValues: {
|
|
20
|
+
title: product.title,
|
|
21
|
+
description: product.description,
|
|
22
|
+
price: product.price,
|
|
23
|
+
imageUrl: product.imageUrl,
|
|
24
|
+
stock: product.stock,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const onSubmit = (values: UpdateProductFormValues) => {
|
|
29
|
+
mutate({
|
|
30
|
+
id: product.id,
|
|
31
|
+
payload: values,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<form
|
|
37
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
38
|
+
className="space-y-4 rounded-2xl border p-5"
|
|
39
|
+
>
|
|
40
|
+
<input className="w-full rounded-xl border p-3" {...form.register("title")} />
|
|
41
|
+
<textarea
|
|
42
|
+
className="w-full rounded-xl border p-3"
|
|
43
|
+
{...form.register("description")}
|
|
44
|
+
/>
|
|
45
|
+
<input
|
|
46
|
+
type="number"
|
|
47
|
+
className="w-full rounded-xl border p-3"
|
|
48
|
+
{...form.register("price")}
|
|
49
|
+
/>
|
|
50
|
+
<input
|
|
51
|
+
className="w-full rounded-xl border p-3"
|
|
52
|
+
{...form.register("imageUrl")}
|
|
53
|
+
/>
|
|
54
|
+
<input
|
|
55
|
+
type="number"
|
|
56
|
+
className="w-full rounded-xl border p-3"
|
|
57
|
+
{...form.register("stock")}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<button
|
|
61
|
+
type="submit"
|
|
62
|
+
disabled={isPending}
|
|
63
|
+
className="rounded-xl bg-black px-4 py-2 text-white disabled:opacity-50"
|
|
64
|
+
>
|
|
65
|
+
{isPending ? "Updating..." : "Update Product"}
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { getProducts, productQueryKeys, ProductCard, type Product } from "@/entities/products";
|
|
3
|
+
|
|
4
|
+
export const AllProductsPage = () => {
|
|
5
|
+
const { data, isLoading, isError } = useQuery<Product[]>({
|
|
6
|
+
queryKey: productQueryKeys.lists(),
|
|
7
|
+
queryFn: getProducts,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
if (isLoading) return <p>Loading...</p>;
|
|
11
|
+
if (isError) return <p>Something went wrong</p>;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
15
|
+
{data?.map((product) => (
|
|
16
|
+
<div key={product.id} className="space-y-3">
|
|
17
|
+
<ProductCard product={product} />
|
|
18
|
+
{/* <DeleteProductButton productId={product.id} /> */}
|
|
19
|
+
</div>
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CreateProductForm } from "@/features/products/create-product";
|
|
2
|
+
|
|
3
|
+
export const CreateProductPage = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mx-auto max-w-2xl">
|
|
6
|
+
<h1 className="mb-4 text-2xl font-bold">Create Product</h1>
|
|
7
|
+
<CreateProductForm />
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { getProductById, productQueryKeys } from "@/entities/products";
|
|
3
|
+
import { UpdateProductForm } from "@/features/products/update-product";
|
|
4
|
+
|
|
5
|
+
type UpdateProductPageProps = {
|
|
6
|
+
productId: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const UpdateProductPage = ({ productId }: UpdateProductPageProps) => {
|
|
10
|
+
const { data, isLoading, isError } = useQuery({
|
|
11
|
+
queryKey: productQueryKeys.detail(productId),
|
|
12
|
+
queryFn: () => getProductById(productId),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (isLoading) return <p>Loading...</p>;
|
|
16
|
+
if (isError || !data) return <p>Product not found</p>;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="mx-auto max-w-2xl">
|
|
20
|
+
<h1 className="mb-4 text-2xl font-bold">Update Product</h1>
|
|
21
|
+
<UpdateProductForm product={data} />
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const BASE_URL = "http://localhost:3000";
|
|
File without changes
|