build 1
This commit is contained in:
161
frontend/src/App.tsx
Normal file
161
frontend/src/App.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user