Apply stack design system: light theme + Lucide icons
- index.css: match portal token system (light body, dark login chrome) - NoticeBoard: white cards/shadows, Lucide Pin/PinOff/Trash2/Plus/X icons, light input/button styles, category pills with navy active state - AuthGate: explicit dark bg on login container (body is now light) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58b3d82b85
commit
24747919cd
5 changed files with 1957 additions and 63 deletions
1859
frontend/package-lock.json
generated
Normal file
1859
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,10 @@ export function AuthGate({ children }: Props) {
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
justifyContent: 'center', height: '100dvh', padding: '1.5rem',
|
justifyContent: 'center', height: '100dvh', padding: '1.5rem',
|
||||||
|
background: 'var(--navy-dark)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--surface)', borderRadius: 'var(--radius)',
|
background: 'var(--navy)', borderRadius: 'var(--radius)',
|
||||||
padding: '2rem', width: '100%', maxWidth: '360px',
|
padding: '2rem', width: '100%', maxWidth: '360px',
|
||||||
border: '1px solid var(--surface-2)',
|
border: '1px solid var(--surface-2)',
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useReducer, useState } from 'react'
|
import { useEffect, useReducer, useState } from 'react'
|
||||||
|
import { Pin, PinOff, Trash2, Plus, X } from 'lucide-react'
|
||||||
|
|
||||||
interface Notice {
|
interface Notice {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -14,11 +15,11 @@ interface Notice {
|
||||||
interface User { email: string; name: string; is_admin: boolean }
|
interface User { email: string; name: string; is_admin: boolean }
|
||||||
|
|
||||||
const CATEGORIES = [
|
const CATEGORIES = [
|
||||||
{ value: 'all', label: 'All' },
|
{ value: 'all', label: 'All' },
|
||||||
{ value: 'general', label: 'General' },
|
{ value: 'general', label: 'General' },
|
||||||
{ value: 'kitchen', label: 'Kitchen' },
|
{ value: 'kitchen', label: 'Kitchen' },
|
||||||
{ value: 'housekeeping', label: 'Housekeeping' },
|
{ value: 'housekeeping',label: 'Housekeeping' },
|
||||||
{ value: 'management', label: 'Management' },
|
{ value: 'management', label: 'Management' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function NoticeBoard({ user }: { user: User }) {
|
export function NoticeBoard({ user }: { user: User }) {
|
||||||
|
|
@ -46,24 +47,27 @@ export function NoticeBoard({ user }: { user: User }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: '720px', margin: '0 auto', padding: '1rem' }}>
|
<div style={{ maxWidth: '720px', margin: '0 auto', padding: '1.5rem 1rem' }}>
|
||||||
<header style={{
|
<header style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
marginBottom: '1.25rem', paddingBottom: '1rem',
|
marginBottom: '1.25rem',
|
||||||
borderBottom: '1px solid var(--surface-2)',
|
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontSize: '1.25rem', color: 'var(--gold)' }}>Noticeboard</h1>
|
<h1 style={{ fontSize: '1.15rem', fontWeight: 700, color: 'var(--text-dark)' }}>
|
||||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{user.name}</p>
|
Noticeboard
|
||||||
|
</h1>
|
||||||
|
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginTop: '0.1rem' }}>{user.name}</p>
|
||||||
</div>
|
</div>
|
||||||
{user.is_admin && (
|
{user.is_admin && (
|
||||||
<button onClick={() => setShowForm(v => !v)} style={{
|
<button onClick={() => setShowForm(v => !v)} style={{
|
||||||
background: showForm ? 'var(--surface-2)' : 'var(--gold)',
|
background: showForm ? 'var(--card-bg)' : 'var(--gold)',
|
||||||
color: showForm ? 'var(--text)' : 'var(--navy-dark)',
|
color: showForm ? 'var(--text-mid)' : 'var(--navy)',
|
||||||
border: 'none', borderRadius: '6px', padding: '0.5rem 1rem',
|
border: showForm ? '1px solid var(--card-border)' : 'none',
|
||||||
fontSize: '0.875rem', fontWeight: 600,
|
borderRadius: '7px', padding: '0.45rem 0.9rem',
|
||||||
|
fontSize: '0.82rem', fontWeight: 600,
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
||||||
}}>
|
}}>
|
||||||
{showForm ? 'Cancel' : '+ Post'}
|
{showForm ? <><X size={14} strokeWidth={2} /> Cancel</> : <><Plus size={14} strokeWidth={2} /> Post</>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -72,13 +76,15 @@ export function NoticeBoard({ user }: { user: User }) {
|
||||||
<PostForm onPosted={() => { setShowForm(false); forceRefresh() }} />
|
<PostForm onPosted={() => { setShowForm(false); forceRefresh() }} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
{/* Category filters */}
|
||||||
|
<div style={{ display: 'flex', gap: '0.4rem', marginBottom: '1.25rem', flexWrap: 'wrap' }}>
|
||||||
{CATEGORIES.map(c => (
|
{CATEGORIES.map(c => (
|
||||||
<button key={c.value} onClick={() => setCategory(c.value)} style={{
|
<button key={c.value} onClick={() => setCategory(c.value)} style={{
|
||||||
background: category === c.value ? 'var(--gold)' : 'var(--surface)',
|
background: category === c.value ? 'var(--navy)' : 'var(--card-bg)',
|
||||||
color: category === c.value ? 'var(--navy-dark)' : 'var(--text-muted)',
|
color: category === c.value ? 'var(--gold)' : 'var(--text-mid)',
|
||||||
border: '1px solid var(--surface-2)', borderRadius: '20px',
|
border: `1px solid ${category === c.value ? 'var(--navy)' : 'var(--card-border)'}`,
|
||||||
padding: '0.3rem 0.875rem', fontSize: '0.8rem', fontWeight: 600,
|
borderRadius: '20px', padding: '0.3rem 0.875rem',
|
||||||
|
fontSize: '0.78rem', fontWeight: 500,
|
||||||
}}>
|
}}>
|
||||||
{c.label}
|
{c.label}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -86,7 +92,7 @@ export function NoticeBoard({ user }: { user: User }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{notices.length === 0 && (
|
{notices.length === 0 && (
|
||||||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '3rem 0', fontSize: '0.9rem' }}>
|
<p style={{ color: 'var(--text-mid)', textAlign: 'center', padding: '3rem 0', fontSize: '0.9rem' }}>
|
||||||
No notices yet.
|
No notices yet.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -94,29 +100,41 @@ export function NoticeBoard({ user }: { user: User }) {
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
{notices.map(n => (
|
{notices.map(n => (
|
||||||
<div key={n.id} style={{
|
<div key={n.id} style={{
|
||||||
background: 'var(--surface)', borderRadius: 'var(--radius)',
|
background: 'var(--card-bg)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
padding: '1rem 1.125rem',
|
padding: '1rem 1.125rem',
|
||||||
border: n.pinned ? '1px solid var(--gold)' : '1px solid var(--surface-2)',
|
border: `1px solid ${n.pinned ? 'var(--gold)' : 'var(--card-border)'}`,
|
||||||
|
borderLeft: `3px solid ${n.pinned ? 'var(--gold)' : 'var(--card-border)'}`,
|
||||||
|
boxShadow: 'var(--shadow-sm)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.375rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.4rem' }}>
|
||||||
{n.pinned && (
|
{n.pinned && (
|
||||||
<span style={{ fontSize: '0.7rem', background: 'var(--gold)', color: 'var(--navy-dark)',
|
<span style={{
|
||||||
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700 }}>
|
fontSize: '0.65rem', background: 'var(--gold)', color: 'var(--navy)',
|
||||||
|
borderRadius: '4px', padding: '0.1rem 0.4rem', fontWeight: 700,
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
}}>
|
||||||
PINNED
|
PINNED
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span style={{ fontSize: '0.7rem', background: 'var(--surface-2)', color: 'var(--text-muted)',
|
<span style={{
|
||||||
borderRadius: '4px', padding: '0.1rem 0.4rem', textTransform: 'uppercase' }}>
|
fontSize: '0.65rem', background: 'var(--body-bg)', color: 'var(--text-mid)',
|
||||||
|
borderRadius: '4px', padding: '0.1rem 0.4rem',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.04em',
|
||||||
|
border: '1px solid var(--card-border)',
|
||||||
|
}}>
|
||||||
{n.category}
|
{n.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>{n.title}</h2>
|
<h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)', marginBottom: '0.35rem' }}>
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
|
{n.title}
|
||||||
|
</h2>
|
||||||
|
<p style={{ color: 'var(--text-mid)', fontSize: '0.875rem', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
|
||||||
{n.body}
|
{n.body}
|
||||||
</p>
|
</p>
|
||||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.75rem', marginTop: '0.75rem' }}>
|
<p style={{ color: 'var(--text-mid)', fontSize: '0.72rem', marginTop: '0.75rem' }}>
|
||||||
{n.author_name} · {new Date(n.created_at).toLocaleDateString('en-GB', {
|
{n.author_name} · {new Date(n.created_at).toLocaleDateString('en-GB', {
|
||||||
day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||||
})}
|
})}
|
||||||
|
|
@ -124,12 +142,15 @@ export function NoticeBoard({ user }: { user: User }) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{user.is_admin && (
|
{user.is_admin && (
|
||||||
<div style={{ display: 'flex', gap: '0.4rem', flexShrink: 0 }}>
|
<div style={{ display: 'flex', gap: '0.35rem', flexShrink: 0 }}>
|
||||||
<button onClick={() => togglePin(n.id)} title={n.pinned ? 'Unpin' : 'Pin'} style={iconBtn}>
|
<button onClick={() => togglePin(n.id)} title={n.pinned ? 'Unpin' : 'Pin'} style={iconBtn}>
|
||||||
{n.pinned ? '📌' : '📍'}
|
{n.pinned
|
||||||
|
? <PinOff size={15} strokeWidth={1.75} />
|
||||||
|
: <Pin size={15} strokeWidth={1.75} />}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => deleteNotice(n.id)} title="Delete" style={{ ...iconBtn, color: 'var(--danger)' }}>
|
<button onClick={() => deleteNotice(n.id)} title="Delete"
|
||||||
✕
|
style={{ ...iconBtn, color: 'var(--danger)' }}>
|
||||||
|
<Trash2 size={15} strokeWidth={1.75} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -172,11 +193,13 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={submit} style={{
|
<form onSubmit={submit} style={{
|
||||||
background: 'var(--surface)', borderRadius: 'var(--radius)',
|
background: 'var(--card-bg)', borderRadius: 'var(--radius)',
|
||||||
padding: '1.25rem', marginBottom: '1.25rem',
|
padding: '1.25rem', marginBottom: '1.25rem',
|
||||||
border: '1px solid var(--gold)', display: 'flex', flexDirection: 'column', gap: '0.75rem',
|
border: '1px solid var(--gold)',
|
||||||
|
boxShadow: 'var(--shadow-md)',
|
||||||
|
display: 'flex', flexDirection: 'column', gap: '0.75rem',
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ fontSize: '0.9rem', color: 'var(--gold)', marginBottom: '0.25rem' }}>New Notice</h2>
|
<h2 style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--text-dark)' }}>New Notice</h2>
|
||||||
<input value={title} onChange={e => setTitle(e.target.value)}
|
<input value={title} onChange={e => setTitle(e.target.value)}
|
||||||
placeholder="Title" required style={inputStyle} />
|
placeholder="Title" required style={inputStyle} />
|
||||||
<textarea value={body} onChange={e => setBody(e.target.value)}
|
<textarea value={body} onChange={e => setBody(e.target.value)}
|
||||||
|
|
@ -191,7 +214,8 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
|
||||||
<input type="date" value={expires} onChange={e => setExpires(e.target.value)}
|
<input type="date" value={expires} onChange={e => setExpires(e.target.value)}
|
||||||
style={{ ...inputStyle, flex: 1 }} title="Expires (optional)" />
|
style={{ ...inputStyle, flex: 1 }} title="Expires (optional)" />
|
||||||
</div>
|
</div>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem', cursor: 'pointer' }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.875rem',
|
||||||
|
cursor: 'pointer', color: 'var(--text-dark)' }}>
|
||||||
<input type="checkbox" checked={pinned} onChange={e => setPinned(e.target.checked)} />
|
<input type="checkbox" checked={pinned} onChange={e => setPinned(e.target.checked)} />
|
||||||
Pin to top
|
Pin to top
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -204,18 +228,18 @@ function PostForm({ onPosted }: { onPosted: () => void }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputStyle: React.CSSProperties = {
|
const inputStyle: React.CSSProperties = {
|
||||||
background: 'var(--navy-dark)', border: '1px solid var(--surface-2)',
|
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
|
||||||
borderRadius: '6px', color: 'var(--text)', padding: '0.6rem 0.75rem',
|
borderRadius: '7px', color: 'var(--text-dark)', padding: '0.6rem 0.75rem',
|
||||||
fontSize: '0.9rem', width: '100%', outline: 'none',
|
fontSize: '0.9rem', width: '100%', outline: 'none',
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconBtn: React.CSSProperties = {
|
const iconBtn: React.CSSProperties = {
|
||||||
background: 'none', border: 'none', fontSize: '1rem',
|
background: 'var(--body-bg)', border: '1px solid var(--card-border)',
|
||||||
padding: '0.25rem', borderRadius: '4px', color: 'var(--text-muted)',
|
borderRadius: '6px', padding: '0.3rem', color: 'var(--text-mid)',
|
||||||
lineHeight: 1,
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitBtn: React.CSSProperties = {
|
const submitBtn: React.CSSProperties = {
|
||||||
background: 'var(--gold)', color: 'var(--navy-dark)', border: 'none',
|
background: 'var(--gold)', color: 'var(--navy)', border: 'none',
|
||||||
borderRadius: '6px', padding: '0.625rem', fontSize: '0.9rem', fontWeight: 600,
|
borderRadius: '7px', padding: '0.625rem', fontSize: '0.9rem', fontWeight: 600,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,36 @@
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--navy: #1e3a5f;
|
/* ── Shell chrome (dark) — for standalone login screens ── */
|
||||||
--navy-dark: #0f1f35;
|
--navy: #1a1a2e;
|
||||||
--gold: #c9a84c;
|
--navy-dark: #0f0f20;
|
||||||
--gold-light: #e8c96d;
|
--gold: #c9a84c;
|
||||||
--surface: #1a2d47;
|
--gold-light: #e8c96d;
|
||||||
--surface-2: #243d5c;
|
--surface: rgba(255,255,255,0.07);
|
||||||
--text: #e8edf2;
|
--surface-2: rgba(255,255,255,0.08);
|
||||||
--text-muted: #8ba3bc;
|
--text: rgba(255,255,255,0.88);
|
||||||
--danger: #e05252;
|
--text-muted: rgba(255,255,255,0.48);
|
||||||
--radius: 10px;
|
|
||||||
|
/* ── Content area (light) ── */
|
||||||
|
--body-bg: #f4f5f7;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--card-border: #e4e8ee;
|
||||||
|
--text-dark: #1e293b;
|
||||||
|
--text-mid: #64748b;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
|
||||||
|
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
|
|
||||||
|
/* ── Shared ── */
|
||||||
|
--danger: #dc2626;
|
||||||
|
--radius: 10px;
|
||||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--navy-dark);
|
background: var(--body-bg);
|
||||||
color: var(--text);
|
color: var(--text-dark);
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button { cursor: pointer; font-family: inherit; }
|
||||||
cursor: pointer;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue