Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 229 additions & 2 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ app.get('/api/health', async (req: Request, res: Response) => {
}
});

// Zod schemas
// Zod schemas for Auth
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
Expand Down Expand Up @@ -73,7 +73,8 @@ app.post('/api/auth/register', async (req: Request, res: Response) => {
},
});

res.status(201).json({ id: user.id, email: user.email, name: user.name });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET || 'fallback_secret', { expiresIn: '24h' });
res.status(201).json({ token, user: { id: user.id, email: user.email, name: user.name } });
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({ errors: err.errors });
Expand Down Expand Up @@ -120,4 +121,230 @@ app.get('/api/auth/me', authenticate as express.RequestHandler, async (req: Auth
}
});

// 5. Products: List & Filter
app.get('/api/products', async (req: Request, res: Response) => {
try {
const { cat, sort, q } = req.query;

const whereClause: any = {};
if (cat) {
whereClause.category = String(cat);
}
if (q) {
whereClause.OR = [
{ name: { contains: String(q), mode: 'insensitive' } },
{ brand: { contains: String(q), mode: 'insensitive' } },
{ description: { contains: String(q), mode: 'insensitive' } }
];
}

let orderBy: any = {};
if (sort === 'price-low') {
orderBy = { price: 'asc' };
} else if (sort === 'price-high') {
orderBy = { price: 'desc' };
} else if (sort === 'new') {
orderBy = { id: 'desc' };
} else if (sort === 'rating') {
orderBy = { rating: 'desc' };
} else {
orderBy = { id: 'asc' };
}

const products = await prisma.product.findMany({
where: whereClause,
orderBy: orderBy,
});

res.json(products);
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});

// 6. Products: Get by ID
app.get('/api/products/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid product ID' });
}

const product = await prisma.product.findUnique({
where: { id },
include: {
reviews: {
orderBy: { createdAt: 'desc' }
}
}
});

if (!product) {
return res.status(404).json({ error: 'Product not found' });
}

res.json(product);
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});

// Zod schemas for order & review
const reviewSchema = z.object({
rating: z.number().min(1).max(5),
comment: z.string().min(3),
});

const orderItemSchema = z.object({
productId: z.number(),
quantity: z.number().min(1),
});

const orderSchema = z.object({
items: z.array(orderItemSchema),
paymentMethod: z.string(),
});

// 7. Products: Submit Review (Protected)
app.post('/api/products/:id/reviews', authenticate as express.RequestHandler, async (req: AuthRequest, res: Response) => {
try {
const productId = parseInt(req.params.id);
if (isNaN(productId)) {
return res.status(400).json({ error: 'Invalid product ID' });
}

const body = reviewSchema.parse(req.body);

const user = await prisma.user.findUnique({ where: { id: req.userId } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}

const product = await prisma.product.findUnique({ where: { id: productId } });
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}

// Create review
const review = await prisma.review.create({
data: {
productId,
userId: user.id,
userName: user.name || 'Anonymous Connoisseur',
rating: body.rating,
comment: body.comment,
}
});

// Recalculate average rating
const allReviews = await prisma.review.findMany({ where: { productId } });
const avgRating = allReviews.reduce((sum, r) => sum + r.rating, 0) / allReviews.length;

await prisma.product.update({
where: { id: productId },
data: {
rating: parseFloat(avgRating.toFixed(1)),
reviewsCount: allReviews.length,
}
});

res.status(201).json(review);
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({ errors: err.errors });
}
res.status(500).json({ error: (err as Error).message });
}
});

// 8. Orders: Place Order
app.post('/api/orders', async (req: Request, res: Response) => {
try {
const body = orderSchema.parse(req.body);

// Optional auth token retrieval
let userId: string | undefined;
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret') as { userId: string };
userId = decoded.userId;
} catch (e) {
// Continue as guest
}
}

// Fetch and check all products
let totalAmount = 0;
const lineItems: { productId: number; quantity: number; price: number }[] = [];

for (const item of body.items) {
const product = await prisma.product.findUnique({ where: { id: item.productId } });
if (!product) {
return res.status(404).json({ error: `Product with ID ${item.productId} not found` });
}
totalAmount += product.price * item.quantity;
lineItems.push({
productId: product.id,
quantity: item.quantity,
price: product.price,
});
}

// Create order inside transaction
const order = await prisma.$transaction(async (tx) => {
const ord = await tx.order.create({
data: {
userId: userId || null,
totalAmount,
paymentMethod: body.paymentMethod,
status: 'PAID', // Monolith mock completes payment immediately
}
});

for (const line of lineItems) {
await tx.orderItem.create({
data: {
orderId: ord.id,
productId: line.productId,
quantity: line.quantity,
price: line.price,
}
});
}

return ord;
});

res.status(201).json(order);
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({ errors: err.errors });
}
res.status(500).json({ error: (err as Error).message });
}
});

// 9. Orders: Get History (Protected)
app.get('/api/orders', authenticate as express.RequestHandler, async (req: AuthRequest, res: Response) => {
try {
const orders = await prisma.order.findMany({
where: { userId: req.userId },
include: {
items: {
include: {
product: true
}
}
},
orderBy: { createdAt: 'desc' }
});

res.json(orders);
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});

export default app;
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,8 @@
"overrides": {
"vite": "8.1.0"
}
},
"prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts"
}
}
60 changes: 60 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,66 @@ model User {
email String @unique
password String
name String?
role String @default("USER")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reviews Review[]
orders Order[]
}

model Product {
id Int @id @default(autoincrement())
brand String
name String
price Float
oldPrice Float
discount Int
rating Float @default(0)
reviewsCount Int @default(0)
badge String?
stock String
premium Boolean @default(false)
emoji String
category String
description String
features String[]
specs Json
colors String[]
tags String[]
reviews Review[]
orderItems OrderItem[]
}

model Review {
id String @id @default(uuid())
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userName String
rating Int
comment String
createdAt DateTime @default(now())
}

model Order {
id String @id @default(uuid())
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
totalAmount Float
status String @default("PENDING")
paymentMethod String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
}

model OrderItem {
id String @id @default(uuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
quantity Int
price Float
}
46 changes: 46 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { PrismaClient } from '@prisma/client';
import { ALL_PRODUCTS } from '../src/app/data/products.ts';

const prisma = new PrismaClient();

async function main() {
console.log('Clearing existing products...');
await prisma.product.deleteMany({});

console.log('Seeding products from frontend dataset...');
for (const product of ALL_PRODUCTS) {
await prisma.product.create({
data: {
id: product.id,
brand: product.brand,
name: product.name,
price: product.price,
oldPrice: product.oldPrice,
discount: product.discount,
rating: product.rating,
reviewsCount: product.reviews,
badge: product.badge,
stock: product.stock,
premium: product.premium,
emoji: product.emoji,
category: product.category,
description: product.description,
features: product.features,
specs: product.specs,
colors: product.colors,
tags: product.tags,
},
});
}

console.log('Seeding completed successfully.');
}

main()
.catch((e) => {
console.error('Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Loading