feat: obrazky sa zmensuju na 1000 px aj pri vlozeni URL a zo schranky

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub_HostacnyandClaude Opus 5.5 committed 2026-10-21 13:45:01 +02:00
1 parent e875b44f92
commit 6100a7af57
2 files changed
+74 -10

No files matched your search

+43 -7
View File
@@ -2,7 +2,7 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { Upload, Loader2, Image as ImageIcon } from 'lucide-react'; import { Upload, Loader2, Image as ImageIcon } from 'lucide-react';
import { shrinkImage, uploadFile } from '@/lib/upload'; import { shrinkImage, shrinkRemoteImage, uploadFile } from '@/lib/upload';
interface ImageUploadInputProps { interface ImageUploadInputProps {
label?: string; label?: string;
@@ -27,16 +27,13 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const uploadImage = async (file: File) => {
const file = e.target.files?.[0];
if (!file) return;
setIsUploading(true); setIsUploading(true);
try { try {
const small = await shrinkImage(file); const small = await shrinkImage(file);
onChange(await uploadFile(small, small.name)); onChange(await uploadFile(small, small.name));
} catch (err: any) { } catch (err) {
alert(`Chyba pri nahrávaní obrázka: ${err.message}`); alert(`Chyba pri nahrávaní obrázka: ${(err as Error).message}`);
} finally { } finally {
setIsUploading(false); setIsUploading(false);
if (fileInputRef.current) { if (fileInputRef.current) {
@@ -45,6 +42,43 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
} }
}; };
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) uploadImage(file);
};
// Vložená URL: príliš veľký obrázok sa zmenší a uloží k nám (posledná spracovaná adresa, aby sa nerobilo dvakrát)
const lastCheckedUrl = useRef('');
const checkUrl = async (url: string) => {
url = url.trim();
if (!url || url === lastCheckedUrl.current) return;
lastCheckedUrl.current = url;
setIsUploading(true);
try {
const smaller = await shrinkRemoteImage(url);
if (smaller) {
lastCheckedUrl.current = smaller;
onChange(smaller);
}
} catch (err) {
console.warn('Obrázok z URL sa nepodarilo zmenšiť:', err);
} finally {
setIsUploading(false);
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
// obrázok zo schránky (Ctrl+V) → nahrá sa ako súbor
const file = [...e.clipboardData.files].find((f) => f.type.startsWith('image/'));
if (file) {
e.preventDefault();
uploadImage(file);
return;
}
const text = e.clipboardData.getData('text');
if (/^https?:\/\//i.test(text.trim())) setTimeout(() => checkUrl(text), 0);
};
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
{label && <label className="block text-[11px] font-semibold text-gray-700">{label}</label>} {label && <label className="block text-[11px] font-semibold text-gray-700">{label}</label>}
@@ -71,6 +105,8 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
type="text" type="text"
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
onPaste={handlePaste}
onBlur={(e) => checkUrl(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
className="flex-1 min-w-0 text-xs font-mono border border-gray-300 rounded-lg p-2 bg-white" className="flex-1 min-w-0 text-xs font-mono border border-gray-300 rounded-lg p-2 bg-white"
/> />
+31 -3
View File
@@ -21,10 +21,10 @@ export async function uploadFile(file: Blob, name: string): Promise<string> {
return data.url; return data.url;
} }
// Zmenšenie veľkej fotky pred nahratím: v e-maile má obrázok najviac 600 px (×2 pre ostré displeje), // Zmenšenie veľkej fotky pred nahratím: v e-maile má obrázok najviac 600 px, dlhšia strana 1000 px stačí
// tlačové podklady s tisíckami px a desiatkami MB sú zbytočné (a bucket berie najviac 10 MB). // aj na ostré displeje – tlačové podklady s tisíckami px a desiatkami MB sú zbytočné (bucket berie najviac 10 MB).
// PNG ostáva PNG kvôli priehľadnosti; SVG a GIF (animácie) sa nechávajú tak. // PNG ostáva PNG kvôli priehľadnosti; SVG a GIF (animácie) sa nechávajú tak.
const MAX_SIDE = 2000; const MAX_SIDE = 1000;
const MAX_BYTES = 9 * 1024 * 1024; const MAX_BYTES = 9 * 1024 * 1024;
export async function shrinkImage(file: File): Promise<File> { export async function shrinkImage(file: File): Promise<File> {
@@ -54,3 +54,31 @@ export async function shrinkImage(file: File): Promise<File> {
const name = type === 'image/jpeg' ? file.name.replace(/\.\w+$/, '.jpg') : file.name; const name = type === 'image/jpeg' ? file.name.replace(/\.\w+$/, '.jpg') : file.name;
return new File([blob], name, { type }); return new File([blob], name, { type });
} }
// Je adresa už v našom úložisku?
export function isOwnStorageUrl(url: string): boolean {
return url.includes('/storage/v1/object/public/newsletter-assets/');
}
// Obrázok vložený cez URL: stiahne sa (priamo, ak to cudzí server dovolí, inak cez /api/img),
// a ak je väčší ako 1000 px, zmenší sa a nahrá do úložiska. Vráti novú adresu, alebo null, ak netreba nič meniť.
export async function shrinkRemoteImage(url: string): Promise<string | null> {
if (!/^https?:\/\//i.test(url) || isOwnStorageUrl(url)) return null;
let blob: Blob | null = null;
try {
const res = await fetch(url);
if (res.ok) blob = await res.blob();
} catch {
// CORS – skúsime cez náš proxy
}
if (!blob || !blob.type.startsWith('image/')) {
const res = await fetch(`/api/img?url=${encodeURIComponent(url)}`);
if (!res.ok) return null;
blob = await res.blob();
}
const name = decodeURIComponent(new URL(url).pathname.split('/').pop() || '') || 'obrazok.png';
const file = new File([blob], name, { type: blob.type });
const small = await shrinkImage(file);
if (small === file) return null;
return uploadFile(small, small.name);
}