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>
495 lines
20 KiB
JavaScript
495 lines
20 KiB
JavaScript
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>
|
||
)
|
||
}
|