turash/bugulma/frontend/components/ui/ImageUpload.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- Remove nested git repository from bugulma/frontend/.git
- Add all frontend files to main repository tracking
- Convert from separate frontend/backend repos to unified monorepo
- Preserve all frontend code and development history as tracked files
- Eliminate nested repository complexity for simpler development workflow

This creates a proper monorepo structure with frontend and backend
coexisting in the same repository for easier development and deployment.
2025-11-25 06:02:57 +01:00

118 lines
3.6 KiB
TypeScript

import React, { useRef, useState } from 'react';
import { useTranslation } from '@/hooks/useI18n.tsx';
import { Trash2, UploadCloud } from 'lucide-react';
import Button from '@/components/ui/Button.tsx';
import Spinner from '@/components/ui/Spinner.tsx';
interface ImageUploadProps {
value: string | null | undefined;
onChange: (value: string | null) => void;
className?: string;
}
const fileToDataUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
const ImageUpload: React.FC<ImageUploadProps> = ({ value, onChange, className }) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setIsLoading(true);
try {
const dataUrl = await fileToDataUrl(file);
onChange(dataUrl);
} catch (error) {
console.error('Error converting file to data URL', error);
onChange(null);
} finally {
setIsLoading(false);
}
}
};
const handleRemoveImage = () => {
onChange(null);
if (fileInputRef.current) {
fileInputRef.current.value = ''; // Reset file input
}
};
return (
<div className={className}>
<div
className="w-full aspect-[4/3] rounded-lg border-2 border-dashed flex items-center justify-center bg-muted/50 relative overflow-hidden cursor-pointer"
onClick={() => !isLoading && fileInputRef.current?.click()}
>
{isLoading ? (
<Spinner className="h-8 w-8 text-primary" />
) : value ? (
<img
src={value}
alt={t('imageUpload.logoAlt')}
className="max-h-full max-w-full object-contain"
loading="eager"
decoding="sync"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
) : (
<div className="text-center text-muted-foreground p-4">
<UploadCloud className="h-10 h-4 mb-2 mx-auto text-current w-10 w-4" />
<p className="font-medium">{t('imageUpload.dropzoneHint')}</p>
</div>
)}
</div>
<input
type="file"
id="image-upload-input"
name="image-upload-input"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/png, image/jpeg, image/svg+xml"
disabled={isLoading}
/>
<div className="flex items-center gap-2 mt-2">
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
>
<UploadCloud className="h-4 mr-2 text-current w-4" />
{value ? t('imageUpload.change') : t('imageUpload.upload')}
</Button>
{value && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10 p-2 h-auto shrink-0"
onClick={handleRemoveImage}
aria-label={t('imageUpload.remove')}
disabled={isLoading}
>
<Trash2 className="h-4 text-current w-4" />
</Button>
)}
</div>
</div>
);
};
export default ImageUpload;