// Shared components for ResDri driver app. // All RTL-aware. Theme via CSS vars. // ──────────────────────────────────────────────────────────── // Iconography — single inline SVG factory. Solid for active, outline otherwise. // ──────────────────────────────────────────────────────────── function Icon({ name, size = 22, color = 'currentColor', strokeWidth = 2 }) { const props = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' }; const fillProps = { width: size, height: size, viewBox: '0 0 24 24', fill: color }; switch (name) { case 'phone': return ; case 'message': return ; case 'chev-left': return ; case 'chev-right': return ; case 'chev-down': return ; case 'chev-up': return ; case 'close': return ; case 'check': return ; case 'check-circle': return ; case 'list': return ; case 'map': return ; case 'nav': return ; case 'walk': return ; case 'shop': return ; case 'package': return ; case 'food': return ; case 'cart': return ; case 'bike': return ; case 'wallet': return ; case 'shekel': return ; case 'whatsapp': return ; case 'lock': return ; case 'pin': return ; case 'camera': return ; case 'volume': return ; case 'clock': return ; case 'arrow-right': return ; case 'arrow-left': return ; case 'gear': return ; case 'bell': return ; default: return ; } } // ──────────────────────────────────────────────────────────── // Vehicle icon (used in order cards + chips) // ──────────────────────────────────────────────────────────── function VehicleIcon({ kind = 'car', size = 18, color = 'currentColor' }) { if (kind === 'bike' || kind === 'motorbike') { return ; } if (kind === 'bicycle') { return ; } // car (default) return ; } // ──────────────────────────────────────────────────────────── // Vertical icon — colored chip // ──────────────────────────────────────────────────────────── function VerticalChip({ kind = 'food', size = 36 }) { const colors = { food: { bg: '#FEE2E2', fg: '#E11D48' }, parcel: { bg: '#DBEAFE', fg: '#2563EB' }, grocery: { bg: '#DCFCE7', fg: '#16A34A' }, }; const c = colors[kind]; const iconName = { food: 'food', parcel: 'package', grocery: 'cart' }[kind]; return (
); } // ──────────────────────────────────────────────────────────── // Big Number — used for payout / cash / etc. 3 sizes. // Always LTR, tabular, with ₪ glyph in proper position. // ──────────────────────────────────────────────────────────── function BigNumber({ value, size = 'md', currency = '₪', accent = false, color, weight = 800 }) { const sizes = { sm: { num: 24, sym: 16, gap: 3 }, md: { num: 40, sym: 22, gap: 4 }, lg: { num: 64, sym: 32, gap: 6 }, xl: { num: 96, sym: 44, gap: 8 }, }; const s = sizes[size] || sizes.md; const c = color || (accent ? 'var(--accent)' : 'var(--text)'); return ( {currency} {value} ); } // ──────────────────────────────────────────────────────────── // Status pill // ──────────────────────────────────────────────────────────── function StatusPill({ tone = 'neutral', children }) { const tones = { neutral: { bg: 'var(--surface-2)', fg: 'var(--text-2)', bd: 'var(--line)' }, success: { bg: 'color-mix(in oklch, var(--success) 14%, transparent)', fg: 'var(--success)', bd: 'color-mix(in oklch, var(--success) 30%, transparent)' }, warning: { bg: 'color-mix(in oklch, var(--warning) 14%, transparent)', fg: 'var(--warning)', bd: 'color-mix(in oklch, var(--warning) 30%, transparent)' }, danger: { bg: 'color-mix(in oklch, var(--danger) 14%, transparent)', fg: 'var(--danger)', bd: 'color-mix(in oklch, var(--danger) 30%, transparent)' }, primary: { bg: 'color-mix(in oklch, var(--primary) 14%, transparent)', fg: 'var(--primary)', bd: 'color-mix(in oklch, var(--primary) 30%, transparent)' }, accent: { bg: 'color-mix(in oklch, var(--accent) 18%, transparent)', fg: 'var(--accent-700)', bd: 'color-mix(in oklch, var(--accent) 40%, transparent)' }, }; const t = tones[tone] || tones.neutral; return ( {children} ); } // ──────────────────────────────────────────────────────────── // Order card — new scannable, action-oriented variant. // Layout (RTL): // [VerticalIcon] [Business · pickup→dropoff with distance · time/efficiency] [payout BIG + bonus] // [قبول inline button] // Optional `featured` adds a teal highlight + "أفضل خيار" ribbon. // ──────────────────────────────────────────────────────────── function OrderCard({ order, onClick, onAccept, compatible = true, featured = false }) { const verticalLabel = { food: 'طعام', parcel: 'طرد', grocery: 'بقالة' }[order.kind]; const totalPay = order.payout + (order.bonus || 0); const perKm = (totalPay / Math.max(0.5, order.distance)).toFixed(0); return (
{/* Featured ribbon */} {featured && (
أفضل خيار
)} {/* HEADER ROW: business + payout (no third column) */}
{order.business}
{verticalLabel} · ~{order.eta} د · {order.distance.toFixed(1)} كم
₪{perKm}/كم
{/* ROUTE — two stacked rows: pickup (from your location), dropoff (from restaurant) */}
{/* Pickup row */}
{order.pickup}
{order.toPickup?.toFixed(1) ?? '—'} كم من موقعك
{/* Connector */}
{/* Dropoff row */}
{order.dropoff}
{order.toDropoff?.toFixed(1) ?? '—'} كم من المتجر
{/* BOTTOM ROW: bonus + accept button */}
{order.bonus > 0 && ( +₪{order.bonus} مكافأة )} {!compatible && غير متوافق} {order.notes && ( ملاحظة )}
); } // ──────────────────────────────────────────────────────────── // Action sheet — bottom anchored, used for active-order next-step. // ──────────────────────────────────────────────────────────── function ActionSheet({ children, padding = 16 }) { return (
{children}
); } // ──────────────────────────────────────────────────────────── // Big primary action button — glove-friendly, 56dp+ // ──────────────────────────────────────────────────────────── function PrimaryButton({ children, onClick, tone = 'primary', size = 'lg', disabled = false, leadingIcon, fullWidth = true }) { const tones = { primary: { bg: 'var(--primary)', fg: '#fff', sh: '0 6px 20px color-mix(in oklch, var(--primary) 28%, transparent)' }, success: { bg: 'var(--success)', fg: '#fff', sh: '0 6px 20px rgba(22,163,74,0.30)' }, danger: { bg: 'var(--danger)', fg: '#fff', sh: '0 6px 20px rgba(220,38,38,0.30)' }, accent: { bg: 'var(--accent)', fg: '#1A1300', sh: '0 6px 20px rgba(245,158,11,0.30)' }, ghost: { bg: 'var(--surface-2)', fg: 'var(--text)', sh: 'none', bd: '1px solid var(--line)' }, outline: { bg: 'transparent', fg: 'var(--text)', sh: 'none', bd: '1.5px solid var(--line-strong)' }, }; const t = tones[tone] || tones.primary; const h = size === 'lg' ? 60 : size === 'md' ? 52 : 44; return ( ); } // ──────────────────────────────────────────────────────────── // "Go online" lever toggle — custom, big // ──────────────────────────────────────────────────────────── function OnlineLever({ online, onToggle }) { return ( ); } // ──────────────────────────────────────────────────────────── // Slide-to-confirm — deliberate destructive/important action // ──────────────────────────────────────────────────────────── function SlideToConfirm({ label = 'اسحب للتأكيد', onConfirm, color = 'var(--success)', icon = 'check' }) { const trackRef = React.useRef(null); const [x, setX] = React.useState(0); const [done, setDone] = React.useState(false); const draggingRef = React.useRef(false); const startXRef = React.useRef(0); const startPosRef = React.useRef(0); const onStart = (e) => { if (done) return; draggingRef.current = true; const clientX = e.touches ? e.touches[0].clientX : e.clientX; startXRef.current = clientX; startPosRef.current = x; }; const onMove = (e) => { if (!draggingRef.current || done) return; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const track = trackRef.current; if (!track) return; const trackW = track.clientWidth; const maxX = trackW - 64; // thumb width + 8 padding // RTL: thumb starts on the right (x=maxX initially via CSS), user drags it LEFT. // We track displacement positively. let delta = startXRef.current - clientX; // positive when moving left let nx = startPosRef.current + delta; nx = Math.max(0, Math.min(maxX, nx)); setX(nx); if (nx >= maxX - 4) { setDone(true); draggingRef.current = false; setTimeout(() => onConfirm && onConfirm(), 200); } }; const onEnd = () => { if (!draggingRef.current) return; draggingRef.current = false; if (!done) setX(0); }; React.useEffect(() => { window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onEnd); window.addEventListener('touchmove', onMove, { passive: false }); window.addEventListener('touchend', onEnd); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onEnd); window.removeEventListener('touchmove', onMove); window.removeEventListener('touchend', onEnd); }; }); // In RTL, "right" is the leading side. The thumb starts on the right and travels left. return (
{/* Filled trail */}
{/* Label */}
{done ? 'تم' : label}
{/* Thumb — starts on the right (RTL leading), drags left */}
); } // ──────────────────────────────────────────────────────────── // Top "Today's earnings" summary bar (sits above map) // ──────────────────────────────────────────────────────────── function EarningsHeader({ today = 142, orders = 4, rating = 4.8 }) { return (
أرباح اليوم
طلبات {orders}
التقييم {rating}
); } // ──────────────────────────────────────────────────────────── // "Connecting" radar — used on incoming alert. Calm, dispatch-radio vibe. // ──────────────────────────────────────────────────────────── function RadarPulse({ size = 180, color = 'var(--primary)' }) { const wave = (delay) => ( ); return (
{[0, 0.6, 1.2].map(wave)}
); } Object.assign(window, { Icon, VehicleIcon, VerticalChip, BigNumber, StatusPill, OrderCard, ActionSheet, PrimaryButton, OnlineLever, SlideToConfirm, EarningsHeader, RadarPulse, });