Set up initial frontend with Vite and integrated Docker for full-stack build
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
||||
import { BudgetProvider, useBudget } from './contexts/BudgetContext'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { usePullToRefresh } from './hooks/usePullToRefresh'
|
||||
import { api } from './api/client'
|
||||
|
||||
import LoginPage from './pages/LoginPage'
|
||||
import SetupPage from './pages/SetupPage'
|
||||
import DashboardPage from './pages/DashboardPage'
|
||||
import ExpensesPage from './pages/ExpensesPage'
|
||||
import PointagePage from './pages/PointagePage'
|
||||
import SimulateurPage from './pages/SimulateurPage'
|
||||
import AdminPage from './pages/AdminPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
import EpargnesPage from './pages/EpargnesPage'
|
||||
|
||||
// ── Toast ─────────────────────────────────────────────────────
|
||||
function Toast({ msg, type }) {
|
||||
return (
|
||||
<div className={`toast show t-${type}`}>{msg}</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Budget switcher ───────────────────────────────────────────
|
||||
function BudgetSwitcher() {
|
||||
const { activeBudgetId, accessibleBudgets, switchBudget } = useBudget()
|
||||
if (accessibleBudgets.length <= 1) return null
|
||||
return (
|
||||
<div className="budget-switcher">
|
||||
{accessibleBudgets.map(b => (
|
||||
<button
|
||||
key={b.id}
|
||||
className={`budget-switch-btn${b.id === activeBudgetId ? ' active' : ''}`}
|
||||
onClick={() => switchBudget(b.id)}
|
||||
>
|
||||
{b.emoji} {b.nom}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main shell (requires auth) ────────────────────────────────
|
||||
const DEFAULT_SETTINGS = {
|
||||
nameA: '', nameB: '',
|
||||
incomeA: '', incomeB: '',
|
||||
sliderVal: 63, sliderLocked: false,
|
||||
setupDone: 'false',
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 'budget', label: 'Budget', dot: 'var(--aa)' },
|
||||
{ id: 'depenses', label: 'Dépenses', dot: 'var(--ab)' },
|
||||
{ id: 'pointage', label: 'Pointage', dot: 'var(--aa)' },
|
||||
{ id: 'simu', label: 'Simulateur', dot: '#f9a875' },
|
||||
{ id: 'epargnes', label: 'Épargnes', dot: 'var(--ab)' },
|
||||
]
|
||||
|
||||
function AppShell() {
|
||||
const { user, logout } = useAuth()
|
||||
const { activeBudget, activeBudgetId } = useBudget()
|
||||
|
||||
const [tab, setTab] = useState('budget')
|
||||
const [accountOpen, setAccountOpen] = useState(false)
|
||||
const [settings, setSettings] = useState(DEFAULT_SETTINGS)
|
||||
const [expenses, setExpenses] = useState([])
|
||||
const [commonExpenses, setCommonExpenses] = useState([])
|
||||
const [categories, setCategories] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [toast, setToast] = useState(null)
|
||||
const [toastTimer, setToastTimer] = useState(null)
|
||||
|
||||
function showToast(msg, type = 'ok') {
|
||||
setToast({ msg, type })
|
||||
clearTimeout(toastTimer)
|
||||
const t = setTimeout(() => setToast(null), 2600)
|
||||
setToastTimer(t)
|
||||
}
|
||||
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [s, e, c, ce] = await Promise.all([
|
||||
api.get('/settings'),
|
||||
api.get('/expenses'),
|
||||
api.get('/categories'),
|
||||
api.get('/expenses?budget_id=1'),
|
||||
])
|
||||
setSettings({
|
||||
...DEFAULT_SETTINGS, ...s,
|
||||
sliderVal: parseInt(s.sliderVal) || 63,
|
||||
sliderLocked: s.sliderLocked === 'true',
|
||||
})
|
||||
setExpenses(e)
|
||||
setCategories(c)
|
||||
setCommonExpenses(ce)
|
||||
} catch {
|
||||
showToast('Impossible de joindre le serveur', 'err')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => { if (user) loadAll() }, [user, activeBudgetId, loadAll])
|
||||
|
||||
const { pullY, refreshing, triggered } = usePullToRefresh(loadAll)
|
||||
|
||||
const visibleTabs = TABS
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-overlay">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.setupDone !== 'true') {
|
||||
return <SetupPage onComplete={loadAll} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
{/* Pull-to-refresh indicator */}
|
||||
{(pullY > 0 || refreshing) && (
|
||||
<div className="ptr-indicator" style={{ '--ptr-y': `${pullY}px` }}>
|
||||
<div className={`ptr-spinner${triggered || refreshing ? ' spin' : ''}`}>↻</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="wrap">
|
||||
<header>
|
||||
<div className="eyebrow">Espace familial</div>
|
||||
<h1>Budget <span>{activeBudget.nom === 'Commun' ? 'Commun' : activeBudget.nom}</span></h1>
|
||||
<div className="account-wrap">
|
||||
<button
|
||||
className={`account-btn${(tab === 'compte' || tab === 'admin') ? ' active' : ''}`}
|
||||
onClick={() => setAccountOpen(o => !o)}
|
||||
title="Compte"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="8" r="4"/>
|
||||
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
{accountOpen && (
|
||||
<>
|
||||
<div className="account-backdrop" onClick={() => setAccountOpen(false)} />
|
||||
<div className="account-dropdown">
|
||||
<button onClick={() => { setTab('compte'); setAccountOpen(false) }}>
|
||||
Compte
|
||||
</button>
|
||||
{user?.isAdmin && (
|
||||
<button onClick={() => { setTab('admin'); setAccountOpen(false) }}>
|
||||
Admin
|
||||
</button>
|
||||
)}
|
||||
<button className="dropdown-logout" onClick={() => { setAccountOpen(false); logout() }}>
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Budget switcher (multi-profil — étape 4) */}
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<BudgetSwitcher />
|
||||
</div>
|
||||
|
||||
{/* Tab nav */}
|
||||
<nav className="tabs-nav">
|
||||
{visibleTabs.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={`tab-btn${tab === t.id ? ' active' : ''}`}
|
||||
onClick={() => setTab(t.id)}
|
||||
>
|
||||
<span className="tdot" style={{ background: t.dot }} />
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Page content */}
|
||||
{tab === 'budget' && (
|
||||
<DashboardPage
|
||||
expenses={expenses}
|
||||
commonExpenses={commonExpenses}
|
||||
categories={categories}
|
||||
settings={settings}
|
||||
onSettingsChange={setSettings}
|
||||
/>
|
||||
)}
|
||||
{tab === 'depenses' && (
|
||||
<ExpensesPage
|
||||
expenses={expenses}
|
||||
categories={categories}
|
||||
onExpensesChange={setExpenses}
|
||||
onToast={showToast}
|
||||
/>
|
||||
)}
|
||||
{tab === 'pointage' && (
|
||||
<PointagePage categories={categories} />
|
||||
)}
|
||||
{tab === 'simu' && (
|
||||
<SimulateurPage
|
||||
settings={settings}
|
||||
expenses={expenses}
|
||||
onExpensesChange={setExpenses}
|
||||
onToast={showToast}
|
||||
/>
|
||||
)}
|
||||
{tab === 'epargnes' && <EpargnesPage onToast={showToast} />}
|
||||
{tab === 'admin' && user?.isAdmin && (
|
||||
<AdminPage
|
||||
categories={categories}
|
||||
onCategoriesChange={setCategories}
|
||||
onToast={showToast}
|
||||
/>
|
||||
)}
|
||||
{tab === 'compte' && (
|
||||
<SettingsPage onToast={showToast} />
|
||||
)}
|
||||
|
||||
<footer className="wrap">Données hébergées sur votre réseau · Budget Commun</footer>
|
||||
|
||||
{toast && <Toast msg={toast.msg} type={toast.type} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Root ──────────────────────────────────────────────────────
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<BudgetProvider>
|
||||
<AppInner />
|
||||
</BudgetProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function AppInner() {
|
||||
const { user, loading } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-overlay">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) return <LoginPage />
|
||||
return <AppShell />
|
||||
}
|
||||
Reference in New Issue
Block a user