A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 3.2 kB View raw
1import { useRef, useState } from 'react'; 2import { validateCoverFile, type CoverUpload } from '../lib/media/cover'; 3 4interface Props { 5 /** The currently chosen cover, or `null` when none is set. */ 6 cover: CoverUpload | null; 7 /** Upload a chosen file to the PDS; resolves with the persistable ref + a preview URL. */ 8 onUpload: ( file: File ) => Promise< CoverUpload >; 9 /** Report the new cover (or `null` when removed) to the parent. */ 10 onChange: ( cover: CoverUpload | null ) => void; 11} 12 13/** 14 * The per-article cover image picker, shown below the editor (outside the block canvas). 15 * Reuses the same PDS upload path as content images via `onUpload`. Surfaces the 1 MB cap as 16 * helper text AND as the rejection message, and — when no cover is set — makes the 17 * "first body image" fallback visible (design 2026-06-10). 18 */ 19export default function CoverImagePicker( { cover, onUpload, onChange }: Props ) { 20 const inputRef = useRef< HTMLInputElement >( null ); 21 const [ uploading, setUploading ] = useState( false ); 22 const [ error, setError ] = useState< string | null >( null ); 23 24 async function handleFiles( files: FileList | null ) { 25 const file = files?.[ 0 ]; 26 if ( ! file ) { 27 return; 28 } 29 const validationError = validateCoverFile( file ); 30 if ( validationError ) { 31 setError( validationError ); 32 return; 33 } 34 setError( null ); 35 setUploading( true ); 36 try { 37 const next = await onUpload( file ); 38 onChange( next ); 39 } catch ( err ) { 40 setError( err instanceof Error ? err.message : String( err ) ); 41 } finally { 42 setUploading( false ); 43 // Let the writer re-select the same file after a remove/replace. 44 if ( inputRef.current ) { 45 inputRef.current.value = ''; 46 } 47 } 48 } 49 50 return ( 51 <section className="studio__cover" aria-label="Cover image"> 52 <span className="studio__cover-label">Cover image</span> 53 54 { cover ? ( 55 <div className="studio__cover-preview"> 56 <img className="studio__cover-image" src={ cover.previewUrl } alt="" /> 57 <div className="studio__cover-actions"> 58 <button 59 type="button" 60 onClick={ () => inputRef.current?.click() } 61 disabled={ uploading } 62 > 63 { uploading ? 'Uploading…' : 'Replace' } 64 </button> 65 <button 66 type="button" 67 onClick={ () => { 68 setError( null ); 69 onChange( null ); 70 } } 71 disabled={ uploading } 72 > 73 Remove 74 </button> 75 </div> 76 </div> 77 ) : ( 78 <div className="studio__cover-empty"> 79 <button 80 type="button" 81 onClick={ () => inputRef.current?.click() } 82 disabled={ uploading } 83 > 84 { uploading ? 'Uploading…' : 'Upload cover image' } 85 </button> 86 <p className="studio__cover-hint"> 87 No cover set. If you don&apos;t add one, the first image in your 88 article will be used. PNG, JPG, or GIF, max 1 MB. 89 </p> 90 </div> 91 ) } 92 93 <input 94 ref={ inputRef } 95 className="studio__cover-input" 96 type="file" 97 accept="image/*" 98 hidden 99 onChange={ ( event ) => void handleFiles( event.target.files ) } 100 /> 101 102 { error && ( 103 <p className="studio__cover-error" role="alert"> 104 { error } 105 </p> 106 ) } 107 </section> 108 ); 109}