- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries - complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables - add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels - update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS - standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
192 lines
No EOL
7.5 KiB
TypeScript
192 lines
No EOL
7.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { OrderItem, PROBLEM_TYPES } from '@/lib/types';
|
|
import { cn } from '@/lib/utils';
|
|
import { X, Camera, AlertTriangle } from 'lucide-react';
|
|
|
|
interface ProblemModalProps {
|
|
item: OrderItem | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConfirm: (item: OrderItem, problemType: string, description: string, photo?: File) => void;
|
|
}
|
|
|
|
export default function ProblemModal({
|
|
item,
|
|
isOpen,
|
|
onClose,
|
|
onConfirm,
|
|
}: ProblemModalProps) {
|
|
const [problemType, setProblemType] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [photo, setPhoto] = useState<File | null>(null);
|
|
const [error, setError] = useState('');
|
|
|
|
if (!isOpen || !item) return null;
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!problemType) {
|
|
setError('Please select a problem type');
|
|
return;
|
|
}
|
|
|
|
if (!description.trim()) {
|
|
setError('Please describe the problem');
|
|
return;
|
|
}
|
|
|
|
onConfirm(item, problemType, description, photo || undefined);
|
|
setProblemType('');
|
|
setDescription('');
|
|
setPhoto(null);
|
|
setError('');
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
|
<div className="absolute inset-0 bg-black/50 modal-overlay" onClick={onClose} />
|
|
<div className="relative bg-[#e6e5e0] bottom-sheet w-full max-w-md flex flex-col max-h-[calc(100dvh-1rem)] sm:max-h-[85vh] border border-[rgba(38,37,30,0.1)] animate-slide-up">
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="flex flex-col min-h-0 flex-1"
|
|
>
|
|
{/* Drag indicator */}
|
|
<div className="sm:hidden flex justify-center pt-2 pb-1">
|
|
<div className="w-10 h-1 bg-[rgba(38,37,30,0.2)] rounded-full" />
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between px-6 pt-2 pb-5">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 bg-[#fbeaef] rounded-xl flex items-center justify-center">
|
|
<AlertTriangle size={18} className="text-[#b3224a]" />
|
|
</div>
|
|
<h2 className="text-lg font-bold text-[#26251e]">Report Problem</h2>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Close"
|
|
className="p-2 rounded-xl hover:bg-[rgba(38,37,30,0.06)] active:bg-[rgba(38,37,30,0.12)] transition-colors"
|
|
>
|
|
<X size={18} className="text-[rgba(38,37,30,0.55)]" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Scrollable body */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-6 pb-6 space-y-5">
|
|
{/* Item info */}
|
|
<div className="p-4 bg-[#fbeaef] border border-[#f3c4d2] rounded-xl">
|
|
<p className="text-sm font-semibold text-[#26251e] truncate">{item.description}</p>
|
|
<p className="text-xs text-[rgba(38,37,30,0.55)] mt-1">
|
|
{item.expand?.supplier?.suppliername || 'Unknown Supplier'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Problem type */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-[#26251e] mb-2">
|
|
What went wrong? *
|
|
</label>
|
|
<div className="space-y-2">
|
|
{PROBLEM_TYPES.map((type) => (
|
|
<button
|
|
key={type.value}
|
|
type="button"
|
|
onClick={() => { setProblemType(type.value); setError(''); }}
|
|
aria-pressed={problemType === type.value}
|
|
className={cn(
|
|
'w-full py-3 px-4 rounded-xl text-sm text-left border-2 transition-all',
|
|
'touch-manipulation press-feedback-sm',
|
|
problemType === type.value
|
|
? 'border-[#cf2d56] bg-[#fbeaef] text-[#8a1b3a] font-semibold'
|
|
: 'border-[rgba(38,37,30,0.1)] bg-[#f7f7f4] text-[#26251e] active:border-[rgba(38,37,30,0.2)]'
|
|
)}
|
|
>
|
|
{type.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label htmlFor="problem-details" className="block text-sm font-semibold text-[#26251e] mb-2">
|
|
Details *
|
|
</label>
|
|
<textarea
|
|
id="problem-details"
|
|
value={description}
|
|
onChange={(e) => { setDescription(e.target.value); setError(''); }}
|
|
className="w-full p-3 text-sm bg-[#f2f1ed] border border-[rgba(38,37,30,0.1)] rounded-xl text-[#26251e] placeholder:text-[rgba(38,37,30,0.4)] focus:ring-2 focus:ring-[#f54e00]/30 focus:border-[#f54e00] focus:bg-[#f7f7f4] transition-all resize-none selectable"
|
|
rows={3}
|
|
placeholder="Describe the problem in detail..."
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
{/* Photo */}
|
|
<div>
|
|
<label className="block text-sm font-semibold text-[#26251e] mb-2">
|
|
Photo (optional)
|
|
</label>
|
|
<label
|
|
htmlFor="problem-photo"
|
|
className={cn(
|
|
'flex items-center justify-center gap-2 py-3 px-4 rounded-xl border-2 border-dashed cursor-pointer',
|
|
'transition-all touch-manipulation press-feedback-sm',
|
|
photo
|
|
? 'border-[#1f8a65] bg-[#e6f1ec] text-[#125a3e]'
|
|
: 'border-[rgba(38,37,30,0.15)] bg-[#f2f1ed] text-[rgba(38,37,30,0.55)] active:border-[rgba(38,37,30,0.3)]'
|
|
)}
|
|
>
|
|
<Camera size={20} aria-hidden="true" />
|
|
<span className="text-sm font-medium">
|
|
{photo ? 'Photo attached' : 'Take or choose photo'}
|
|
</span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
capture="environment"
|
|
onChange={(e) => setPhoto(e.target.files?.[0] || null)}
|
|
className="hidden"
|
|
id="problem-photo"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="p-3 bg-[#fbeaef] border border-[#f3c4d2] rounded-xl animate-fade-in" role="alert">
|
|
<p className="text-xs text-[#8a1b3a] font-medium">{error}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pinned footer — sits above BottomNav (z-50). pb accounts for
|
|
the 4rem nav height + safe-area inset + visual gap so the
|
|
button never hides under the tab bar. */}
|
|
<div
|
|
className="px-6 pt-3 pb-[calc(4rem+env(safe-area-inset-bottom,0px)+0.75rem)] border-t border-[rgba(38,37,30,0.08)] bg-[#e6e5e0] flex-shrink-0"
|
|
style={{ boxShadow: '0 -8px 24px rgba(0,0,0,0.06)' }}
|
|
>
|
|
<button
|
|
type="submit"
|
|
className={cn(
|
|
'w-full py-4 px-4 rounded-xl text-white font-semibold text-[15px]',
|
|
'bg-[#cf2d56] active:bg-[#8a1b3a] transition-all',
|
|
'touch-manipulation press-feedback min-h-[56px]'
|
|
)}
|
|
style={{ boxShadow: '0 4px 12px rgba(207,45,86,0.3)' }}
|
|
>
|
|
Report Problem
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |