feat: comprehensive update with Signal, Copilot, themes, and PDF features
## Signal Messaging Integration - Added SignalService for sending messages to drivers via Signal - SignalMessage model for tracking message history - Driver chat modal for real-time messaging - Send schedule via Signal (ICS + PDF attachments) ## AI Copilot - Natural language interface for VIP Coordinator - Capabilities: create VIPs, schedule events, assign drivers - Help and guidance for users - Floating copilot button in UI ## Theme System - Dark/light/system theme support - Color scheme selection (blue, green, purple, orange, red) - ThemeContext for global state - AppearanceMenu in header ## PDF Schedule Export - VIPSchedulePDF component for schedule generation - PDF settings (header, footer, branding) - Preview PDF in browser - Settings stored in database ## Database Migrations - add_signal_messages: SignalMessage model - add_pdf_settings: Settings model for PDF config - add_reminder_tracking: lastReminderSent for events - make_driver_phone_optional: phone field nullable ## Event Management - Event status service for automated updates - IN_PROGRESS/COMPLETED status tracking - Reminder tracking for notifications ## UI/UX Improvements - Driver schedule modal - Improved My Schedule page - Better error handling and loading states - Responsive design improvements ## Other Changes - AGENT_TEAM.md documentation - Seed data improvements - Ability factory updates - Driver profile page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
163
frontend/src/components/AppearanceMenu.tsx
Normal file
163
frontend/src/components/AppearanceMenu.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Settings, Sun, Moon, Monitor, Check } from 'lucide-react';
|
||||
import { useTheme, ThemeMode, ColorScheme } from '@/hooks/useTheme';
|
||||
|
||||
const modes: { value: ThemeMode; label: string; icon: typeof Sun }[] = [
|
||||
{ value: 'light', label: 'Light', icon: Sun },
|
||||
{ value: 'dark', label: 'Dark', icon: Moon },
|
||||
{ value: 'system', label: 'System', icon: Monitor },
|
||||
];
|
||||
|
||||
const colorSchemes: { value: ColorScheme; label: string; color: string }[] = [
|
||||
{ value: 'blue', label: 'Blue', color: 'bg-blue-500' },
|
||||
{ value: 'purple', label: 'Purple', color: 'bg-purple-500' },
|
||||
{ value: 'green', label: 'Green', color: 'bg-green-500' },
|
||||
{ value: 'orange', label: 'Orange', color: 'bg-orange-500' },
|
||||
];
|
||||
|
||||
interface AppearanceMenuProps {
|
||||
/** Compact mode for mobile - shows inline instead of dropdown */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function AppearanceMenu({ compact = false }: AppearanceMenuProps) {
|
||||
const { mode, colorScheme, resolvedTheme, setMode, setColorScheme } = useTheme();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const CurrentIcon = resolvedTheme === 'dark' ? Moon : Sun;
|
||||
|
||||
// Compact inline mode for mobile drawer
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Theme Mode */}
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Theme
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
{modes.map(({ value, label, icon: Icon }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setMode(value)}
|
||||
className={`flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||
mode === value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-accent text-foreground'
|
||||
}`}
|
||||
title={label}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="sr-only sm:not-sr-only">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Color Scheme */}
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Accent Color
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
{colorSchemes.map(({ value, label, color }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setColorScheme(value)}
|
||||
className={`relative w-8 h-8 rounded-full ${color} transition-transform hover:scale-110 ${
|
||||
colorScheme === value ? 'ring-2 ring-offset-2 ring-offset-card ring-foreground' : ''
|
||||
}`}
|
||||
title={label}
|
||||
aria-label={`${label} color scheme`}
|
||||
>
|
||||
{colorScheme === value && (
|
||||
<Check className="absolute inset-0 m-auto h-4 w-4 text-white" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Standard dropdown mode for header
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center justify-center w-9 h-9 rounded-lg hover:bg-accent transition-colors"
|
||||
aria-label="Appearance settings"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<CurrentIcon className="h-5 w-5 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-52 rounded-xl bg-popover border border-border shadow-elevated z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
|
||||
{/* Theme Mode Section */}
|
||||
<div className="p-3 border-b border-border">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Theme
|
||||
</p>
|
||||
<div className="flex gap-1">
|
||||
{modes.map(({ value, label, icon: Icon }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setMode(value)}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-2 rounded-lg text-xs font-medium transition-colors ${
|
||||
mode === value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted hover:bg-accent text-foreground'
|
||||
}`}
|
||||
title={label}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Color Scheme Section */}
|
||||
<div className="p-3">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Accent Color
|
||||
</p>
|
||||
<div className="flex gap-2 justify-between">
|
||||
{colorSchemes.map(({ value, label, color }) => (
|
||||
<button
|
||||
key={value}
|
||||
onClick={() => setColorScheme(value)}
|
||||
className={`relative w-9 h-9 rounded-full ${color} transition-all hover:scale-110 ${
|
||||
colorScheme === value
|
||||
? 'ring-2 ring-offset-2 ring-offset-popover ring-foreground scale-110'
|
||||
: ''
|
||||
}`}
|
||||
title={label}
|
||||
aria-label={`${label} color scheme`}
|
||||
>
|
||||
{colorScheme === value && (
|
||||
<Check className="absolute inset-0 m-auto h-4 w-4 text-white drop-shadow-sm" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user