Files
ui-tracker/frontend/src/App.tsx
2026-03-27 23:33:31 -05:00

162 lines
5.3 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { Package, Plus, Settings as SettingsIcon } from 'lucide-react';
import type { WatchedItem, Settings } from './types';
import { getItems, getSettings } from './api/client';
import ItemCard from './components/ItemCard';
import AddItemModal from './components/AddItemModal';
import EditItemModal from './components/EditItemModal';
import SettingsModal from './components/SettingsModal';
export default function App() {
const [items, setItems] = useState<WatchedItem[]>([]);
const [settings, setSettings] = useState<Settings | null>(null);
const [loading, setLoading] = useState(true);
const [connected, setConnected] = useState<boolean | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [editItem, setEditItem] = useState<WatchedItem | null>(null);
const [showSettings, setShowSettings] = useState(false);
const fetchItems = useCallback(async () => {
try {
const data = await getItems();
setItems(data);
setConnected(true);
} catch {
setConnected(false);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchItems();
const id = setInterval(fetchItems, 10_000);
return () => clearInterval(id);
}, [fetchItems]);
useEffect(() => {
getSettings().then(setSettings).catch(() => null);
}, []);
const inStock = items.filter(i => i.last_status === 'in_stock').length;
const active = items.filter(i => i.is_active === 1).length;
return (
<div className="app">
{/* ── Header ── */}
<header className="header">
<div className="header-inner">
<div className="logo">
<div className="logo-icon">
<Package size={18} strokeWidth={2.5} />
</div>
<span className="logo-text">UI <span>Stock</span> Tracker</span>
</div>
<div className="header-stats">
<div className="stat-pill">
<strong>{items.length}</strong> Tracked
</div>
<div className="stat-pill active">
<strong>{active}</strong> Active
</div>
{inStock > 0 && (
<div className="stat-pill in-stock">
<strong>{inStock}</strong> In Stock
</div>
)}
</div>
<div className="header-spacer" />
<div className="header-actions">
<div
className={`conn-dot ${connected === true ? 'connected' : connected === false ? 'error' : ''}`}
title={connected === true ? 'Connected to backend' : connected === false ? 'Cannot reach backend' : 'Connecting…'}
/>
<button
className="btn-secondary"
onClick={() => setShowSettings(true)}
title="Settings"
>
<SettingsIcon size={15} />
Settings
</button>
<button className="btn-primary" onClick={() => setShowAdd(true)}>
<Plus size={16} strokeWidth={2.5} />
Add Item
</button>
</div>
</div>
</header>
{/* ── Main ── */}
<main className="main">
{loading ? (
<div className="loading">
<div className="spinner" />
Connecting to backend
</div>
) : connected === false ? (
<div className="error-banner">
Cannot reach the backend. Make sure the Docker container is running.
</div>
) : items.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">
<Package size={36} strokeWidth={1.5} />
</div>
<h2>Nothing tracked yet</h2>
<p>
Add a product URL from <strong>store.ui.com</strong> and the tracker will alert you
the moment it comes back in stock.
</p>
<button className="btn-primary" onClick={() => setShowAdd(true)}>
<Plus size={16} strokeWidth={2.5} />
Add your first item
</button>
</div>
) : (
<>
<div className="section-header">
<span className="section-title">{items.length} item{items.length !== 1 ? 's' : ''} tracked</span>
</div>
<div className="items-grid">
{items.map(item => (
<ItemCard
key={item.id}
item={item}
onEdit={setEditItem}
onRefresh={fetchItems}
/>
))}
</div>
</>
)}
</main>
{/* ── Modals ── */}
{showAdd && (
<AddItemModal
onClose={() => setShowAdd(false)}
onAdded={() => { setShowAdd(false); fetchItems(); }}
/>
)}
{editItem && (
<EditItemModal
item={editItem}
onClose={() => setEditItem(null)}
onSaved={() => { setEditItem(null); fetchItems(); }}
/>
)}
{showSettings && (
<SettingsModal
settings={settings}
onClose={() => setShowSettings(false)}
onSaved={s => { setSettings(s); setShowSettings(false); }}
/>
)}
</div>
);
}