Phase 10 Implementation: Bulk color system compliance across frontend Applied DESIGN_COLOR_RULES.md to 40 component and page files: - Indigo (3) → primary/secondary (primary actions) - Green (12) → tertiary (#00e639) (success/healthy status) - Red/Rose (20) → error (#ffb4ab) (destructive actions) - Amber/Sky (9) → primary/secondary (warnings/info) - Blue/Slate (various) → primary/design system colors (focus states) - Black → bg-surface-container-lowest (proper design color) Files updated: 40 components + pages Color replacements: 44+ instances Build status: ✓ Zero errors, compiled in 6.3s Test status: Pending (npm run test) All colors now follow DESIGN.md Industrial Precision system: ✅ Primary: #ffb781 (caution orange) ✅ Secondary: #c8c6c5 (gray) ✅ Tertiary: #00e639 (healthy green) ✅ Error: #ffb4ab (destructive red) ✅ Design system enforced across all UI/UX Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
import React, { useRef, useState } from 'react';
|
|
import { Camera, Upload, Loader2 } from 'lucide-react';
|
|
import { usePhotoUpload } from '@/hooks/usePhotoUpload';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
interface ItemPhotoUploadProps {
|
|
itemId: number;
|
|
onUploadSuccess: (photo: { thumbnail_url: string; full_url: string; uploaded_at: string }) => void;
|
|
onError: (errorMessage: string) => void;
|
|
}
|
|
|
|
export default function ItemPhotoUpload({
|
|
itemId,
|
|
onUploadSuccess,
|
|
onError,
|
|
}: ItemPhotoUploadProps) {
|
|
const { upload, isLoading, error } = usePhotoUpload();
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const cameraInputRef = useRef<HTMLInputElement>(null);
|
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
const toastIdRef = useRef<string | null>(null);
|
|
|
|
// Sync hook error to local state for display
|
|
React.useEffect(() => {
|
|
if (error) {
|
|
setLocalError(error);
|
|
}
|
|
}, [error]);
|
|
|
|
// Cleanup: dismiss pending toasts on unmount
|
|
React.useEffect(() => {
|
|
return () => {
|
|
if (toastIdRef.current) {
|
|
toast.dismiss(toastIdRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleFileSelect = async (file: File) => {
|
|
setLocalError(null);
|
|
|
|
if (!file) return;
|
|
|
|
toastIdRef.current = toast.loading('Uploading...');
|
|
|
|
try {
|
|
const photo = await upload(file, itemId);
|
|
toast.success('Photo uploaded successfully', { id: toastIdRef.current });
|
|
toastIdRef.current = null;
|
|
onUploadSuccess(photo);
|
|
|
|
// Reset file inputs
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
if (cameraInputRef.current) {
|
|
cameraInputRef.current.value = '';
|
|
}
|
|
} catch (err: any) {
|
|
const errorMsg = err.message || 'Upload failed';
|
|
setLocalError(errorMsg);
|
|
toast.error(errorMsg, { id: toastIdRef.current || undefined });
|
|
toastIdRef.current = null;
|
|
onError(errorMsg);
|
|
}
|
|
};
|
|
|
|
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = e.target.files;
|
|
if (files && files.length > 0) {
|
|
handleFileSelect(files[0]);
|
|
}
|
|
};
|
|
|
|
const handleCameraCapture = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = e.target.files;
|
|
if (files && files.length > 0) {
|
|
handleFileSelect(files[0]);
|
|
}
|
|
};
|
|
|
|
const triggerFileInput = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
const triggerCameraInput = () => {
|
|
cameraInputRef.current?.click();
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
{/* Hidden file inputs */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleFileInputChange}
|
|
className="sr-only"
|
|
aria-label="Upload photo from device"
|
|
/>
|
|
|
|
<input
|
|
ref={cameraInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
capture="environment"
|
|
onChange={handleCameraCapture}
|
|
className="sr-only"
|
|
aria-label="Capture photo with camera"
|
|
/>
|
|
|
|
{/* Button group */}
|
|
<div className="flex gap-2">
|
|
{/* File upload button */}
|
|
<button
|
|
onClick={triggerFileInput}
|
|
disabled={isLoading}
|
|
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-primary text-white rounded-none font-normal text-base transition-colors hover:bg-primary/90 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
aria-label="Upload photo"
|
|
>
|
|
{isLoading ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<Upload className="w-5 h-5" />
|
|
)}
|
|
<span>Upload</span>
|
|
</button>
|
|
|
|
{/* Camera button (mobile) */}
|
|
<button
|
|
onClick={triggerCameraInput}
|
|
disabled={isLoading}
|
|
className="flex items-center justify-center gap-2 flex-1 px-4 py-2.5 bg-border text-white rounded-none font-normal text-base transition-colors hover:bg-surface-bright disabled:opacity-60 disabled:cursor-not-allowed"
|
|
aria-label="Capture photo with camera"
|
|
>
|
|
{isLoading ? (
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
) : (
|
|
<Camera className="w-5 h-5" />
|
|
)}
|
|
<span>Camera</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Status messages */}
|
|
{isLoading && (
|
|
<div className="text-sm text-secondary">Uploading...</div>
|
|
)}
|
|
|
|
{localError && (
|
|
<div className="text-sm text-error">{localError}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|