Adds Docker setup and basic API endpoints
WHAT: Introduces Dockerfiles for development and production, Docker Compose configurations, a Makefile for common tasks, shell scripts for environment setup/teardown, a basic Express API with SQLite integration. WHY: Enables easy setup and deployment of the application using Docker. Provides basic API endpoints for managing purchase items. HOW: - Creates `docker-compose.dev.yml` and `docker-compose.prod.yml` to define services and volumes. - Introduces `Dockerfile.dev` and `Dockerfile.prod` to build container images with necessary dependencies. - Adds `Makefile` with commands for building, running, and managing the application. - Implements shell scripts for simplified Docker environment management. - Sets up Express API with endpoints for CRUD operations on purchase items, using SQLite as the database. - Uses `better-sqlite3` to connect and interact with the SQLite database.
This commit is contained in:
parent
3b638a9509
commit
4557728932
20 changed files with 1955 additions and 20 deletions
43
src/lib/api.ts
Normal file
43
src/lib/api.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { PurchaseItem as StoreItem } from '@/store/purchase-store';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
||||
|
||||
export async function fetchItems(): Promise<StoreItem[]> {
|
||||
const res = await fetch(`${API_BASE}/api/items`);
|
||||
if (!res.ok) throw new Error('Failed to fetch items');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function createItem(item: StoreItem): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/api/items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create item');
|
||||
}
|
||||
|
||||
export async function updateItem(item: StoreItem): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/api/items/${encodeURIComponent(item.id)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update item');
|
||||
}
|
||||
|
||||
export async function deleteItemById(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/api/items/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Failed to delete item');
|
||||
}
|
||||
|
||||
export async function togglePurchased(id: string, purchased: boolean): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/api/items/${encodeURIComponent(id)}/purchased`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ purchased }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to toggle purchased');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
|
|||
import { DogMotivation } from "@/features/purchase/components/DogMotivation";
|
||||
|
||||
function HomePage() {
|
||||
const { items, totalBudget, getPurchasedTotal, getRemainingTotal } = usePurchaseStore();
|
||||
const { items, totalBudget, getPurchasedTotal, getRemainingTotal, hydrateFromServer } = usePurchaseStore();
|
||||
const [currentDate, setCurrentDate] = useState("");
|
||||
|
||||
const purchasedTotal = getPurchasedTotal();
|
||||
|
|
@ -29,6 +29,11 @@ function HomePage() {
|
|||
setCurrentDate(formattedDate);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// hydrate from backend once
|
||||
hydrateFromServer();
|
||||
}, [hydrateFromServer]);
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import * as api from '@/lib/api';
|
||||
|
||||
export type PurchaseItem = {
|
||||
id: string;
|
||||
|
|
@ -30,10 +31,11 @@ type PurchaseStore = {
|
|||
setTotalBudget: (budget: number) => void;
|
||||
setExchangeRates: (rates: { SGD?: number; USD?: number }) => void;
|
||||
recalculatePrices: () => void;
|
||||
addItem: (item: PurchaseItem) => void;
|
||||
updateItem: (item: PurchaseItem) => void;
|
||||
deleteItem: (id: string) => void;
|
||||
togglePurchased: (id: string) => void;
|
||||
hydrateFromServer: () => Promise<void>;
|
||||
addItem: (item: PurchaseItem) => Promise<void>;
|
||||
updateItem: (item: PurchaseItem) => Promise<void>;
|
||||
deleteItem: (id: string) => Promise<void>;
|
||||
togglePurchased: (id: string) => Promise<void>;
|
||||
|
||||
// Calculations
|
||||
getPurchasedTotal: () => number;
|
||||
|
|
@ -224,21 +226,57 @@ export const usePurchaseStore = create<PurchaseStore>()(
|
|||
})
|
||||
});
|
||||
},
|
||||
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
|
||||
updateItem: (item) =>
|
||||
set((state) => ({
|
||||
items: state.items.map((i) => (i.id === item.id ? item : i)),
|
||||
})),
|
||||
deleteItem: (id) =>
|
||||
set((state) => ({
|
||||
items: state.items.filter((item) => item.id !== id),
|
||||
})),
|
||||
togglePurchased: (id) =>
|
||||
set((state) => ({
|
||||
items: state.items.map((item) =>
|
||||
item.id === id ? { ...item, purchased: !item.purchased } : item
|
||||
),
|
||||
})),
|
||||
hydrateFromServer: async () => {
|
||||
try {
|
||||
const serverItems = await api.fetchItems();
|
||||
set({ items: serverItems });
|
||||
} catch (e) {
|
||||
console.warn('hydrateFromServer failed, using local data');
|
||||
}
|
||||
},
|
||||
addItem: async (item) => {
|
||||
set((state) => ({ items: [...state.items, item] }));
|
||||
try {
|
||||
await api.createItem(item);
|
||||
} catch (e) {
|
||||
// rollback
|
||||
set((state) => ({ items: state.items.filter((i) => i.id !== item.id) }));
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
updateItem: async (item) => {
|
||||
const prev = get().items;
|
||||
set({ items: prev.map((i) => (i.id === item.id ? item : i)) });
|
||||
try {
|
||||
await api.updateItem(item);
|
||||
} catch (e) {
|
||||
set({ items: prev });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
deleteItem: async (id) => {
|
||||
const prev = get().items;
|
||||
set({ items: prev.filter((i) => i.id !== id) });
|
||||
try {
|
||||
await api.deleteItemById(id);
|
||||
} catch (e) {
|
||||
set({ items: prev });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
togglePurchased: async (id) => {
|
||||
const prev = get().items;
|
||||
const next = prev.map((i) => (i.id === id ? { ...i, purchased: !i.purchased } : i));
|
||||
set({ items: next });
|
||||
try {
|
||||
const item = next.find((i) => i.id === id)!;
|
||||
await api.togglePurchased(id, item.purchased);
|
||||
} catch (e) {
|
||||
set({ items: prev });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
getPurchasedTotal: () =>
|
||||
get().items.reduce(
|
||||
(sum, item) => sum + (item.purchased ? item.price : 0),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue