feat: hlasenie pod polom s URL obrazka (zmensene / v poriadku / za prihlasenim)

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:48:37 +02:00
1 parent 6100a7af57
commit 48fd82a77d
3 files changed
+48 -13

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ export async function GET(request: Request) {
const res = await fetch(target, { redirect: 'follow' }); const res = await fetch(target, { redirect: 'follow' });
const contentType = res.headers.get('content-type') || ''; const contentType = res.headers.get('content-type') || '';
if (!res.ok || !contentType.startsWith('image/')) { if (!res.ok || !contentType.startsWith('image/')) {
return Response.json({ error: `Obrázok sa nepodarilo načítať (${res.status}).` }, { status: 502 }); return Response.json({ error: `Obrázok sa nepodarilo načítať (${res.status}).`, upstreamStatus: res.status }, { status: 502 });
} }
const body = await res.arrayBuffer(); const body = await res.arrayBuffer();
if (body.byteLength > MAX_BYTES) { if (body.byteLength > MAX_BYTES) {
+30 -6
View File
@@ -49,19 +49,31 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
// Vložená URL: príliš veľký obrázok sa zmenší a uloží k nám (posledná spracovaná adresa, aby sa nerobilo dvakrát) // 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 lastCheckedUrl = useRef('');
const [urlNote, setUrlNote] = useState<{ tone: 'ok' | 'warn'; text: string } | null>(null);
const checkUrl = async (url: string) => { const checkUrl = async (url: string) => {
url = url.trim(); url = url.trim();
if (!url || url === lastCheckedUrl.current) return; if (!url || url === lastCheckedUrl.current) return;
lastCheckedUrl.current = url; lastCheckedUrl.current = url;
setIsUploading(true); setIsUploading(true);
setUrlNote(null);
try { try {
const smaller = await shrinkRemoteImage(url); const result = await shrinkRemoteImage(url);
if (smaller) { if (result.status === 'shrunk') {
lastCheckedUrl.current = smaller; lastCheckedUrl.current = result.url;
onChange(smaller); onChange(result.url);
setUrlNote({ tone: 'ok', text: 'Obrázok bol väčší ako 1000 px – zmenšili sme ho a uložili k nám.' });
} else if (result.status === 'small') {
setUrlNote({ tone: 'ok', text: 'Obrázok je v poriadku (najviac 1000 px), adresa ostáva.' });
} else if (result.status === 'login') {
setUrlNote({
tone: 'warn',
text: 'Obrázok je za prihlásením – v e-maile sa nezobrazí. Otvorte ho v prehliadači, pravým klikom „Kopírovať obrázok“ a sem vložte Ctrl+V.'
});
} else if (result.status === 'failed') {
setUrlNote({ tone: 'warn', text: `Obrázok sa nepodarilo stiahnuť (${result.error}). Skontrolujte adresu, prípadne obrázok skopírujte a vložte Ctrl+V.` });
} }
} catch (err) { } catch (err) {
console.warn('Obrázok z URL sa nepodarilo zmenšiť:', err); setUrlNote({ tone: 'warn', text: `Obrázok sa nepodarilo spracovať: ${(err as Error).message}` });
} finally { } finally {
setIsUploading(false); setIsUploading(false);
} }
@@ -104,7 +116,13 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
<input <input
type="text" type="text"
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => {
setUrlNote(null);
onChange(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') checkUrl(e.currentTarget.value);
}}
onPaste={handlePaste} onPaste={handlePaste}
onBlur={(e) => checkUrl(e.target.value)} onBlur={(e) => checkUrl(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
@@ -140,6 +158,12 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
</button> </button>
</div> </div>
{urlNote && (
<p className={`text-[10px] leading-snug ${urlNote.tone === 'warn' ? 'text-amber-700 bg-amber-50 border border-amber-200 rounded-md px-2 py-1' : 'text-emerald-700'}`}>
{urlNote.text}
</p>
)}
{onAltChange && ( {onAltChange && (
<input <input
type="text" type="text"
+17 -6
View File
@@ -60,10 +60,17 @@ export function isOwnStorageUrl(url: string): boolean {
return url.includes('/storage/v1/object/public/newsletter-assets/'); return url.includes('/storage/v1/object/public/newsletter-assets/');
} }
export type RemoteImageResult =
| { status: 'skip' } // nie je to cudzia URL (naše úložisko, nedokončený text)
| { status: 'small' } // obrázok je dosť malý, adresa ostáva
| { status: 'shrunk'; url: string } // zmenšený a uložený k nám
| { status: 'login' } // server obrázka chce prihlásenie – v e-maile sa nezobrazí
| { status: 'failed'; error: string };
// Obrázok vložený cez URL: stiahne sa (priamo, ak to cudzí server dovolí, inak cez /api/img), // 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ť. // a ak je väčší ako 1000 px, zmenší sa a nahrá do úložiska.
export async function shrinkRemoteImage(url: string): Promise<string | null> { export async function shrinkRemoteImage(url: string): Promise<RemoteImageResult> {
if (!/^https?:\/\//i.test(url) || isOwnStorageUrl(url)) return null; if (!/^https?:\/\/[^/]+\.[^/]+\//i.test(url) || isOwnStorageUrl(url)) return { status: 'skip' };
let blob: Blob | null = null; let blob: Blob | null = null;
try { try {
const res = await fetch(url); const res = await fetch(url);
@@ -73,12 +80,16 @@ export async function shrinkRemoteImage(url: string): Promise<string | null> {
} }
if (!blob || !blob.type.startsWith('image/')) { if (!blob || !blob.type.startsWith('image/')) {
const res = await fetch(`/api/img?url=${encodeURIComponent(url)}`); const res = await fetch(`/api/img?url=${encodeURIComponent(url)}`);
if (!res.ok) return null; if (!res.ok) {
const data = await res.json().catch(() => ({}));
if (data.upstreamStatus === 401 || data.upstreamStatus === 403) return { status: 'login' };
return { status: 'failed', error: data.error || `Chyba ${res.status}` };
}
blob = await res.blob(); blob = await res.blob();
} }
const name = decodeURIComponent(new URL(url).pathname.split('/').pop() || '') || 'obrazok.png'; const name = decodeURIComponent(new URL(url).pathname.split('/').pop() || '') || 'obrazok.png';
const file = new File([blob], name, { type: blob.type }); const file = new File([blob], name, { type: blob.type });
const small = await shrinkImage(file); const small = await shrinkImage(file);
if (small === file) return null; if (small === file) return { status: 'small' };
return uploadFile(small, small.name); return { status: 'shrunk', url: await uploadFile(small, small.name) };
} }