Files
simple-pos-system/waiter_pwa/src/components/TableCard.jsx

679 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from 'react'
import useThemeStore from '../store/themeStore'
import useTableColourStore from '../store/tableColourStore'
const API_URL = import.meta.env.VITE_API_URL || ''
const STATUS_LABELS = {
free: 'ΕΛΕΥΘΕΡΟ',
open: 'ΑΝΟΙΧΤΟ',
mine: 'ΔΙΚΟ ΜΟΥ',
paid: 'ΠΛΗΡΩΜΕΝΟ',
partially_paid: 'ΜΕΡ. ΠΛHΡ.',
}
const DRAG_THRESHOLD = 8
const HOLD_MS = 480
// ─── Avatar helpers ───────────────────────────────────────────────────────────
const AVATAR_PALETTE = ['#3758c9', '#7a44c9', '#2f9e5e', '#d94b26', '#8a6d2b', '#0d7a8a', '#c93775', '#1d6f3a']
function avatarColor(name = '') {
let h = 0
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0
return AVATAR_PALETTE[h % AVATAR_PALETTE.length]
}
function WaiterAvatar({ waiter, size = 22, ring }) {
const displayName = waiter.nickname || waiter.full_name || waiter.username || '?'
const initials = displayName.trim().split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase()
const ringStyle = ring ? { boxShadow: `0 0 0 2px ${ring}` } : {}
if (waiter.avatar_url) {
return (
<img
src={API_URL + waiter.avatar_url}
alt={displayName}
style={{
width: size, height: size, borderRadius: '50%',
objectFit: 'cover', flexShrink: 0,
...ringStyle,
}}
/>
)
}
return (
<div style={{
width: size, height: size, borderRadius: '50%',
background: avatarColor(displayName),
color: 'white', fontSize: size * 0.4, fontWeight: 700,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
...ringStyle,
}}>{initials}</div>
)
}
// Renders [icon] Name, [icon] Name inline. Falls back to icons + "X Waiters" if they don't fit
// (we approximate "don't fit" as > 2 waiters for the compact footer height).
function WaiterRow({ waiters, size = 22, cfg }) {
if (!waiters?.length) return null
const textColor = cfg.nameText
// ≤ 2 waiters: show icon + name pairs
if (waiters.length <= 2) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'nowrap', overflow: 'hidden', minWidth: 0 }}>
{waiters.map((w, i) => {
const name = w.nickname || w.full_name || w.username || '?'
return (
<div key={w.id} style={{ display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' }}>
{i > 0 && <span style={{ color: textColor, opacity: 0.3, fontSize: 14, flexShrink: 0 }}>·</span>}
<WaiterAvatar waiter={w} size={size} />
<span style={{
fontSize: 12, fontWeight: 600, color: textColor, opacity: 0.85,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{name}</span>
</div>
)
})}
</div>
)
}
// > 2 waiters: icons only + "X Waiters" label
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{waiters.slice(0, 3).map((w, i) => (
<div key={w.id} style={{ marginLeft: i === 0 ? 0 : -(size * 0.28) }}>
<WaiterAvatar waiter={w} size={size} ring={cfg.cardBg} />
</div>
))}
{waiters.length > 3 && (
<div style={{
marginLeft: -(size * 0.28), height: size, padding: '0 6px',
borderRadius: size, background: `${cfg.nameText}20`,
color: cfg.nameText, fontSize: 10, fontWeight: 700,
display: 'flex', alignItems: 'center',
}}>+{waiters.length - 3}</div>
)}
<span style={{ fontSize: 12, fontWeight: 600, color: textColor, opacity: 0.7, marginLeft: 4 }}>
{waiters.length} σερβιτόροι
</span>
</div>
)
}
// ─── Status pill ──────────────────────────────────────────────────────────────
function StatusPill({ label, badgeBg, badgeText, small }) {
return (
<span style={{
display: 'inline-flex', alignItems: 'center',
height: small ? 18 : 20,
padding: small ? '0 6px' : '0 8px',
borderRadius: 4,
background: badgeBg,
color: badgeText,
fontSize: small ? 9 : 10,
fontWeight: 800,
letterSpacing: 0.4,
whiteSpace: 'nowrap',
}}>{label}</span>
)
}
// ─── Flag dot ─────────────────────────────────────────────────────────────────
function FlagDot({ flag, size = 22 }) {
const textColor = flag.text_color || '#ffffff'
return (
<div
title={flag.name}
style={{
width: size, height: size, borderRadius: '50%',
background: flag.color || '#6295F3',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: size * 0.55,
flexShrink: 0,
color: textColor,
}}
>
{flag.emoji || '🏷️'}
</div>
)
}
// ─── Flag overflow row: show up to maxShow dots, then +N bubble ───────────────
function FlagDots({ flags, size, maxShow }) {
if (!flags.length) return null
const visible = flags.slice(0, maxShow)
const overflow = flags.length - maxShow
return (
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
{visible.map(f => <FlagDot key={f.id} flag={f} size={size} />)}
{overflow > 0 && (
<div style={{
width: size, height: size, borderRadius: '50%',
background: 'rgba(0,0,0,0.18)',
color: '#fff', fontSize: size * 0.44, fontWeight: 800,
display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>+{overflow}</div>
)}
</div>
)
}
// ─── Flag chip (icon + label) ─────────────────────────────────────────────────
function FlagChip({ flag }) {
const textColor = flag.text_color || '#ffffff'
return (
<div
title={flag.name}
style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
height: 26, padding: '0 9px',
borderRadius: 13,
background: flag.color || '#6295F3',
flexShrink: 0,
}}
>
<span style={{ fontSize: 13, lineHeight: 1 }}>{flag.emoji || '🏷️'}</span>
<span style={{ fontSize: 11, fontWeight: 700, color: textColor, whiteSpace: 'nowrap' }}>
{flag.name}
</span>
</div>
)
}
// ─── Amount display ───────────────────────────────────────────────────────────
function Amount({ value, size = 22, color }) {
const s = Number(value || 0).toFixed(2)
const [whole, cents] = s.split('.')
const isNum = typeof size === 'number'
const centsSize = isNum ? size * 0.56 : `calc(${size} * 0.56)`
return (
<div style={{ lineHeight: 1, color: color || 'inherit' }}>
<span style={{ fontSize: size, fontWeight: 800, letterSpacing: -0.5 }}>{whole}</span>
<span style={{ fontSize: centsSize, fontWeight: 800, opacity: 0.8 }}>.{cents}</span>
</div>
)
}
// ─── Card variants ────────────────────────────────────────────────────────────
// 1x1 — square-ish, 4 per row. Badges top (up to 2 + +N), name center, status bottom.
function Card1x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
return (
<div style={{
width: '100%', aspectRatio: '1 / 1.05',
background: cfg.cardBg, borderRadius: 14,
position: 'relative', overflow: 'hidden',
display: 'flex', flexDirection: 'column',
padding: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
}}>
{/* top strip: badges up to 2, then +N */}
<div style={{ height: '20%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3 }}>
<FlagDots flags={flags} size={16} maxShow={2} />
</div>
{/* center: name */}
<div style={{
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: 800, fontSize: 'clamp(18px, 5vw, 26px)',
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1,
}}>
{table.label || `T${table.number}`}
</div>
{/* bottom strip: status */}
<div style={{ height: '20%', display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
<span style={{
fontSize: 7, fontWeight: 800, letterSpacing: 0.3,
color: cfg.badgeText, textTransform: 'uppercase',
background: cfg.badgeBg, borderRadius: 3,
padding: '1px 4px', whiteSpace: 'nowrap',
}}>
{STATUS_LABELS[statusKey]}
</span>
</div>
</div>
)
}
// 2x1 — half width, compact horizontal. Name left, status + badges (up to 3 + +N) right.
function Card2x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
return (
<div style={{
width: '100%', height: 64,
background: cfg.cardBg, borderRadius: 14,
padding: '10px 12px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
gap: 10, overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
}}>
<div style={{
fontWeight: 800, fontSize: 'clamp(18px, 4.5vw, 24px)',
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1, flexShrink: 0,
}}>
{table.label || `T${table.number}`}
</div>
<div style={{
display: 'flex', flexDirection: 'column',
alignItems: 'flex-end', justifyContent: 'center', gap: 4,
}}>
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
{flags.length > 0 && (
<FlagDots flags={flags} size={18} maxShow={3} />
)}
</div>
</div>
)
}
// 2x2 — current-style square. Name top-left, status (slightly smaller) below, amount bottom-left, flags right.
function Card2x2({ table, order, flags, waiterObjects, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
return (
<div style={{
width: '100%', minHeight: 116,
background: cfg.cardBg, borderRadius: 16,
padding: '12px 12px 12px',
display: 'flex', gap: 8, overflow: 'hidden',
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
}}>
{/* left column */}
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<span style={{
fontSize: 'clamp(22px, 5.5vw, 36px)', fontWeight: 800,
lineHeight: 1.05, color: cfg.nameText, letterSpacing: -0.5,
}}>
{table.label || `T${table.number}`}
</span>
<div style={{ marginTop: 5 }}>
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
</div>
<div style={{ marginTop: 'auto', paddingTop: 8, minHeight: 28 }}>
{showAmount && <Amount value={total} size={'clamp(22px, 5.5vw, 36px)'} color={cfg.nameText} />}
</div>
</div>
{/* right column: flags — show 2, then +N */}
{flags.length > 0 && (
<div style={{
display: 'flex', flexDirection: 'column-reverse',
gap: 4, alignItems: 'flex-end', justifyContent: 'flex-start',
}}>
<FlagDots flags={flags} size={26} maxShow={2} />
</div>
)}
</div>
)
}
// 4x1 — full width horizontal. Name + amount left-center, badges (up to 3 + +N) + status right.
function Card4x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
return (
<div style={{
width: '100%', height: 68,
background: cfg.cardBg, borderRadius: 14,
padding: '12px 14px',
display: 'flex', alignItems: 'center', gap: 14, overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
}}>
{/* name */}
<div style={{
fontWeight: 800, fontSize: 'clamp(20px, 4.5vw, 28px)',
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1, flexShrink: 0,
}}>
{table.label || `T${table.number}`}
</div>
{/* separator dot */}
<span style={{ color: cfg.nameText, opacity: 0.3, fontSize: 20, lineHeight: 1, flexShrink: 0 }}>·</span>
{/* amount */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
{showAmount && <Amount value={total} size={'clamp(20px, 4.5vw, 28px)'} color={cfg.nameText} />}
</div>
{/* flags up to 3 + +N */}
{flags.length > 0 && (
<FlagDots flags={flags} size={24} maxShow={3} />
)}
{/* status */}
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} />
</div>
)
}
// 4x2 — full width, tall. One main row: name+zone left, status center, amount+flags right. Flag chips below. Waiter footer.
function Card4x2({ table, order, flags, waiterObjects, groupName, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
const showWaiters = !isFree && waiterObjects.length > 0
return (
<div style={{
width: '100%',
background: cfg.cardBg, borderRadius: 16,
overflow: 'hidden',
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
display: 'flex', flexDirection: 'column',
}}>
{/* main body */}
<div style={{ padding: '14px 14px 12px', display: 'flex', flexDirection: 'column', gap: 10 }}>
{/* top row: name LEFT | status CENTER | amount RIGHT — all top-aligned */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
{/* left: name + zone */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontWeight: 800, fontSize: 'clamp(30px, 7vw, 44px)',
letterSpacing: -1.5, lineHeight: 1, color: cfg.nameText,
}}>
{table.label || `T${table.number}`}
</div>
{groupName && (
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: 0.8,
color: cfg.nameText, opacity: 0.6,
textTransform: 'uppercase', marginTop: 3,
}}>
{groupName}
</div>
)}
</div>
{/* center: status pill — top-aligned via paddingTop to optically align with name cap */}
<div style={{ paddingTop: 4, flexShrink: 0 }}>
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} />
</div>
{/* right: amount — top-aligned */}
{showAmount && (
<div style={{ flexShrink: 0 }}>
<Amount value={total} size={'clamp(30px, 7vw, 44px)'} color={cfg.nameText} />
</div>
)}
</div>
{/* flag chips row — right-aligned */}
{flags.length > 0 && (
<div style={{ display: 'flex', justifyContent: 'flex-end', flexWrap: 'wrap', gap: 6 }}>
{flags.slice(0, 4).map(f => <FlagChip key={f.id} flag={f} />)}
{flags.length > 4 && (
<div style={{
height: 26, padding: '0 9px', borderRadius: 13,
background: 'rgba(0,0,0,0.18)', color: '#fff',
fontSize: 11, fontWeight: 800,
display: 'flex', alignItems: 'center',
}}>+{flags.length - 4}</div>
)}
</div>
)}
</div>
{/* footer: waiters */}
<div style={{
borderTop: `1px solid ${cfg.nameText}22`,
padding: '10px 14px', minHeight: 40,
display: 'flex', alignItems: 'center',
}}>
{showWaiters
? <WaiterRow waiters={waiterObjects} size={24} cfg={cfg} />
: <span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.45 }}></span>
}
</div>
</div>
)
}
// 4x3 — full width, two-column detail card. Left: name/zone/status/amount. Right: order items list. Footer: waiters.
function Card4x3({ table, order, flags, waiterObjects, groupName, cfg, statusKey }) {
const isFree = !order
const activeItems = order?.items?.filter(i => i.status === 'active') ?? []
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
const showWaiters = !isFree && waiterObjects.length > 0
return (
<div style={{
width: '100%',
background: cfg.cardBg, borderRadius: 16,
overflow: 'hidden',
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
display: 'flex', flexDirection: 'column',
}}>
<div style={{ display: 'flex', padding: '14px 14px 10px', gap: 14, minWidth: 0, overflow: 'hidden' }}>
{/* left column: name, zone, amount, status, flags */}
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 100, flexShrink: 0, justifyContent: 'space-between' }}>
<div>
<div style={{
fontWeight: 800, fontSize: 'clamp(28px, 6vw, 40px)',
letterSpacing: -1.5, lineHeight: 1, color: cfg.nameText,
}}>
{table.label || `T${table.number}`}
</div>
{groupName && (
<div style={{
fontSize: 10, fontWeight: 700, letterSpacing: 0.8,
color: cfg.nameText, opacity: 0.6,
textTransform: 'uppercase', marginTop: 3,
}}>
{groupName}
</div>
)}
</div>
<div style={{ marginTop: 10 }}>
{!isFree && <Amount value={total} size={'clamp(22px, 5vw, 32px)'} color={cfg.nameText} />}
</div>
<div style={{ marginTop: 8 }}>
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
</div>
{flags.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<FlagDots flags={flags} size={22} maxShow={3} />
</div>
)}
</div>
{/* divider */}
<div style={{ width: 1, background: `${cfg.nameText}20`, alignSelf: 'stretch', flexShrink: 0 }} />
{/* right column: order items */}
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
{isFree ? (
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.35 }}>Ελεύθερο</span>
</div>
) : activeItems.length === 0 ? (
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.35 }}>Κανένα είδος</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, minWidth: 0 }}>
{activeItems.slice(0, 7).map(item => (
<div key={item.id} style={{ display: 'flex', alignItems: 'baseline', gap: 5, overflow: 'hidden', minWidth: 0 }}>
<span style={{
fontSize: 11, fontWeight: 700, color: cfg.nameText,
background: `${cfg.nameText}18`, borderRadius: 3,
padding: '1px 5px', flexShrink: 0,
}}>{item.quantity}×</span>
<span style={{
fontSize: 12, fontWeight: 500, color: cfg.nameText,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
}}>{item.product?.name || `#${item.product_id}`}</span>
<span style={{ fontSize: 11, fontWeight: 700, color: cfg.nameText, opacity: 0.7, flexShrink: 0 }}>
{(item.unit_price * item.quantity).toFixed(2)}
</span>
</div>
))}
{activeItems.length > 7 && (
<div style={{ fontSize: 11, color: cfg.nameText, opacity: 0.5, marginTop: 2 }}>
+{activeItems.length - 7} ακόμα
</div>
)}
</div>
)}
</div>
</div>
{/* footer: waiters */}
<div style={{
borderTop: `1px solid ${cfg.nameText}22`,
padding: '10px 14px', minHeight: 38,
display: 'flex', alignItems: 'center',
}}>
{showWaiters
? <WaiterRow waiters={waiterObjects} size={22} cfg={cfg} />
: <span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.45 }}></span>
}
</div>
</div>
)
}
// ─── Main export ──────────────────────────────────────────────────────────────
export default function TableCard({
table,
order,
isMine,
flags = [],
groupName = '',
waiterObjects = [],
density = '2x2',
onClick,
onLongPress,
}) {
const holdTimer = useRef(null)
const startPos = useRef({ x: 0, y: 0 })
const didFire = useRef(false)
const [showTip, setShowTip] = useState(false)
const dark = useThemeStore(s => s.dark)
const colours = useTableColourStore(s => s.colours)
let statusKey = 'free'
if (order?.status === 'paid') statusKey = 'paid'
else if (order?.status === 'partially_paid') statusKey = 'partially_paid'
else if (order && isMine) statusKey = 'mine'
else if (order) statusKey = 'open'
const mode = dark ? 'dark' : 'light'
const cfg = colours[mode][statusKey]
function cancel() {
clearTimeout(holdTimer.current)
holdTimer.current = null
}
function onTouchStart(e) {
const t = e.touches[0]
startPos.current = { x: t.clientX, y: t.clientY }
didFire.current = false
holdTimer.current = setTimeout(() => {
didFire.current = true
if (onLongPress) onLongPress()
else setShowTip(true)
}, HOLD_MS)
}
function onTouchMove(e) {
if (!holdTimer.current) return
const t = e.touches[0]
const dx = Math.abs(t.clientX - startPos.current.x)
const dy = Math.abs(t.clientY - startPos.current.y)
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) cancel()
}
function onTouchEnd() { cancel(); setShowTip(false) }
function onMouseDown(e) {
startPos.current = { x: e.clientX, y: e.clientY }
didFire.current = false
holdTimer.current = setTimeout(() => {
didFire.current = true
if (onLongPress) onLongPress()
else setShowTip(true)
}, HOLD_MS)
}
function onMouseMove(e) {
if (!holdTimer.current) return
const dx = Math.abs(e.clientX - startPos.current.x)
const dy = Math.abs(e.clientY - startPos.current.y)
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) cancel()
}
function onMouseUp() { cancel(); setShowTip(false) }
function onMouseLeave() { cancel(); setShowTip(false) }
function handleClick(e) {
if (didFire.current) { e.preventDefault(); return }
onClick?.()
}
const cardProps = { table, order, flags, waiterObjects, groupName, cfg, statusKey }
const CardComponent = {
'1x1': Card1x1,
'2x1': Card2x1,
'2x2': Card2x2,
'4x1': Card4x1,
'4x2': Card4x2,
'4x3': Card4x3,
}[density] || Card2x2
return (
<div style={{ position: 'relative', minWidth: 0, overflow: 'hidden' }}>
<button
style={{ display: 'block', width: '100%', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
onClick={handleClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
<CardComponent {...cardProps} />
</button>
{showTip && flags.length > 0 && (
<div style={{
position: 'absolute', bottom: 'calc(100% + 8px)', right: 0,
background: 'var(--bg2)', border: '1px solid var(--border)',
borderRadius: 10, padding: '8px 12px', zIndex: 50,
boxShadow: '0 4px 16px var(--shadow)',
minWidth: 160, pointerEvents: 'none',
}}>
{flags.map(f => (
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }}>
<span style={{ fontSize: 15 }}>{f.emoji || '🏷️'}</span>
<span style={{ fontSize: 13, color: 'var(--text)' }}>{f.name}</span>
</div>
))}
</div>
)}
</div>
)
}