feat: initial commit — local services (backend + manager dashboard + waiter PWA)
Includes all work to date: - local_backend: FastAPI backend with products, orders, tables, shifts, cloud sync - manager_dashboard: React manager UI with product/category management, reports, settings - waiter_pwa: React PWA for waiter devices - Category reparent endpoint and UI - Waiter domain: local_ip sent on heartbeat, waiter_domain persisted from cloud response - QR code modal in AppInfoTab for waiter domain - Product form: number input spinners removed, category pre-selected on new product - Category row: count badge moved to far right Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
494
manager_dashboard/src/pages/Settings/tabs/ColoursTab.jsx
Normal file
494
manager_dashboard/src/pages/Settings/tabs/ColoursTab.jsx
Normal file
@@ -0,0 +1,494 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { DEFAULT_COLOURS } from '../../../store/tableColourStore'
|
||||
import client from '../../../api/client'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
// ─── Colour slot metadata ────────────────────────────────────────────────────
|
||||
|
||||
const SLOTS = [
|
||||
{ key: 'cardBg', label: 'Κύριο Φόντο', hint: 'Φόντο κάρτας' },
|
||||
{ key: 'badgeBg', label: 'Δευτερεύον Φόντο', hint: 'Φόντο badge κατάστασης' },
|
||||
{ key: 'nameText', label: 'Κύριο Κείμενο', hint: 'Όνομα τραπεζιού' },
|
||||
{ key: 'badgeText', label: 'Δευτερεύον Κείμενο', hint: 'Ετικέτα badge' },
|
||||
]
|
||||
|
||||
const STATUSES = [
|
||||
{ key: 'free', label: 'Ελεύθερο' },
|
||||
{ key: 'open', label: 'Ανοιχτό (όχι δικό μου)' },
|
||||
{ key: 'mine', label: 'Ανοιχτό (δικό μου)' },
|
||||
{ key: 'partially_paid', label: 'Μερικώς Πληρωμένο' },
|
||||
{ key: 'paid', label: 'Πληρωμένο' },
|
||||
]
|
||||
|
||||
const STATUS_LABELS_MOCK = {
|
||||
free: 'ΕΛΕΥΘΕΡΟ',
|
||||
open: 'ΑΝΟΙΧΤΟ',
|
||||
mine: 'ΔΙΚΟ ΜΟΥ',
|
||||
partially_paid: 'ΜΕΡ. ΠΛHΡ.',
|
||||
paid: 'ΠΛΗΡΩΜΕΝΟ',
|
||||
}
|
||||
|
||||
// Quick-suggest palettes per slot type
|
||||
const QUICK_SWATCHES = {
|
||||
cardBg: ['#dde5ef', '#243044', '#FF8F60', '#e8610a', '#FFDC67', '#81D264', '#a78bfa', '#38bdf8', '#f43f5e', '#1e293b'],
|
||||
badgeBg: ['rgba(255,255,255,0.92)', 'rgba(0,0,0,0.55)', 'rgba(255,255,255,0.6)', 'rgba(30,41,59,0.85)', '#ffffff', '#000000'],
|
||||
nameText: ['#ffffff', '#1e293b', '#3d5270', '#94b8d4', '#f8fafc', '#111827', '#fef3c7', '#dcfce7'],
|
||||
badgeText: ['#3d5270', '#94b8d4', '#e8610a', '#FF8F60', '#FFDC67', '#d4a800', '#81D264', '#ffffff', '#1e293b'],
|
||||
}
|
||||
|
||||
// ─── Color picker modal ──────────────────────────────────────────────────────
|
||||
|
||||
// Parse any css colour string into { hex, alpha }.
|
||||
// Handles: #rrggbb, #rgb, rgba(r,g,b,a), rgb(r,g,b)
|
||||
function parseColour(v) {
|
||||
if (!v) return { hex: '#ffffff', alpha: 1 }
|
||||
const s = v.trim()
|
||||
// rgba / rgb
|
||||
const rgbaMatch = s.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/)
|
||||
if (rgbaMatch) {
|
||||
const r = parseInt(rgbaMatch[1]).toString(16).padStart(2, '0')
|
||||
const g = parseInt(rgbaMatch[2]).toString(16).padStart(2, '0')
|
||||
const b = parseInt(rgbaMatch[3]).toString(16).padStart(2, '0')
|
||||
const a = rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1
|
||||
return { hex: `#${r}${g}${b}`, alpha: Math.min(1, Math.max(0, a)) }
|
||||
}
|
||||
// #rgb shorthand
|
||||
if (/^#[0-9a-fA-F]{3}$/.test(s)) {
|
||||
const [, r, g, b] = s
|
||||
return { hex: `#${r}${r}${g}${g}${b}${b}`, alpha: 1 }
|
||||
}
|
||||
// #rrggbb
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(s)) return { hex: s, alpha: 1 }
|
||||
return { hex: '#ffffff', alpha: 1 }
|
||||
}
|
||||
|
||||
function buildColour(hex, alpha) {
|
||||
if (alpha >= 1) return hex
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return `rgba(${r},${g},${b},${alpha.toFixed(2)})`
|
||||
}
|
||||
|
||||
function ColourPickerModal({ value, onClose, onChange, slot }) {
|
||||
const parsed = parseColour(value)
|
||||
const [hex, setHex] = useState(parsed.hex)
|
||||
const [alpha, setAlpha] = useState(parsed.alpha)
|
||||
|
||||
// keep parent in sync whenever hex or alpha changes
|
||||
useEffect(() => { onChange(buildColour(hex, alpha)) }, [hex, alpha])
|
||||
|
||||
function commitSwatch(v) {
|
||||
const p = parseColour(v)
|
||||
setHex(p.hex)
|
||||
setAlpha(p.alpha)
|
||||
}
|
||||
|
||||
const preview = buildColour(hex, alpha)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: 24,
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff', borderRadius: 20, padding: 28, width: '100%', maxWidth: 400,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>Επιλογή Χρώματος</div>
|
||||
<div style={{ fontSize: 12, color: '#6b7280', marginTop: 2 }}>{SLOTS.find(s => s.key === slot)?.label}</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ background: 'none', border: 'none', fontSize: 22, cursor: 'pointer', color: '#6b7280', lineHeight: 1 }}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Preview swatch — checkerboard behind so alpha is visible */}
|
||||
<div style={{
|
||||
width: '100%', height: 56, borderRadius: 12, marginBottom: 20,
|
||||
border: '1px solid #e5e7eb', overflow: 'hidden', position: 'relative',
|
||||
backgroundImage: 'linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)',
|
||||
backgroundSize: '12px 12px',
|
||||
backgroundPosition: '0 0,0 6px,6px -6px,-6px 0',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, background: preview,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 11, fontFamily: 'monospace', color: alpha > 0.5 ? '#fff' : '#374151',
|
||||
textShadow: alpha > 0.5 ? '0 1px 3px rgba(0,0,0,0.5)' : 'none',
|
||||
}}>
|
||||
{preview}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colour picker + hex input */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151', marginBottom: 8 }}>Χρώμα</div>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<input
|
||||
type="color"
|
||||
value={hex}
|
||||
onChange={e => setHex(e.target.value)}
|
||||
style={{ width: 48, height: 40, borderRadius: 8, border: '1px solid #e5e7eb', cursor: 'pointer', padding: 2, flexShrink: 0 }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={hex}
|
||||
onChange={e => {
|
||||
const v = e.target.value
|
||||
setHex(v)
|
||||
}}
|
||||
spellCheck={false}
|
||||
style={{
|
||||
flex: 1, height: 40, borderRadius: 8, border: '1px solid #e5e7eb',
|
||||
padding: '0 12px', fontSize: 13, fontFamily: 'monospace', color: '#111827',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Opacity slider — always visible */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>Διαφάνεια</div>
|
||||
<div style={{ fontSize: 12, fontFamily: 'monospace', color: '#6b7280' }}>{Math.round(alpha * 100)}%</div>
|
||||
</div>
|
||||
{/* Gradient track so you can see what you're dragging */}
|
||||
<div style={{
|
||||
position: 'relative', height: 28,
|
||||
background: `linear-gradient(to right, transparent, ${hex})`,
|
||||
borderRadius: 8, border: '1px solid #e5e7eb',
|
||||
backgroundImage: `linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%),linear-gradient(to right,transparent,${hex})`,
|
||||
backgroundSize: '10px 10px,10px 10px,10px 10px,10px 10px,100% 100%',
|
||||
backgroundPosition: '0 0,0 5px,5px -5px,-5px 0,0 0',
|
||||
}}>
|
||||
<input
|
||||
type="range"
|
||||
min={0} max={1} step={0.01}
|
||||
value={alpha}
|
||||
onChange={e => setAlpha(parseFloat(e.target.value))}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, width: '100%', height: '100%',
|
||||
opacity: 0, cursor: 'pointer', margin: 0,
|
||||
}}
|
||||
/>
|
||||
{/* thumb indicator */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '50%', transform: 'translate(-50%,-50%)',
|
||||
left: `${alpha * 100}%`,
|
||||
width: 20, height: 20, borderRadius: '50%',
|
||||
background: preview, border: '2px solid #fff',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.3)',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick swatches */}
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#374151', marginBottom: 8 }}>Γρήγορη επιλογή</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{(QUICK_SWATCHES[slot] || []).map(c => {
|
||||
const p = parseColour(c)
|
||||
const built = buildColour(p.hex, p.alpha)
|
||||
return (
|
||||
<button
|
||||
key={c}
|
||||
title={c}
|
||||
onClick={() => commitSwatch(c)}
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 8,
|
||||
backgroundImage: `linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)`,
|
||||
backgroundSize: '8px 8px',
|
||||
backgroundPosition: '0 0,0 4px,4px -4px,-4px 0',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
border: built === preview ? '3px solid #3758c9' : '2px solid #e5e7eb',
|
||||
cursor: 'pointer', flexShrink: 0,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.10)',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'absolute', inset: 0, background: c }} />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20, paddingTop: 16, borderTop: '1px solid #f3f4f6', display: 'flex', gap: 10 }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
flex: 1, height: 40, borderRadius: 10, border: '1px solid #e5e7eb',
|
||||
background: '#f9fafb', fontSize: 14, fontWeight: 600, cursor: 'pointer', color: '#374151',
|
||||
}}
|
||||
>Κλείσιμο</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Single colour slot row ──────────────────────────────────────────────────
|
||||
|
||||
function ColourSlotRow({ mode, status, slotKey, label, value, onOpen }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 0' }}>
|
||||
<button
|
||||
onClick={() => onOpen(mode, status, slotKey, value)}
|
||||
style={{
|
||||
width: 44, height: 28, borderRadius: 8, background: value,
|
||||
border: '1.5px solid #e5e7eb', cursor: 'pointer', flexShrink: 0,
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.10)',
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: '#374151' }}>{label}</div>
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', fontFamily: 'monospace', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Mini mock table card (for preview) ──────────────────────────────────────
|
||||
|
||||
function MockCard({ cfg, label, mockName, groupName = 'ΜΕΣΑ' }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: 90, borderRadius: 12, background: cfg.cardBg,
|
||||
position: 'relative', flexShrink: 0,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Table name + group */}
|
||||
<div style={{ position: 'absolute', top: 8, left: 10, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<span style={{
|
||||
fontSize: 17, fontWeight: 800, color: cfg.nameText,
|
||||
lineHeight: 1, letterSpacing: -0.5,
|
||||
}}>{mockName}</span>
|
||||
<span style={{
|
||||
fontSize: 7, fontWeight: 600, letterSpacing: 0.8,
|
||||
color: cfg.nameText + '80',
|
||||
textTransform: 'uppercase',
|
||||
}}>{groupName}</span>
|
||||
</div>
|
||||
{/* Status badge — tight equal padding on all sides */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 7, left: 7,
|
||||
background: cfg.badgeBg,
|
||||
borderRadius: 4, padding: '2px 5px',
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
<span style={{ fontSize: 7, fontWeight: 700, color: cfg.badgeText, whiteSpace: 'nowrap', lineHeight: 1 }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Preview panel (6 mock cards per theme) ──────────────────────────────────
|
||||
|
||||
function PreviewPanel({ colours, mode }) {
|
||||
const isDark = mode === 'dark'
|
||||
const panelBg = isDark ? '#0d1520' : '#f1f5f9'
|
||||
const panelLabel = isDark ? '🌙 Προεπισκόπηση σκοτεινού θέματος' : '☀️ Προεπισκόπηση φωτεινού θέματος'
|
||||
const labelCol = isDark ? '#94a3b8' : '#64748b'
|
||||
|
||||
const mockCards = [
|
||||
{ status: 'free', name: 'TABLE 1', group: 'ΜΕΣΑ' },
|
||||
{ status: 'open', name: 'TABLE 2', group: 'ΜΕΣΑ' },
|
||||
{ status: 'mine', name: 'TABLE 3', group: 'ΜΕΣΑ' },
|
||||
{ status: 'partially_paid', name: 'TABLE 4', group: 'ΞΑΠΛΩΣΤΡΕΣ' },
|
||||
{ status: 'paid', name: 'TABLE 5', group: 'ΞΑΠΛΩΣΤΡΕΣ' },
|
||||
{ status: 'free', name: 'TABLE 6', group: 'ΞΑΠΛΩΣΤΡΕΣ' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: panelBg, borderRadius: 16, padding: 16,
|
||||
border: '1px solid ' + (isDark ? '#253245' : '#cbd5e1'),
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: labelCol, marginBottom: 12, letterSpacing: 0.3 }}>
|
||||
{panelLabel}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
|
||||
{mockCards.map((mc, i) => (
|
||||
<MockCard
|
||||
key={i}
|
||||
cfg={colours[mode][mc.status]}
|
||||
label={STATUS_LABELS_MOCK[mc.status]}
|
||||
mockName={mc.name}
|
||||
groupName={mc.group}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Status block (one status, showing all 4 slots) ──────────────────────────
|
||||
|
||||
function StatusBlock({ mode, status, label, colours, onOpen }) {
|
||||
const cfg = colours[mode][status]
|
||||
return (
|
||||
<div style={{ background: '#f9fafb', borderRadius: 12, padding: '14px 16px', border: '1px solid #f0f0f0' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{ width: 88, flexShrink: 0 }}>
|
||||
<MockCard cfg={cfg} label={STATUS_LABELS_MOCK[status]} mockName="T1" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: '#111827' }}>{label}</div>
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', marginTop: 2 }}>Πατήστε ένα χρώμα για επεξεργασία</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, borderTop: '1px solid #ebebeb', paddingTop: 8 }}>
|
||||
{SLOTS.map(slot => (
|
||||
<ColourSlotRow
|
||||
key={slot.key}
|
||||
mode={mode}
|
||||
status={status}
|
||||
slotKey={slot.key}
|
||||
label={slot.label}
|
||||
value={cfg[slot.key]}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Mode section (light or dark) ────────────────────────────────────────────
|
||||
|
||||
function ModeSection({ mode, colours, onOpen }) {
|
||||
const label = mode === 'light' ? '☀️ Φωτεινό θέμα' : '🌙 Σκοτεινό θέμα'
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: '#111827', marginBottom: 14 }}>{label}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{STATUSES.map(s => (
|
||||
<StatusBlock
|
||||
key={s.key}
|
||||
mode={mode}
|
||||
status={s.key}
|
||||
label={s.label}
|
||||
colours={colours}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main tab ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function ColoursTab() {
|
||||
const [colours, setColours] = useState(DEFAULT_COLOURS)
|
||||
const [modal, setModal] = useState(null) // { mode, status, slot, value }
|
||||
const [saving, setSaving] = useState(false)
|
||||
const saveTimer = useRef(null)
|
||||
|
||||
// Load from backend on mount
|
||||
useEffect(() => {
|
||||
client.get('/api/settings/').then(r => {
|
||||
const raw = r.data?.['ui.table_colours']?.value
|
||||
if (raw) {
|
||||
try { setColours(JSON.parse(raw)) } catch {}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Debounced save to backend — 600 ms after last change
|
||||
const saveToBackend = useCallback((next) => {
|
||||
clearTimeout(saveTimer.current)
|
||||
setSaving(true)
|
||||
saveTimer.current = setTimeout(() => {
|
||||
client.put('/api/settings/ui.table_colours', { value: JSON.stringify(next) })
|
||||
.then(() => setSaving(false))
|
||||
.catch(() => { toast.error('Σφάλμα αποθήκευσης χρωμάτων'); setSaving(false) })
|
||||
}, 600)
|
||||
}, [])
|
||||
|
||||
function setColour(mode, status, slot, value) {
|
||||
setColours(prev => {
|
||||
const next = {
|
||||
...prev,
|
||||
[mode]: {
|
||||
...prev[mode],
|
||||
[status]: { ...prev[mode][status], [slot]: value },
|
||||
},
|
||||
}
|
||||
saveToBackend(next)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function openModal(mode, status, slot, value) {
|
||||
setModal({ mode, status, slot, value })
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
setColour(modal.mode, modal.status, modal.slot, value)
|
||||
setModal(m => ({ ...m, value }))
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (window.confirm('Επαναφορά όλων των χρωμάτων στις προεπιλογές; Δεν μπορεί να αναιρεθεί.')) {
|
||||
setColours(DEFAULT_COLOURS)
|
||||
saveToBackend(DEFAULT_COLOURS)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="card" style={{ padding: 24 }}>
|
||||
{saving && <p style={{ fontSize: 12, color: '#9ca3af', marginBottom: 16 }}>Αποθήκευση…</p>}
|
||||
|
||||
{/* Live previews side by side */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 32 }}>
|
||||
<PreviewPanel colours={colours} mode="light" />
|
||||
<PreviewPanel colours={colours} mode="dark" />
|
||||
</div>
|
||||
|
||||
{/* Light + Dark mode settings */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32 }}>
|
||||
<ModeSection mode="light" colours={colours} onOpen={openModal} />
|
||||
<ModeSection mode="dark" colours={colours} onOpen={openModal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset all button at bottom */}
|
||||
<div style={{ marginTop: 32, paddingTop: 24, borderTop: '1px solid #e5e7eb', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
style={{
|
||||
height: 40, padding: '0 20px', borderRadius: 10,
|
||||
border: '1.5px solid #fca5a5', background: '#fff5f5',
|
||||
color: '#dc2626', fontSize: 14, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Επαναφορά προεπιλογών
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Colour picker modal */}
|
||||
{modal && (
|
||||
<ColourPickerModal
|
||||
value={modal.value}
|
||||
slot={modal.slot}
|
||||
onClose={() => setModal(null)}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user