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:
1 parent
6100a7af57
commit
48fd82a77d
3 files changed
+48
-13
No files matched your search
@@ -12,7 +12,7 @@ export async function GET(request: Request) {
|
||||
const res = await fetch(target, { redirect: 'follow' });
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
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();
|
||||
if (body.byteLength > MAX_BYTES) {
|
||||
|
||||
@@ -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)
|
||||
const lastCheckedUrl = useRef('');
|
||||
const [urlNote, setUrlNote] = useState<{ tone: 'ok' | 'warn'; text: string } | null>(null);
|
||||
const checkUrl = async (url: string) => {
|
||||
url = url.trim();
|
||||
if (!url || url === lastCheckedUrl.current) return;
|
||||
lastCheckedUrl.current = url;
|
||||
setIsUploading(true);
|
||||
setUrlNote(null);
|
||||
try {
|
||||
const smaller = await shrinkRemoteImage(url);
|
||||
if (smaller) {
|
||||
lastCheckedUrl.current = smaller;
|
||||
onChange(smaller);
|
||||
const result = await shrinkRemoteImage(url);
|
||||
if (result.status === 'shrunk') {
|
||||
lastCheckedUrl.current = result.url;
|
||||
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) {
|
||||
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 {
|
||||
setIsUploading(false);
|
||||
}
|
||||
@@ -104,7 +116,13 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
|
||||
<input
|
||||
type="text"
|
||||
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}
|
||||
onBlur={(e) => checkUrl(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
@@ -140,6 +158,12 @@ export const ImageUploadInput: React.FC<ImageUploadInputProps> = ({
|
||||
</button>
|
||||
</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 && (
|
||||
<input
|
||||
type="text"
|
||||
|
||||
+17
-6
@@ -60,10 +60,17 @@ export function isOwnStorageUrl(url: string): boolean {
|
||||
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),
|
||||
// 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;
|
||||
// a ak je väčší ako 1000 px, zmenší sa a nahrá do úložiska.
|
||||
export async function shrinkRemoteImage(url: string): Promise<RemoteImageResult> {
|
||||
if (!/^https?:\/\/[^/]+\.[^/]+\//i.test(url) || isOwnStorageUrl(url)) return { status: 'skip' };
|
||||
let blob: Blob | null = null;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
@@ -73,12 +80,16 @@ export async function shrinkRemoteImage(url: string): Promise<string | null> {
|
||||
}
|
||||
if (!blob || !blob.type.startsWith('image/')) {
|
||||
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();
|
||||
}
|
||||
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);
|
||||
if (small === file) return { status: 'small' };
|
||||
return { status: 'shrunk', url: await uploadFile(small, small.name) };
|
||||
}
|
||||
Reference in new issue
Block a user