Skip to content
Draft
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
2 changes: 2 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import orderRoutes from './routes/order';
import branchRoutes from './routes/branch';
import headquartersRoutes from './routes/headquarters';
import supplierRoutes from './routes/supplier';
import wishlistRoutes from './routes/wishlist';

const app = express();
const port = process.env.PORT || 3000;
Expand Down Expand Up @@ -74,6 +75,7 @@ app.use('/api/orders', orderRoutes);
app.use('/api/branches', branchRoutes);
app.use('/api/headquarters', headquartersRoutes);
app.use('/api/suppliers', supplierRoutes);
app.use('/api/wishlist', wishlistRoutes);

app.get('/', (req, res) => {
res.send('Hello, world!');
Expand Down
32 changes: 32 additions & 0 deletions api/src/models/wishlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @swagger
* components:
* schemas:
* WishlistItem:
* type: object
* required:
* - wishlistItemId
* - email
* - productId
* - addedAt
* properties:
* wishlistItemId:
* type: integer
* description: The unique identifier for the wishlist item
* email:
* type: string
* description: Email address of the user
* productId:
* type: integer
* description: The ID of the product in the wishlist
* addedAt:
* type: string
* format: date-time
* description: Timestamp when the item was added to the wishlist
*/
export interface WishlistItem {
wishlistItemId: number;
email: string;
productId: number;
addedAt: Date;
}
104 changes: 104 additions & 0 deletions api/src/routes/wishlist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import wishlistRouter, { resetWishlist } from './wishlist';
import { wishlistItems as seedWishlistItems } from '../seedData';

let app: express.Express;

describe('Wishlist API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
app.use('/wishlist', wishlistRouter);
resetWishlist();
});

it('should get all wishlist items for a user', async () => {
const response = await request(app).get('/wishlist/user@example.com');
expect(response.status).toBe(200);
expect(response.body.length).toBe(3);
expect(response.body.every((item: any) => item.email === 'user@example.com')).toBe(true);
});

it('should get empty array for user with no wishlist items', async () => {
const response = await request(app).get('/wishlist/newuser@example.com');
expect(response.status).toBe(200);
expect(response.body.length).toBe(0);
});

it('should add a new product to wishlist', async () => {
const newItem = {
email: 'newuser@example.com',
productId: 4
};
const response = await request(app).post('/wishlist').send(newItem);
expect(response.status).toBe(201);
expect(response.body.email).toBe(newItem.email);
expect(response.body.productId).toBe(newItem.productId);
expect(response.body.wishlistItemId).toBeDefined();
expect(response.body.addedAt).toBeDefined();
});

it('should prevent duplicate wishlist items', async () => {
const duplicateItem = {
email: 'user@example.com',
productId: 1
};
const response = await request(app).post('/wishlist').send(duplicateItem);
expect(response.status).toBe(400);
expect(response.text).toBe('Product already in wishlist');
});

it('should return 404 when adding non-existent product', async () => {
const invalidItem = {
email: 'user@example.com',
productId: 9999
};
const response = await request(app).post('/wishlist').send(invalidItem);
expect(response.status).toBe(404);
expect(response.text).toBe('Product not found');
});

it('should remove a product from wishlist', async () => {
const response = await request(app).delete('/wishlist/user@example.com/1');
expect(response.status).toBe(204);

// Verify it's removed
const getResponse = await request(app).get('/wishlist/user@example.com');
expect(getResponse.body.some((item: any) => item.productId === 1)).toBe(false);
});

it('should return 404 when removing non-existent wishlist item', async () => {
const response = await request(app).delete('/wishlist/user@example.com/9999');
expect(response.status).toBe(404);
expect(response.text).toBe('Wishlist item not found');
});

it('should filter wishlist items by email correctly', async () => {
const user1Response = await request(app).get('/wishlist/user@example.com');
const user2Response = await request(app).get('/wishlist/admin@github.com');

expect(user1Response.body.length).toBe(3);
expect(user2Response.body.length).toBe(2);

expect(user1Response.body.every((item: any) => item.email === 'user@example.com')).toBe(true);
expect(user2Response.body.every((item: any) => item.email === 'admin@github.com')).toBe(true);
});

it('should reset wishlist to seed data', async () => {
// Add a new item
await request(app).post('/wishlist').send({
email: 'test@example.com',
productId: 5
});

// Reset the wishlist
resetWishlist();

// Verify it was reset to seed data
const response = await request(app).get('/wishlist/user@example.com');
expect(response.status).toBe(200);
expect(response.body.length).toBe(seedWishlistItems.filter(item => item.email === 'user@example.com').length);
});
});
151 changes: 151 additions & 0 deletions api/src/routes/wishlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @swagger
* tags:
* name: Wishlist
* description: API endpoints for managing user wishlists
*/

/**
* @swagger
* /api/wishlist/{email}:
* get:
* summary: Get all wishlist items for a user
* tags: [Wishlist]
* parameters:
* - in: path
* name: email
* required: true
* schema:
* type: string
* description: User email address
* responses:
* 200:
* description: List of wishlist items for the user
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/WishlistItem'
*
* /api/wishlist:
* post:
* summary: Add a product to the wishlist
* tags: [Wishlist]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* email:
* type: string
* description: User email address
* productId:
* type: integer
* description: Product ID to add to wishlist
* responses:
* 201:
* description: Product added to wishlist successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/WishlistItem'
* 400:
* description: Product already in wishlist or product not found
* 404:
* description: Product not found
*
* /api/wishlist/{email}/{productId}:
* delete:
* summary: Remove a product from the wishlist
* tags: [Wishlist]
* parameters:
* - in: path
* name: email
* required: true
* schema:
* type: string
* description: User email address
* - in: path
* name: productId
* required: true
* schema:
* type: integer
* description: Product ID
* responses:
* 204:
* description: Product removed from wishlist successfully
* 404:
* description: Wishlist item not found
*/

import express from 'express';
import { WishlistItem } from '../models/wishlist';
import { wishlistItems as seedWishlistItems } from '../seedData';
import { products as seedProducts } from '../seedData';

const router = express.Router();

let wishlistItems: WishlistItem[] = [...seedWishlistItems];

// Add reset function for testing
export const resetWishlist = () => {
wishlistItems = [...seedWishlistItems];
};

// Get all wishlist items for a user
router.get('/:email', (req, res) => {
const userWishlist = wishlistItems.filter(item => item.email === req.params.email);
res.json(userWishlist);
});

// Add a product to the wishlist
router.post('/', (req, res) => {
const { email, productId } = req.body;

// Validate that product exists
const productExists = seedProducts.some(p => p.productId === productId);
if (!productExists) {
res.status(404).send('Product not found');
return;
}

// Check if already in wishlist (prevent duplicates)
const existingItem = wishlistItems.find(
item => item.email === email && item.productId === productId
);
if (existingItem) {
res.status(400).send('Product already in wishlist');
return;
}

// Create new wishlist item
const newWishlistItem: WishlistItem = {
wishlistItemId: Math.max(...wishlistItems.map(item => item.wishlistItemId), 0) + 1,
email,
productId,
addedAt: new Date()
};

wishlistItems.push(newWishlistItem);
res.status(201).json(newWishlistItem);
});

// Remove a product from the wishlist
router.delete('/:email/:productId', (req, res) => {
const { email, productId } = req.params;
const index = wishlistItems.findIndex(
item => item.email === email && item.productId === parseInt(productId)
);

if (index !== -1) {
wishlistItems.splice(index, 1);
res.status(204).send();
} else {
res.status(404).send('Wishlist item not found');
}
});

export default router;
35 changes: 35 additions & 0 deletions api/src/seedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Order } from './models/order';
import { OrderDetail } from './models/orderDetail';
import { Delivery } from './models/delivery';
import { OrderDetailDelivery } from './models/orderDetailDelivery';
import { WishlistItem } from './models/wishlist';

// Suppliers
export const suppliers: Supplier[] = [
Expand Down Expand Up @@ -291,4 +292,38 @@ export const orderDetailDeliveries: OrderDetailDelivery[] = [
quantity: 20,
notes: "Delivery"
}
];

// Wishlist Items
export const wishlistItems: WishlistItem[] = [
{
wishlistItemId: 1,
email: "user@example.com",
productId: 1,
addedAt: new Date("2024-01-15T10:30:00Z")
},
{
wishlistItemId: 2,
email: "user@example.com",
productId: 3,
addedAt: new Date("2024-01-16T14:20:00Z")
},
{
wishlistItemId: 3,
email: "user@example.com",
productId: 7,
addedAt: new Date("2024-01-20T09:15:00Z")
},
{
wishlistItemId: 4,
email: "admin@github.com",
productId: 2,
addedAt: new Date("2024-01-18T11:45:00Z")
},
{
wishlistItemId: 5,
email: "admin@github.com",
productId: 5,
addedAt: new Date("2024-01-19T16:30:00Z")
}
];
7 changes: 6 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import Products from './components/entity/product/Products';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import { WishlistProvider } from './context/WishlistContext';
import AdminProducts from './components/admin/AdminProducts';
import Wishlist from './components/Wishlist';
import { useTheme } from './context/ThemeContext';

// Wrapper component to apply theme classes
Expand All @@ -24,6 +26,7 @@ function ThemedApp() {
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />} />
<Route path="/login" element={<Login />} />
<Route path="/wishlist" element={<Wishlist />} />
<Route path="/admin/products" element={<AdminProducts />} />
</Routes>
</main>
Expand All @@ -37,7 +40,9 @@ function App() {
return (
<AuthProvider>
<ThemeProvider>
<ThemedApp />
<WishlistProvider>
<ThemedApp />
</WishlistProvider>
</ThemeProvider>
</AuthProvider>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const api = {
headquarters: '/api/headquarters',
deliveries: '/api/deliveries',
orderDetails: '/api/order-details',
orderDetailDeliveries: '/api/order-detail-deliveries'
orderDetailDeliveries: '/api/order-detail-deliveries',
wishlist: '/api/wishlist'
}
};
Loading