Initial commit: Fast Inventory app
Monorepo with Express + SQLite backend and Vite + React frontend. Features: item CRUD, file uploads with thumbnails, soft delete, item duplication with file copying, autocomplete inputs, stats dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
96
client/src/api.ts
Normal file
96
client/src/api.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Item, CreateItemBody, UpdateItemBody, StatsResponse, ItemFile } from './types';
|
||||
|
||||
const BASE = '/api';
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...init,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Request failed: ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Items
|
||||
listItems(params?: Record<string, string>): Promise<Item[]> {
|
||||
const qs = params ? '?' + new URLSearchParams(params).toString() : '';
|
||||
return request(`/items${qs}`);
|
||||
},
|
||||
getItem(id: string): Promise<Item & { files: ItemFile[] }> {
|
||||
return request(`/items/${id}`);
|
||||
},
|
||||
createItem(body: CreateItemBody): Promise<Item> {
|
||||
return request('/items', { method: 'POST', body: JSON.stringify(body) });
|
||||
},
|
||||
updateItem(id: string, body: UpdateItemBody): Promise<Item> {
|
||||
return request(`/items/${id}`, { method: 'PUT', body: JSON.stringify(body) });
|
||||
},
|
||||
deleteItem(id: string): Promise<void> {
|
||||
return request(`/items/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
restoreItem(id: string): Promise<Item> {
|
||||
return request(`/items/${id}/restore`, { method: 'POST' });
|
||||
},
|
||||
permanentDeleteItem(id: string): Promise<void> {
|
||||
return request(`/items/${id}/permanent`, { method: 'DELETE' });
|
||||
},
|
||||
duplicateItem(id: string): Promise<Item> {
|
||||
return request(`/items/${id}/duplicate`, { method: 'POST' });
|
||||
},
|
||||
|
||||
bulkUpdateItems(ids: string[], updates: { category?: string; location?: string; status?: string; flagged?: number }): Promise<{ updated: number }> {
|
||||
return request('/items/bulk', { method: 'PATCH', body: JSON.stringify({ ids, updates }) });
|
||||
},
|
||||
|
||||
// Suggestions
|
||||
getCategories(): Promise<string[]> {
|
||||
return request('/suggestions/categories');
|
||||
},
|
||||
getLocations(): Promise<string[]> {
|
||||
return request('/suggestions/locations');
|
||||
},
|
||||
|
||||
// Files
|
||||
async uploadFiles(itemId: string, files: File[], type: 'photo' | 'receipt'): Promise<ItemFile[]> {
|
||||
const form = new FormData();
|
||||
form.append('type', type);
|
||||
for (const f of files) form.append('files', f);
|
||||
const res = await fetch(`${BASE}/items/${itemId}/files`, { method: 'POST', body: form });
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
return res.json();
|
||||
},
|
||||
deleteFile(fileId: string): Promise<void> {
|
||||
return request(`/files/${fileId}`, { method: 'DELETE' });
|
||||
},
|
||||
setFeaturedPhoto(itemId: string, fileId: string): Promise<void> {
|
||||
return request(`/items/${itemId}/files/${fileId}/featured`, { method: 'PUT' });
|
||||
},
|
||||
|
||||
// Stats
|
||||
getStats(): Promise<StatsResponse> {
|
||||
return request('/stats');
|
||||
},
|
||||
};
|
||||
|
||||
// Image variant URL helpers
|
||||
function variantUrl(storedName: string, suffix: string): string {
|
||||
const dot = storedName.lastIndexOf('.');
|
||||
return `/api/uploads/${storedName.slice(0, dot)}${suffix}${storedName.slice(dot)}`;
|
||||
}
|
||||
|
||||
export function thumbUrl(storedName: string): string {
|
||||
return variantUrl(storedName, '_thumb');
|
||||
}
|
||||
|
||||
export function largeUrl(storedName: string): string {
|
||||
return variantUrl(storedName, '_large');
|
||||
}
|
||||
|
||||
export function uploadUrl(storedName: string): string {
|
||||
return `/api/uploads/${storedName}`;
|
||||
}
|
||||
Reference in New Issue
Block a user