diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a5e51d3..8f96679 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,12 @@ import './App.css' import Survey from './components/Survey' +import Admin from './components/Admin' export default function App() { + if (window.location.pathname === '/admin') { + return + } + const scrollToQuestions = () => { document.getElementById('questions')?.scrollIntoView({ behavior: 'smooth' }) } diff --git a/frontend/src/components/Admin.css b/frontend/src/components/Admin.css new file mode 100644 index 0000000..2202710 --- /dev/null +++ b/frontend/src/components/Admin.css @@ -0,0 +1,174 @@ +.admin-gate { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--cream); +} + +.admin-gate-form { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + max-width: 320px; + padding: 2rem; +} + +.admin-gate-title { + font-family: 'Cormorant Garamond', serif; + font-size: 2rem; + font-weight: 300; + color: var(--text); + text-align: center; + margin-bottom: 0.5rem; +} + +.admin-gate-input { + padding: 0.75rem 1rem; + border: 1.5px solid var(--pink-light); + background: none; + font-family: 'Montserrat', sans-serif; + font-size: 1rem; + color: var(--text); + outline: none; + transition: border-color 0.2s; +} + +.admin-gate-input:focus { + border-color: var(--pink); +} + +.admin-gate-error { + color: var(--pink); + font-size: 0.85rem; + text-align: center; +} + +.admin-gate-btn { + padding: 0.875rem; + background: var(--pink); + color: #fff; + border: none; + font-family: 'Montserrat', sans-serif; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.15em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.2s; +} + +.admin-gate-btn:hover:not(:disabled) { background: #C2536A; } +.admin-gate-btn:disabled { opacity: 0.6; cursor: not-allowed; } + +/* ── Admin panel ── */ + +.admin { + min-height: 100vh; + background: var(--cream); + padding: 2rem; + max-width: 800px; + margin: 0 auto; +} + +.admin-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 0.5rem; +} + +.admin-title { + font-family: 'Cormorant Garamond', serif; + font-size: 2rem; + font-weight: 300; + color: var(--text); +} + +.admin-stats { + display: flex; + gap: 1.5rem; +} + +.admin-stat { + font-size: 0.85rem; + color: var(--text-muted); +} + +.admin-stat b { + color: var(--pink); +} + +.admin-empty { + color: var(--text-muted); + text-align: center; + margin-top: 4rem; +} + +.admin-cards { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.admin-card { + background: #fff; + border: 1px solid var(--pink-light); + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.admin-card-header { + display: flex; + justify-content: space-between; + align-items: baseline; + flex-wrap: wrap; + gap: 0.5rem; +} + +.admin-card-name { + font-family: 'Cormorant Garamond', serif; + font-size: 1.3rem; + color: var(--text); +} + +.admin-card-date { + font-size: 0.75rem; + color: var(--text-muted); +} + +.admin-card-fields { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1.5rem; +} + +.admin-field { + display: flex; + gap: 0.4rem; + font-size: 0.9rem; + color: var(--text); +} + +.admin-field-label { + color: var(--text-muted); +} + +.admin-card-partner { + border-top: 1px solid var(--pink-light); + padding-top: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.admin-partner-label { + font-size: 0.85rem; + font-weight: 500; + color: var(--pink); +} diff --git a/frontend/src/components/Admin.tsx b/frontend/src/components/Admin.tsx new file mode 100644 index 0000000..2531733 --- /dev/null +++ b/frontend/src/components/Admin.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react' +import './Admin.css' + +interface Response { + id: string + name: string + alcohol: string | null + food: string | null + with_partner: boolean + partner_name: string | null + partner_alcohol: string | null + partner_food: string | null + created_at: string +} + +export default function Admin() { + const [password, setPassword] = useState('') + const [authed, setAuthed] = useState(false) + const [error, setError] = useState(false) + const [responses, setResponses] = useState([]) + const [loading, setLoading] = useState(false) + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError(false) + try { + const res = await fetch('/api/admin/responses', { + headers: { 'x-admin-password': password }, + }) + if (res.status === 401) { setError(true); return } + if (!res.ok) throw new Error() + setResponses(await res.json()) + setAuthed(true) + } catch { + setError(true) + } finally { + setLoading(false) + } + } + + if (!authed) { + return ( +
+
+

Админ

+ setPassword(e.target.value)} + autoFocus + /> + {error &&

Неверный пароль

} + +
+
+ ) + } + + const total = responses.length + const totalGuests = responses.reduce((acc, r) => acc + (r.with_partner ? 2 : 1), 0) + + return ( +
+
+

Ответы гостей

+
+ {total} анкет + {totalGuests} гостей +
+
+ + {responses.length === 0 ? ( +

Ответов пока нет

+ ) : ( +
+ {responses.map(r => ( +
+
+ {r.name} + + {new Date(r.created_at).toLocaleDateString('ru-RU', { + day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' + })} + +
+
+ {r.alcohol &&
Алкоголь{r.alcohol}
} + {r.food &&
Не ест{r.food}
} +
+ {r.with_partner && ( +
+ Пара: {r.partner_name} +
+ {r.partner_alcohol &&
Алкоголь{r.partner_alcohol}
} + {r.partner_food &&
Не ест{r.partner_food}
} +
+
+ )} +
+ ))} +
+ )} +
+ ) +}