SkateSpots/src/components/PhotoDropzone.tsx
SkateSpots 09735375c2 feat(i18n): Spanish (Castellano) — infra + Map/Spots, Marketplace, Lab (batch 1)
- First-visit language auto-detect (Spanish devices land in es; stored choice wins).
- Sasquatch AI replies in the app language (skate trick names kept English).
- i18n safety net: scripts/check-i18n.mjs wired into npm test — fails on any t()
  key missing from en/es or any en/es key drift.
- ~1,680 new keys (2,297 total) across mapUi / spotUi / marketUi / labUi and
  ~65 files wired to t(): Map, spots (detail/create/edit/chat), marketplace,
  trades, offers, escrow, shipping, The Lab, /become/* forms, coaches, skaters.
- Policy: only skate TRICK names stay English; user-generated content untouched.
2026-10-01 23:57:20 +07:00

104 lines
3.4 KiB
TypeScript

import { useState } from 'react';
import { Loader2, Upload } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/contexts/language';
interface PhotoDropzoneProps {
/** Called with the image files the user picked or dropped. */
onFiles: (files: File[]) => void | Promise<void>;
/** Show a spinner and block input while an upload is in flight. */
uploading?: boolean;
/** Disable interaction (e.g. while the form is submitting). */
disabled?: boolean;
/** Allow more than one file at a time. Defaults to true. */
multiple?: boolean;
/** Primary tile label. */
label?: string;
/** Secondary line under the label (hidden while dragging). */
hint?: string;
/** Wrapper classes — pass sizing such as `aspect-video` here. */
className?: string;
/** Accessible name for the file input. Defaults to `label`. */
inputLabel?: string;
}
const IMAGE_RE = /^image\//;
/**
* A drop target for images that also works as click-to-browse.
*
* Users can select or drop several photos at once, instead of adding them one
* at a time. Collecting the files is all this does — uploading is the caller's
* responsibility via `onFiles`, so each screen keeps its own toasts/progress.
*/
export function PhotoDropzone({
onFiles,
uploading = false,
disabled = false,
multiple = true,
label,
hint,
className,
inputLabel,
}: PhotoDropzoneProps) {
const { t } = useTranslation();
const [dragging, setDragging] = useState(false);
const busy = uploading || disabled;
const resolvedLabel = label ?? t('spotUi.addPhotos');
const resolvedHint = hint ?? t('spotUi.dragDropOrClick');
const emit = (fileList: FileList | null) => {
if (!fileList) return;
const files = Array.from(fileList).filter((f) => IMAGE_RE.test(f.type));
if (files.length > 0) void onFiles(files);
};
return (
<label
onDragEnter={(e) => {
e.preventDefault();
if (!busy) setDragging(true);
}}
onDragOver={(e) => {
e.preventDefault();
if (!busy) setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
if (!busy) emit(e.dataTransfer.files);
}}
className={cn(
'flex flex-col items-center justify-center rounded-lg border-2 border-dashed px-2 text-center cursor-pointer transition-colors',
dragging ? 'border-[#ccff00] bg-[#ccff00]/10' : 'border-gray-600 hover:bg-muted/50 hover:border-gray-500',
busy && 'opacity-60 cursor-not-allowed',
className,
)}
>
<input
type="file"
accept="image/*"
multiple={multiple}
className="hidden"
disabled={busy}
aria-label={inputLabel ?? resolvedLabel}
onChange={(e) => {
emit(e.target.files);
e.target.value = '';
}}
/>
{busy ? (
<Loader2 className="h-6 w-6 animate-spin text-[#ccff00]" />
) : (
<>
<Upload className={cn('h-6 w-6 mb-1', dragging ? 'text-[#ccff00]' : 'text-muted-foreground')} />
<span className={cn('text-xs font-bold', dragging ? 'text-[#ccff00]' : 'text-muted-foreground')}>
{dragging ? t('spotUi.dropPhotos') : resolvedLabel}
</span>
{resolvedHint && <span className="mt-0.5 text-[10px] text-muted-foreground/80">{resolvedHint}</span>}
</>
)}
</label>
);
}