Skip to content

Commit 1400f2a

Browse files
Merge pull request #1 from Repith/testing-workspace
Description & In Stock flow
2 parents 03ddf28 + bd383c0 commit 1400f2a

File tree

26 files changed

+791
-154
lines changed

26 files changed

+791
-154
lines changed

TODO.txt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
5. + on dashboard change inStock as sum of products in stock
77

88
CHANGES:
9-
Reseeded Prisma DB with new data
109
[ADMIN]
11-
- added switch from shadcn/ui
12-
- fixed theme changer
13-
- fixed toast provider
10+
- created description in prisma db
1411
[STORE]
15-
- created cart-dialog | summary-dialog | cart-item-dialog
16-
- fixed bug with disabled add-to-cart
12+
- created description accordion in info components
13+
- added accordion | table from shadcn/ui
14+
- minor bug fixes
15+
- created size-table

ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/[productId]/components/product-form.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const formSchema = z.object({
4343
colorId: z.string().min(1),
4444
isFeatured: z.boolean().default(false).optional(),
4545
isArchived: z.boolean().default(false).optional(),
46+
description: z.string().min(1),
47+
inStock: z.coerce.number().min(1),
4648
});
4749

4850
type ProductFormValues = z.infer<typeof formSchema>;
@@ -71,7 +73,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({
7173
const [loading, setLoading] = useState(false);
7274

7375
const title = initialData ? "Edit product" : "Create product";
74-
const description = initialData ? "Edit product" : "Add a new product";
76+
const subTitle = initialData ? "Edit product" : "Add a new product";
7577
const toastMessage = initialData ? "Product updated." : "Product created.";
7678
const action = initialData ? "Save changes" : "Create";
7779

@@ -81,6 +83,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({
8183
? {
8284
...initialData,
8385
price: parseFloat(String(initialData?.price)),
86+
inStock: parseFloat(String(initialData?.inStock)),
8487
}
8588
: {
8689
name: "",
@@ -91,6 +94,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({
9194
colorId: "",
9295
isFeatured: false,
9396
isArchived: false,
97+
description: "",
98+
inStock: 0,
9499
},
95100
});
96101

@@ -138,8 +143,10 @@ export const ProductForm: React.FC<ProductFormProps> = ({
138143
onConfirm={onDelete}
139144
loading={loading}
140145
/>
146+
147+
{/* Product delete */}
141148
<div className="flex items-center justify-between">
142-
<Heading title={title} description={description}></Heading>
149+
<Heading title={title} description={subTitle}></Heading>
143150
{initialData && (
144151
<Button
145152
variant="destructive"
@@ -157,6 +164,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({
157164
onSubmit={form.handleSubmit(onSubmit)}
158165
className="w-full space-y-8"
159166
>
167+
{/* Image(s) upload */}
160168
<FormField
161169
control={form.control}
162170
name="images"
@@ -182,6 +190,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({
182190
)}
183191
/>
184192
<div className="grid grid-cols-3 gap-8">
193+
{/* Name */}
185194
<FormField
186195
control={form.control}
187196
name="name"
@@ -199,6 +208,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({
199208
</FormItem>
200209
)}
201210
/>
211+
212+
{/* Price */}
202213
<FormField
203214
control={form.control}
204215
name="price"
@@ -212,6 +223,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({
212223
</FormItem>
213224
)}
214225
/>
226+
227+
{/* Category */}
215228
<FormField
216229
control={form.control}
217230
name="categoryId"
@@ -244,6 +257,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({
244257
</FormItem>
245258
)}
246259
/>
260+
261+
{/* Size */}
247262
<FormField
248263
control={form.control}
249264
name="sizeId"
@@ -276,6 +291,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({
276291
</FormItem>
277292
)}
278293
/>
294+
295+
{/* Color */}
279296
<FormField
280297
control={form.control}
281298
name="colorId"
@@ -314,6 +331,45 @@ export const ProductForm: React.FC<ProductFormProps> = ({
314331
</FormItem>
315332
)}
316333
/>
334+
{/* Description */}
335+
<FormField
336+
control={form.control}
337+
name="description"
338+
render={({ field }) => (
339+
<FormItem>
340+
<FormLabel>Description</FormLabel>
341+
<FormControl>
342+
<Input
343+
disabled={loading}
344+
placeholder="Product description"
345+
{...field}
346+
/>
347+
</FormControl>
348+
<FormMessage />
349+
</FormItem>
350+
)}
351+
/>
352+
353+
{/* In stock */}
354+
<FormField
355+
control={form.control}
356+
name="inStock"
357+
render={({ field }) => (
358+
<FormItem>
359+
<FormLabel>Description</FormLabel>
360+
<FormControl>
361+
<Input
362+
disabled={loading}
363+
placeholder="In stock"
364+
{...field}
365+
/>
366+
</FormControl>
367+
<FormMessage />
368+
</FormItem>
369+
)}
370+
/>
371+
372+
{/* Is Featured | Is Archived */}
317373
<FormField
318374
control={form.control}
319375
name="isFeatured"

ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/components/columns.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type ProductColumn = {
1414
isFeatured: boolean;
1515
isArchived: boolean;
1616
createdAt: string;
17+
inStock: number;
1718
};
1819

1920
export const columns: ColumnDef<ProductColumn>[] = [
@@ -54,7 +55,10 @@ export const columns: ColumnDef<ProductColumn>[] = [
5455
</div>
5556
),
5657
},
57-
58+
{
59+
accessorKey: "inStock",
60+
header: "In stock",
61+
},
5862
{
5963
accessorKey: "createdAt",
6064
header: "Date",

ecommerce-admin/app/(dashboard)/[storeId]/(routes)/products/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const ProductsPage = async ({ params }: { params: { storeId: string } }) => {
3030
category: item.category.name,
3131
size: item.size.value,
3232
color: item.color.value,
33+
description: item.description,
34+
inStock: item.inStock,
3335
createdAt: format(item.createdAt, "MMMM do, yyyy"),
3436
}));
3537

Lines changed: 62 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
import Stripe from "stripe";
22
import { NextResponse } from "next/server";
3+
import { PrismaClient } from "@prisma/client";
34

4-
import { stripe } from "@/lib/stripe";
5-
import prismadb from "@/lib/prismadb";
5+
const prisma = new PrismaClient();
66

77
const corsHeaders = {
88
"Access-Control-Allow-Origin": "*",
99
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1010
"Access-Control-Allow-Headers": "Content-Type, Authorization",
1111
};
1212

13-
export async function OPTIONS() {
14-
return NextResponse.json({}, { headers: corsHeaders });
15-
}
16-
1713
export async function POST(
1814
req: Request,
1915
{ params }: { params: { storeId: string } }
2016
) {
2117
const { productIds } = await req.json();
2218

23-
if (!productIds || productIds.length === 0) {
24-
return new NextResponse("Product ids are required", { status: 400 });
25-
}
19+
const quantities = productIds.map((item) => item.quantity);
2620

27-
const products = await prismadb.product.findMany({
21+
const products = await prisma.product.findMany({
2822
where: {
2923
id: {
30-
in: productIds,
24+
in: productIds.map((item) => item.productId),
3125
},
3226
},
3327
});
3428

3529
const line_items: Stripe.Checkout.SessionCreateParams.LineItem[] = [];
3630

37-
products.forEach((product) => {
31+
products.forEach((product, index) => {
32+
const quantity = quantities[index];
33+
if (quantity <= 0) {
34+
throw new Error(`Invalid quantity for product "${product.name}"`);
35+
}
36+
3837
line_items.push({
39-
quantity: 1,
38+
quantity,
4039
price_data: {
4140
currency: "USD",
4241
product_data: {
@@ -47,21 +46,56 @@ export async function POST(
4746
});
4847
});
4948

50-
const order = await prismadb.order.create({
51-
data: {
52-
storeId: params.storeId,
53-
isPaid: false,
54-
orderItems: {
55-
create: productIds.map((productId: string) => ({
56-
product: {
57-
connect: {
58-
id: productId,
59-
},
49+
let order;
50+
try {
51+
await prisma.$transaction(async (prisma) => {
52+
order = await prisma.order.create({
53+
data: {
54+
storeId: params.storeId,
55+
isPaid: false,
56+
orderItems: {
57+
create: productIds.map((item) => ({
58+
product: {
59+
connect: {
60+
id: item.productId,
61+
},
62+
},
63+
quantity: item.quantity,
64+
})),
6065
},
61-
})),
62-
},
63-
},
64-
});
66+
},
67+
});
68+
69+
const productIdsToUpdate = productIds.map((item) => item.productId);
70+
const productsToUpdate = await prisma.product.findMany({
71+
where: {
72+
id: {
73+
in: productIdsToUpdate,
74+
},
75+
},
76+
});
77+
78+
for (const productToUpdate of productsToUpdate) {
79+
const orderedQuantity =
80+
productIds.find((item) => item.productId === productToUpdate.id)
81+
?.quantity || 0;
82+
83+
await prisma.product.update({
84+
where: {
85+
id: productToUpdate.id,
86+
},
87+
data: {
88+
isArchived: true,
89+
inStock: productToUpdate.inStock - orderedQuantity,
90+
},
91+
});
92+
}
93+
});
94+
} catch (error) {
95+
return new NextResponse(`Error processing order: ${error.message}`, {
96+
status: 500,
97+
});
98+
}
6599

66100
const session = await stripe.checkout.sessions.create({
67101
line_items,
@@ -77,10 +111,5 @@ export async function POST(
77111
},
78112
});
79113

80-
return NextResponse.json(
81-
{ url: session.url },
82-
{
83-
headers: corsHeaders,
84-
}
85-
);
114+
return NextResponse.json({ url: session.url }, { headers: corsHeaders });
86115
}

ecommerce-admin/app/api/[storeId]/products/[productId]/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export async function PATCH(
4949
sizeId,
5050
isFeatured,
5151
isArchived,
52+
description,
53+
inStock
5254
} = body;
5355

5456
if (!userId) {
@@ -83,6 +85,14 @@ export async function PATCH(
8385
return new NextResponse("Size id is required", { status: 400 });
8486
}
8587

88+
if (!description) {
89+
return new NextResponse("Description is required", { status: 400 });
90+
}
91+
92+
if (!inStock) {
93+
return new NextResponse("Need at least 1 item in stock", { status: 400 });
94+
}
95+
8696
const storeByUserId = await prismadb.store.findFirst({
8797
where: {
8898
id: params.storeId,
@@ -104,6 +114,8 @@ export async function PATCH(
104114
categoryId,
105115
colorId,
106116
sizeId,
117+
description,
118+
inStock,
107119
images: {
108120
deleteMany: {},
109121
},

ecommerce-admin/app/api/[storeId]/products/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export async function POST(
2121
images,
2222
isFeatured,
2323
isArchived,
24+
description,
25+
inStock
2426
} = body;
2527

2628
if (!userId) {
@@ -51,6 +53,14 @@ export async function POST(
5153
return new NextResponse("Size id is required", { status: 400 });
5254
}
5355

56+
if (!description) {
57+
return new NextResponse("Description is required", { status: 400 });
58+
}
59+
60+
if (!inStock) {
61+
return new NextResponse("Need at least 1 item in stock", { status: 400 });
62+
}
63+
5464
if (!params.storeId) {
5565
return new NextResponse("Store id is required", { status: 400 });
5666
}
@@ -76,6 +86,8 @@ export async function POST(
7686
colorId,
7787
sizeId,
7888
storeId: params.storeId,
89+
description,
90+
inStock,
7991
images: {
8092
createMany: {
8193
data: [...images.map((image: { url: string }) => image)],

0 commit comments

Comments
 (0)