Files
gerbeur/src/components/ImagePicker.tsx
khannurien 1cb904d2cf
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 47s
v3: generated placeholder thumbnails for pages with no preview image, and a fix for hotlink-protected ones
Thumbnails were failing in two different ways that both ended as an empty box.

Cloudflare hotlink protection answers a cross-site Referer with 403, so images
we had extracted correctly (dles.aukspot.com's og:image among them) never
rendered — every onError handler set display:none and swallowed it. The new
Thumbnail component loads with referrerPolicy="no-referrer", retries once
through /api/proxy-image for hosts that reject an empty referrer too, and only
then falls back to a placeholder.

Separately, the extraction cascade ended at the page's icon and then a guessed
/favicon.ico, so thumbnailUrl was almost never empty — just a 16x16 icon
cover-cropped into a 128x72 box. It now stops at real artwork, with faviconUrl
and accentColor (theme-color / msapplication-TileColor / mask-icon) as their own
fields. An absent thumbnailUrl finally means "no artwork", which is what makes
the placeholder possible: the site's own color mixed into the theme surface,
with its favicon centered on it, or its initial. Contrast holds for any
third-party color by construction rather than by luminance math, so nyt only
has to set --thumb-tint-strength to 0% to stay monochrome and geocities only
has to raise it. Missing accents fall back to a stable hostname-derived hue, so
rows saved before this get a tint with no backfill.

Migration 0011 reclassifies favicon-shaped thumbnailUrls on existing dumps.
The journal mosaic keeps its pull-quote and text fallbacks — the placeholder
appears there only to repair a broken image.

Also fixed: refresh silently overwrote good metadata with a failure stub, the
refresh button swallowed every error, refresh never broadcast the update,
extractBestIcon ranked SVG icons below 16x16 PNGs, and shared links with no
artwork carried no og:image at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:43:55 +00:00

72 lines
1.7 KiB
TypeScript

import { useRef } from "react";
interface ImagePickerProps {
src: string | null;
alt?: string;
size?: number;
borderRadius?: number;
onChange: (file: File) => void;
uploading?: boolean;
accept?: string;
}
export function ImagePicker({
src,
alt = "",
size = 80,
borderRadius = 8,
onChange,
uploading = false,
accept = "image/jpeg,image/png,image/gif,image/webp",
}: ImagePickerProps) {
const inputRef = useRef<HTMLInputElement>(null);
const sizeStyle = { width: size, height: size, borderRadius };
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) onChange(file);
e.target.value = "";
};
return (
<div
className="img-picker"
style={sizeStyle}
onClick={() => !uploading && inputRef.current?.click()}
title={src ? "Change image" : "Add image"}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
}}
>
{src
? (
<img
src={src}
alt={alt}
className="img-picker-img"
style={{ borderRadius }}
referrerPolicy="no-referrer"
/>
)
: (
<div className="img-picker-placeholder" style={{ borderRadius }}>
<span>+</span>
</div>
)}
<div className="img-picker-overlay" style={{ borderRadius }}>
{uploading ? "…" : "✎"}
</div>
<input
ref={inputRef}
type="file"
accept={accept}
onChange={handleChange}
disabled={uploading}
style={{ display: "none" }}
/>
</div>
);
}