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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@
# Production: Your deployed API URL
NEXT_PUBLIC_API_URL=http://localhost:8080

# Payment Provider
# Which payment provider to use for checkout
# 'mock' - Simulates payment flow locally (default for development)
# 'api' - Routes through backend to real provider (for production)
NEXT_PUBLIC_PAYMENT_PROVIDER=mock

2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Include runtime environment file so Next.js can load it at startup
COPY --from=builder /app/.env ./.env

# Set correct ownership
RUN chown -R nextjs:nodejs /app
Expand Down
2,526 changes: 1,200 additions & 1,326 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"next": "^16.1.1",
"next": "^16.2.6",
"react": "^19.2.3",
"react-dom": "^19.2.3"
},
Expand All @@ -33,5 +33,8 @@
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
"typescript": "^5"
},
"overrides": {
"postcss": ">=8.5.10"
}
}
29 changes: 20 additions & 9 deletions src/__tests__/components/EquipmentList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ const mockEquipmentData: EquipmentData = {
classId: 1,
className: 'Grade 1 Equipment',
items: [
{ id: 1, name: 'Notebook', quantity: 5 },
{ id: 2, name: 'Pencil', quantity: 10 },
{ id: 3, name: 'Eraser', quantity: 2 },
{ id: 4, name: 'Ruler', quantity: 1 },
{ id: 1, name: 'Notebook', quantity: 5, unitPrice: 12.5 },
{ id: 2, name: 'Pencil', quantity: 10, unitPrice: 3 },
{ id: 3, name: 'Eraser', quantity: 2, unitPrice: 1.25 },
{ id: 4, name: 'Ruler', quantity: 1, unitPrice: 6.75 },
],
};

Expand Down Expand Up @@ -61,9 +61,19 @@ describe('EquipmentList', () => {
render(<EquipmentList {...defaultProps} />);

expect(screen.getByText('Item')).toBeInTheDocument();
expect(screen.getByText('Price')).toBeInTheDocument();
expect(screen.getByText('Quantity')).toBeInTheDocument();
});

it('should render item prices', () => {
render(<EquipmentList {...defaultProps} />);

expect(screen.getByText('12.50 ILS')).toBeInTheDocument();
expect(screen.getByText('3.00 ILS')).toBeInTheDocument();
expect(screen.getByText('1.25 ILS')).toBeInTheDocument();
expect(screen.getByText('6.75 ILS')).toBeInTheDocument();
});

it('should render quantity inputs for each item', () => {
render(<EquipmentList {...defaultProps} />);

Expand Down Expand Up @@ -173,7 +183,7 @@ describe('EquipmentList', () => {
expect(mockOnQuantityChange).toHaveBeenCalledWith(1, 0);
});

it('should clamp quantity to maximum 99', async () => {
it('should clamp quantity to the initial item quantity', async () => {
render(<EquipmentList {...defaultProps} />);
const user = userEvent.setup();

Expand All @@ -184,7 +194,7 @@ describe('EquipmentList', () => {
// Should be clamped in some calls
const calls = mockOnQuantityChange.mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[1]).toBeLessThanOrEqual(99);
expect(lastCall[1]).toBeLessThanOrEqual(5);
});
});

Expand All @@ -193,11 +203,12 @@ describe('EquipmentList', () => {
render(<EquipmentList {...defaultProps} />);

const inputs = screen.getAllByRole('spinbutton');
inputs.forEach(input => {
const expectedMax = ['5', '10', '2', '1'];

inputs.forEach((input, index) => {
expect(input).toHaveAttribute('min', '0');
expect(input).toHaveAttribute('max', '99');
expect(input).toHaveAttribute('max', expectedMax[index]);
});
});
});
});

19 changes: 8 additions & 11 deletions src/__tests__/pages/CartPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* These tests mock the CartContext to provide different states
*/
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, within } from '@testing-library/react';
import '@testing-library/jest-dom';

// Mock next/link
Expand Down Expand Up @@ -163,7 +163,9 @@ describe('CartPage', () => {
render(<CartPage />);

// Total: 5 + 10 + 3 = 18 items
expect(screen.getByText(/18/)).toBeInTheDocument();
const summary = screen.getByText(/items total/i).closest('p');
expect(summary).not.toBeNull();
expect(within(summary as HTMLElement).getByText(/^18$/)).toBeInTheDocument();
expect(screen.getByText(/items total/i)).toBeInTheDocument();
});

Expand Down Expand Up @@ -320,17 +322,12 @@ describe('CartPage', () => {
];
});

it('should show disabled checkout button', () => {
it('should show checkout link pointing to /checkout', () => {
render(<CartPage />);

const checkoutButton = screen.getByRole('button', { name: /proceed to checkout/i });
expect(checkoutButton).toBeDisabled();
});

it('should show coming soon message', () => {
render(<CartPage />);

expect(screen.getByText(/checkout functionality will be available soon/i)).toBeInTheDocument();
const checkoutLink = screen.getByRole('link', { name: /proceed to checkout/i });
expect(checkoutLink).toBeInTheDocument();
expect(checkoutLink).toHaveAttribute('href', '/checkout');
});
});

Expand Down
77 changes: 55 additions & 22 deletions src/app/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,21 @@ export default function CartPage() {
});
};

const formatCurrency = (amount: number) => `${amount.toFixed(2)} ILS`;

// Calculate total items across all entries
const totalItems = cartEntries.reduce(
(sum, entry) => sum + (Array.isArray(entry.items) ? entry.items.reduce((itemSum, item) => itemSum + item.quantity, 0) : 0),
0
);

const totalCost = cartEntries.reduce(
(sum, entry) => sum + (Array.isArray(entry.items)
? entry.items.reduce((itemSum, item) => itemSum + item.quantity * (item.unitPrice ?? 1), 0)
: 0),
0
);

// Debug: log cartEntries on every render
console.log('[CartPage] render, cartEntries:', cartEntries);

Expand Down Expand Up @@ -117,7 +126,9 @@ export default function CartPage() {
<span className="font-semibold text-zinc-900 dark:text-white">{cartEntries.length}</span>
{cartEntries.length === 1 ? ' equipment list' : ' equipment lists'} •{' '}
<span className="font-semibold text-zinc-900 dark:text-white">{totalItems}</span>
{totalItems === 1 ? ' item' : ' items'} total
{totalItems === 1 ? ' item' : ' items'} total •{' '}
<span className="font-semibold text-zinc-900 dark:text-white">{formatCurrency(totalCost)}</span>
{' '}total cost
</p>
</div>

Expand Down Expand Up @@ -159,35 +170,57 @@ export default function CartPage() {

{/* Items list */}
<div className="p-4">
<div className="grid grid-cols-[1fr_auto] gap-2 text-sm">
{entry.items.map((item) => (
<div key={item.id} className="contents">
<span className="text-zinc-700 dark:text-zinc-300">
{item.name}
</span>
<span className="text-zinc-500 dark:text-zinc-400 text-right">
×{item.quantity}
</span>
</div>
))}
<div className="grid grid-cols-[1fr_auto_auto_auto] gap-2 text-sm">
<div className="contents text-xs uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
<span>Item</span>
<span className="text-right">Qty</span>
<span className="text-right">Unit Price</span>
<span className="text-right">Line Total</span>
</div>
{entry.items.map((item) => {
const unitPrice = item.unitPrice ?? 1;
const lineTotal = unitPrice * item.quantity;

return (
<div key={item.id} className="contents">
<span className="text-zinc-700 dark:text-zinc-300">
{item.name}
</span>
<span className="text-zinc-500 dark:text-zinc-400 text-right">
×{item.quantity}
</span>
<span className="text-zinc-500 dark:text-zinc-400 text-right">
{formatCurrency(unitPrice)}
</span>
<span className="text-zinc-700 dark:text-zinc-300 text-right">
{formatCurrency(lineTotal)}
</span>
</div>
);
})}
</div>

<div className="mt-3 flex justify-end text-sm font-medium text-zinc-700 dark:text-zinc-300">
Subtotal: {formatCurrency(
entry.items.reduce(
(sum, item) => sum + item.quantity * (item.unitPrice ?? 1),
0
)
)}
</div>
</div>
</div>
))}
</div>

{/* Checkout placeholder */}
{/* Checkout */}
<div className="mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-700">
<button
disabled
className="w-full py-4 px-6 bg-emerald-600 text-white font-semibold rounded-lg opacity-50 cursor-not-allowed"
title="Checkout coming soon"
<Link
href="/checkout"
className="block w-full py-4 px-6 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold rounded-lg text-center transition-colors"
>
Proceed to Checkout (Coming Soon)
</button>
<p className="text-center text-sm text-zinc-500 dark:text-zinc-400 mt-2">
Checkout functionality will be available soon.
</p>
Proceed to Checkout
</Link>
</div>
</>
)}
Expand Down
Loading
Loading