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:
1 parent
e875b44f92
commit
6100a7af57
2 files changed
+74
-10
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useRef, useState } from '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 {
|
||||
label?: string;
|
||||
@@ -27,16 +27,13 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadImage = async (file: File) => {
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const small = await shrinkImage(file);
|
||||
onChange(await uploadFile(small, small.name));
|
||||
} catch (err: any) {
|
||||
alert(`Chyba pri nahrávaní obrázka: ${err.message}`);
|
||||
} catch (err) {
|
||||
alert(`Chyba pri nahrávaní obrázka: ${(err as Error).message}`);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
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 (
|
||||
<div className="space-y-1.5">
|
||||
{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"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onPaste={handlePaste}
|
||||
onBlur={(e) => checkUrl(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 min-w-0 text-xs font-mono border border-gray-300 rounded-lg p-2 bg-white"
|
||||
/>
|
||||
|
||||
+31
-3
@@ -21,10 +21,10 @@ export async function uploadFile(file: Blob, name: string): Promise<string> {
|
||||
return data.url;
|
||||
}
|
||||
|
||||
// Zmenšenie veľkej fotky pred nahratím: v e-maile má obrázok najviac 600 px (×2 pre ostré displeje),
|
||||
// tlačové podklady s tisíckami px a desiatkami MB sú zbytočné (a bucket berie najviac 10 MB).
|
||||
// Zmenšenie veľkej fotky pred nahratím: v e-maile má obrázok najviac 600 px, dlhšia strana 1000 px stačí
|
||||
// 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.
|
||||
const MAX_SIDE = 2000;
|
||||
const MAX_SIDE = 1000;
|
||||
const MAX_BYTES = 9 * 1024 * 1024;
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user