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 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
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user