Skip to content

Commit aa1706a

Browse files
tastybentoclaude
andcommitted
Fix blog cover image: unclickable clear button and Discord CDN links
The "clear" button lived inside a truncated span with a max width, so a long cover URL overflowed and clipped the button out of view, making it impossible to remove a cover once set. Split the row so the button sits outside the truncation and stays clickable for any URL length. Also add an "Upload cover" button (self-hosts via the existing image endpoint) and reject Discord attachment CDN links, which are signed and expire within ~24h and therefore break as covers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qfc4jt4tJes3NUR8TxHnFY
1 parent db415fb commit aa1706a

1 file changed

Lines changed: 65 additions & 14 deletions

File tree

src/web/components/admin/BlogEditor.tsx

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export default function BlogEditor({ user, postId, onClose }: Props) {
4545
const [savedAt, setSavedAt] = useState<number | null>(null);
4646
const [dirty, setDirty] = useState(false);
4747
const fileInputRef = useRef<HTMLInputElement>(null);
48+
const coverInputRef = useRef<HTMLInputElement>(null);
4849
const textareaRef = useRef<HTMLTextAreaElement>(null);
4950

5051
// Initial load.
@@ -159,6 +160,37 @@ export default function BlogEditor({ user, postId, onClose }: Props) {
159160
}
160161
}
161162

163+
async function handleCoverUpload(file: File): Promise<void> {
164+
setBusy(true);
165+
try {
166+
const { url } = await UploadBlogImage(user.csrfToken, file);
167+
setCoverImage(url);
168+
} catch (e) {
169+
setError(extract(e));
170+
} finally {
171+
setBusy(false);
172+
if (coverInputRef.current) coverInputRef.current.value = '';
173+
}
174+
}
175+
176+
function promptForCoverUrl(): void {
177+
const url = window.prompt('Cover image URL? (must be /blog/images/... or https://)');
178+
if (!url) return;
179+
const trimmed = url.trim();
180+
if (!trimmed) return;
181+
// Discord attachment CDN links are signed and expire within ~24h, so
182+
// they break as soon as the signature lapses. Steer authors away from them.
183+
if (/^https:\/\/(cdn\.discordapp\.com|media\.discordapp\.net)\//i.test(trimmed)) {
184+
window.alert(
185+
'That looks like a Discord attachment link. Those URLs are signed and stop\n' +
186+
'working within about a day, so the cover would break. Please use "Upload\n' +
187+
'cover" instead to host the image here.',
188+
);
189+
return;
190+
}
191+
setCoverImage(trimmed);
192+
}
193+
162194
function insertAtCursor(text: string) {
163195
const ta = textareaRef.current;
164196
if (!ta) {
@@ -271,31 +303,50 @@ export default function BlogEditor({ user, postId, onClose }: Props) {
271303
if (f) handleImageUpload(f);
272304
}}
273305
/>
306+
<input
307+
ref={coverInputRef}
308+
type="file"
309+
accept="image/png,image/jpeg,image/webp,image/gif"
310+
style={{ display: 'none' }}
311+
onChange={(e) => {
312+
const f = e.target.files?.[0];
313+
if (f) handleCoverUpload(f);
314+
}}
315+
/>
274316
{coverImage && (
275-
<span css={tw`text-xs text-gray-500 truncate`} style={{ maxWidth: 280 }}>
276-
Cover: {coverImage}
317+
<span css={tw`flex items-center gap-2 text-xs text-gray-500`} style={{ minWidth: 0 }}>
318+
<span css={tw`truncate`} style={{ maxWidth: 240 }} title={coverImage}>
319+
Cover: {coverImage}
320+
</span>
277321
<button
278322
type="button"
279323
onClick={() => setCoverImage(null)}
280-
css={tw`ml-2 text-red-700 hover:underline`}
324+
css={tw`text-red-700 hover:underline flex-shrink-0`}
281325
style={{ background: 'transparent', border: 'none', cursor: 'pointer' }}
282326
>
283327
clear
284328
</button>
285329
</span>
286330
)}
287331
{!coverImage && (
288-
<button
289-
type="button"
290-
onClick={() => {
291-
const url = window.prompt('Cover image URL? (must be /blog/images/... or https://)');
292-
if (url) setCoverImage(url);
293-
}}
294-
css={tw`text-sm px-3 py-1 rounded border border-gray-300 hover:bg-gray-50`}
295-
style={{ background: 'white', cursor: 'pointer' }}
296-
>
297-
Set cover image
298-
</button>
332+
<>
333+
<button
334+
type="button"
335+
onClick={() => coverInputRef.current?.click()}
336+
css={tw`text-sm px-3 py-1 rounded border border-gray-300 hover:bg-gray-50`}
337+
style={{ background: 'white', cursor: 'pointer' }}
338+
>
339+
Upload cover
340+
</button>
341+
<button
342+
type="button"
343+
onClick={promptForCoverUrl}
344+
css={tw`text-sm px-3 py-1 rounded border border-gray-300 hover:bg-gray-50`}
345+
style={{ background: 'white', cursor: 'pointer' }}
346+
>
347+
Cover from URL
348+
</button>
349+
</>
299350
)}
300351
</div>
301352

0 commit comments

Comments
 (0)