@@ -5,20 +5,31 @@ import { DominantField } from './DominantField';
55interface JsonFeedItem {
66 title ?: string ;
77 content_text ?: string ;
8+ content_html ?: string ;
9+ url ?: string ;
10+ external_url ?: string ;
11+ date_published ?: string ;
812}
913
1014interface JsonFeedResponse {
1115 items ?: JsonFeedItem [ ] ;
1216}
1317
18+ interface PreviewItem {
19+ title : string ;
20+ excerpt : string ;
21+ publishedLabel : string ;
22+ url ?: string ;
23+ }
24+
1425interface ResultDisplayProps {
1526 result : FeedRecord ;
1627 onCreateAnother : ( ) => void ;
1728}
1829
1930export function ResultDisplay ( { result, onCreateAnother } : ResultDisplayProps ) {
2031 const [ copyNotice , setCopyNotice ] = useState ( '' ) ;
21- const [ previewItems , setPreviewItems ] = useState < string [ ] > ( [ ] ) ;
32+ const [ previewItems , setPreviewItems ] = useState < PreviewItem [ ] > ( [ ] ) ;
2233 const [ previewError , setPreviewError ] = useState ( '' ) ;
2334 const copyResetRef = useRef < number | undefined > ( undefined ) ;
2435
@@ -45,14 +56,14 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
4556 } ) ;
4657 if ( ! response . ok ) throw new Error ( 'Preview request failed' ) ;
4758 const payload = ( await response . json ( ) ) as JsonFeedResponse ;
48- const itemTitles =
59+ const items =
4960 payload . items
50- ?. map ( ( item ) => normalizePreviewText ( item . title || item . content_text ) )
51- . filter ( ( title ) : title is string => Boolean ( title ) )
52- . slice ( 0 , 3 ) || [ ] ;
61+ ?. map ( ( item ) => normalizePreviewItem ( item ) )
62+ . filter ( ( item ) : item is PreviewItem => Boolean ( item ) )
63+ . slice ( 0 , 5 ) || [ ] ;
5364
5465 if ( ! isCancelled ) {
55- setPreviewItems ( itemTitles ) ;
66+ setPreviewItems ( items ) ;
5667 setPreviewError ( '' ) ;
5768 }
5869 } catch {
@@ -82,12 +93,16 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
8293 } ;
8394
8495 return (
85- < section class = "result-shell" aria-live = "polite" >
86- < div class = "result-copy" >
87- < p class = "result-meta" > { result . name } </ p >
88- </ div >
96+ < section class = "result-shell layout-stack" aria-live = "polite" >
97+ < header class = "result-hero layout-rail-reading layout-stack" style = { { '--stack-gap' : 'var(--space-3)' } } >
98+ < p class = "result-kicker ui-eyebrow" > Feed created</ p >
99+ < h1 class = "result-title" > Your feed is ready</ h1 >
100+ < p class = "result-meta layout-rail-copy" > { result . name } </ p >
101+ < p class = "result-lede layout-rail-copy" > Subscribe to this URL in your RSS reader.</ p >
102+ </ header >
89103
90104 < DominantField
105+ className = "layout-rail-reading"
91106 id = "feed-url"
92107 label = "Feed URL"
93108 value = { fullUrl }
@@ -98,32 +113,51 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
98113 onAction = { ( ) => void copyToClipboard ( fullUrl ) }
99114 />
100115
101- < div class = "result-actions result-actions--quiet" >
102- < a href = { fullUrl } class = "btn btn--ghost btn--linkish " target = "_blank" rel = "noopener noreferrer" >
116+ < div class = "result-actions result-actions--quiet layout-rail-reading " >
117+ < a href = { fullUrl } class = "btn btn--ghost" target = "_blank" rel = "noopener noreferrer" >
103118 Open feed
104119 </ a >
105- < a href = { jsonFeedUrl } class = "btn btn--ghost btn--linkish " target = "_blank" rel = "noopener noreferrer" >
106- JSON Feed
120+ < a href = { jsonFeedUrl } class = "btn btn--ghost" target = "_blank" rel = "noopener noreferrer" >
121+ Open JSON Feed
107122 </ a >
108123 < button type = "button" class = "btn btn--quiet btn--linkish" onClick = { onCreateAnother } >
109124 Create another feed
110125 </ button >
111126 </ div >
112127
113128 { previewItems . length > 0 && (
114- < section class = "result-preview" aria-label = "Feed preview" >
115- < p class = "result-preview__label" > Feed preview</ p >
116- < ul class = "result-preview__list" >
129+ < section class = "result-preview layout-rail-reading layout-stack" aria-label = "Feed preview" >
130+ < div class = "result-preview__header layout-stack layout-stack--tight" >
131+ < p class = "result-preview__label ui-eyebrow" > Preview</ p >
132+ < p class = "result-preview__intro" > Latest items from this feed</ p >
133+ </ div >
134+ < ul class = "result-preview__list" role = "list" >
117135 { previewItems . map ( ( item ) => (
118- < li key = { item } > { item } </ li >
136+ < li key = { `${ item . title } -${ item . publishedLabel || 'undated' } ` } >
137+ < article class = "preview-card ui-card layout-stack layout-stack--tight" >
138+ < h2 class = "preview-card__title" > { item . title } </ h2 >
139+ { item . publishedLabel && < p class = "preview-card__date" > { item . publishedLabel } </ p > }
140+ { item . excerpt && < p class = "preview-card__excerpt" > { item . excerpt } </ p > }
141+ { item . url && (
142+ < p class = "preview-card__actions" >
143+ < a href = { item . url } target = "_blank" rel = "noopener noreferrer" >
144+ Open original
145+ </ a >
146+ </ p >
147+ ) }
148+ </ article >
149+ </ li >
119150 ) ) }
120151 </ ul >
121152 </ section >
122153 ) }
123154
124155 { previewError && (
125- < section class = "result-preview" aria-label = "Feed preview status" >
126- < p class = "result-preview__label" > Feed preview</ p >
156+ < section class = "result-preview layout-rail-reading layout-stack" aria-label = "Feed preview status" >
157+ < div class = "result-preview__header layout-stack layout-stack--tight" >
158+ < p class = "result-preview__label ui-eyebrow" > Preview</ p >
159+ < p class = "result-preview__intro" > Latest items from this feed</ p >
160+ </ div >
127161 < p class = "field-help" > { previewError } </ p >
128162 </ section >
129163 ) }
@@ -141,14 +175,63 @@ function normalizePreviewText(value?: string): string | null {
141175 if ( ! value ) return null ;
142176
143177 const normalized = decodeHtmlEntities ( value )
178+ . replace ( / < [ ^ > ] * > / g, ' ' )
144179 . replace ( / \s + / g, ' ' )
180+ . replace ( / \s + ( [ . , ! ? ; : ] ) / g, '$1' )
145181 . replace ( / ^ \d + \. \s + / , '' )
146182 . replace ( / \s + \( [ ^ ) ] * \) \s * $ / , '' )
147183 . trim ( ) ;
148184
149185 return normalized || null ;
150186}
151187
188+ function normalizePreviewItem ( item : JsonFeedItem ) : PreviewItem | null {
189+ const excerptSource = item . content_text || item . content_html ;
190+ const title = normalizePreviewText ( item . title ) || normalizePreviewText ( excerptSource ) || 'Untitled item' ;
191+ const excerpt = normalizePreviewExcerpt ( excerptSource , title ) ;
192+
193+ return {
194+ title,
195+ excerpt,
196+ publishedLabel : formatPublishedDate ( item . date_published ) ,
197+ url : normalizePreviewUrl ( item . url || item . external_url ) ,
198+ } ;
199+ }
200+
201+ function normalizePreviewExcerpt ( value : string | undefined , title : string ) : string {
202+ const excerpt = normalizePreviewText ( value ) ;
203+ if ( ! excerpt || excerpt === title ) return '' ;
204+ return truncateText ( excerpt , 220 ) ;
205+ }
206+
207+ function normalizePreviewUrl ( value ?: string ) : string | undefined {
208+ if ( ! value ) return undefined ;
209+ if ( ! / ^ h t t p s ? : \/ \/ / i. test ( value ) ) return undefined ;
210+ return value ;
211+ }
212+
213+ function formatPublishedDate ( value ?: string ) : string {
214+ if ( ! value ) return '' ;
215+
216+ const parsed = new Date ( value ) ;
217+ if ( Number . isNaN ( parsed . getTime ( ) ) ) return '' ;
218+
219+ return new Intl . DateTimeFormat ( undefined , {
220+ month : 'short' ,
221+ day : 'numeric' ,
222+ year : 'numeric' ,
223+ } ) . format ( parsed ) ;
224+ }
225+
226+ function truncateText ( value : string , maxLength : number ) : string {
227+ if ( value . length <= maxLength ) return value ;
228+
229+ const clipped = value . slice ( 0 , maxLength ) . trimEnd ( ) ;
230+ const safeBoundary = clipped . lastIndexOf ( ' ' ) ;
231+
232+ return `${ ( safeBoundary > maxLength * 0.6 ? clipped . slice ( 0 , safeBoundary ) : clipped ) . trimEnd ( ) } ...` ;
233+ }
234+
152235function decodeHtmlEntities ( value : string ) : string {
153236 if ( typeof document === 'undefined' ) return value ;
154237
0 commit comments