/
dmitriysturov
/
workspaces_platform
Обзор
Документация
Войти
/
dmitriysturov
/
workspaces_platform
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/src/components/EntityImage.tsx
167 строк
6 KB
dmitriysturov
vkr
04 июн 2026, 22:24
04 июн 2026, 22:24
971dfdc
Код
Авторство
О чём код?
import { type ReactNode, useEffect, useRef, useState } from 'react' import { resourceImageSrc } from '../utils/resourceImage' import { Button } from './ui/Button' type EntityImageProps = { imageUrl?: string | null fallbackIcon: ReactNode label: string className?: string } type EntityImageFieldProps = EntityImageProps & { canEdit: boolean isSubmitting: boolean onUpload: (file: File) => Promise<unknown> onDelete: () => Promise<unknown> } const MAX_IMAGE_SIZE = 5 * 1024 * 1024 const ALLOWED_TYPES = ['image/png', 'image/jpeg', 'image/webp'] export function EntityImage({ imageUrl, fallbackIcon, label, className }: EntityImageProps) { const src = resourceImageSrc(imageUrl) return ( <span className={['entity-image', className].filter(Boolean).join(' ')} aria-label={label}> {src ? <img src={src} alt="" /> : fallbackIcon} </span> ) } export function EntityImageField({ imageUrl, fallbackIcon, label, canEdit, isSubmitting, onUpload, onDelete }: EntityImageFieldProps) { const [previewUrl, setPreviewUrl] = useState<string | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null) const [error, setError] = useState<string | null>(null) const inputRef = useRef<HTMLInputElement | null>(null) useEffect(() => { return () => { if (previewUrl) { URL.revokeObjectURL(previewUrl) } } }, [previewUrl]) function resetSelection() { if (previewUrl) { URL.revokeObjectURL(previewUrl) } setPreviewUrl(null) setSelectedFile(null) if (inputRef.current) { inputRef.current.value = '' } } async function handleSelect(file?: File | null) { setError(null) resetSelection() if (!file) { return } if (!ALLOWED_TYPES.includes(file.type)) { setError('Загрузите изображение в формате PNG, JPG или WEBP.') return } if (file.size > MAX_IMAGE_SIZE) { setError('Изображение должно быть не больше 5 МБ.') return } const objectUrl = URL.createObjectURL(file) try { const size = await readImageSize(objectUrl) if (size.width !== size.height) { URL.revokeObjectURL(objectUrl) setError('Изображение должно быть квадратным.') return } setPreviewUrl(objectUrl) setSelectedFile(file) } catch { URL.revokeObjectURL(objectUrl) setError('Не удалось прочитать изображение.') } } async function handleUpload() { if (!selectedFile) { return } try { setError(null) await onUpload(selectedFile) resetSelection() } catch (err) { setError(err instanceof Error ? err.message : 'Не удалось сохранить изображение.') } } async function handleDelete() { try { setError(null) await onDelete() resetSelection() } catch (err) { setError(err instanceof Error ? err.message : 'Не удалось удалить изображение.') } } return ( <section className="settings-block entity-image-block"> <header className="section-header"> <div> <h3>Изображение</h3> <p className="muted">Загрузите квадратное изображение в формате PNG, JPG или WEBP.</p> </div> </header> <div className="entity-image-editor"> <EntityImage imageUrl={previewUrl || imageUrl} fallbackIcon={fallbackIcon} label={label} className="entity-image-large" /> <div className="entity-image-controls"> <input ref={inputRef} type="file" accept="image/png,image/jpeg,image/webp" hidden onChange={(event) => void handleSelect(event.currentTarget.files?.[0])} /> {canEdit ? ( <div className="form-actions"> <Button type="button" variant="secondary" disabled={isSubmitting} onClick={() => inputRef.current?.click()}> Загрузить изображение </Button> {selectedFile && ( <Button type="button" disabled={isSubmitting} onClick={() => void handleUpload()}> Сохранить изображение </Button> )} {selectedFile && ( <Button type="button" variant="ghost" disabled={isSubmitting} onClick={resetSelection}> Отмена </Button> )} {imageUrl && !selectedFile && ( <Button type="button" variant="ghost" disabled={isSubmitting} onClick={() => void handleDelete()}> Удалить изображение </Button> )} </div> ) : ( <p className="compact-empty">У вас нет прав на изменение изображения.</p> )} {selectedFile && <p className="muted">Предпросмотр готов. Сохраните изображение, чтобы применить его.</p>} {error && <div className="form-error">{error}</div>} </div> </div> </section> ) } function readImageSize(src: string) { return new Promise<{ width: number; height: number }>((resolve, reject) => { const image = new Image() image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight }) image.onerror = reject image.src = src }) }