Skip to content

Commit 1185771

Browse files
committed
Refine feed preview and utility surfaces
1 parent 4a8c1d1 commit 1185771

7 files changed

Lines changed: 117 additions & 66 deletions

File tree

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ describe('ResultDisplay', () => {
1717
beforeEach(() => {
1818
vi.clearAllMocks();
1919
vi.spyOn(window, 'fetch').mockResolvedValue({
20-
text: async () =>
21-
`<?xml version="1.0"?><rss><channel><item><description>1. Item One ( example.com )</description></item><item><description>2. Item Two ( example.com )</description></item><item><description>3. Item Three ( example.com )</description></item></channel></rss>`,
20+
ok: true,
21+
json: async () => ({
22+
items: [
23+
{ title: 'Item One' },
24+
{ content_text: '2. Item Two ( example.com )' },
25+
{ title: 'Item Three' },
26+
],
27+
}),
2228
} as Response);
2329
});
2430

@@ -32,6 +38,9 @@ describe('ResultDisplay', () => {
3238
expect(screen.getByText('Item One')).toBeInTheDocument();
3339
expect(screen.getByText('Item Three')).toBeInTheDocument();
3440
});
41+
expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.xml', {
42+
headers: { Accept: 'application/feed+json' },
43+
});
3544
});
3645

3746
it('calls onCreateAnother when the reset button is clicked', () => {

frontend/src/components/AppPanels.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export function CreateFeedPanel({
131131
{!feedCreationEnabled && (
132132
<p class="field-help field-help--alert">Custom feed generation is disabled for this instance.</p>
133133
)}
134-
{accessTokenRequired && !hasAccessToken && <p class="field-help">Token requested only if needed.</p>}
135134
</div>
136135

137136
{showTokenPrompt && (
@@ -217,6 +216,14 @@ export function UtilityStrip({ hasAccessToken, onClearToken }: UtilityStripProps
217216
>
218217
Getting started
219218
</a>
219+
<a
220+
href="https://github.com/html2rss/html2rss-web"
221+
target="_blank"
222+
rel="noopener noreferrer"
223+
class="utility-link"
224+
>
225+
Source code
226+
</a>
220227
{hasAccessToken && (
221228
<button type="button" class="utility-button" onClick={onClearToken}>
222229
Clear saved token

frontend/src/components/DominantField.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface DominantFieldProps {
1111
disabled?: boolean;
1212
actionLabel: string;
1313
actionText: string;
14+
actionVariant?: 'default' | 'soft';
1415
onAction?: () => void;
1516
onInput?: JSX.GenericEventHandler<HTMLInputElement>;
1617
inputRef?: Ref<HTMLInputElement>;
@@ -28,6 +29,7 @@ export function DominantField({
2829
disabled = false,
2930
actionLabel,
3031
actionText,
32+
actionVariant = 'default',
3133
onAction,
3234
onInput,
3335
inputRef,
@@ -55,7 +57,9 @@ export function DominantField({
5557
</label>
5658
<button
5759
type={onAction ? 'button' : 'submit'}
58-
class={`dominant-field__action${actionText.length > 1 ? ' dominant-field__action--text' : ''}`}
60+
class={`dominant-field__action${actionText.length > 1 ? ' dominant-field__action--text' : ''}${
61+
actionVariant === 'soft' ? ' dominant-field__action--soft' : ''
62+
}`}
5963
disabled={disabled}
6064
aria-label={actionLabel}
6165
onClick={onAction}

frontend/src/components/Footer.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

frontend/src/components/ResultDisplay.tsx

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { useEffect, useRef, useState } from 'preact/hooks';
22
import type { FeedRecord } from '../api/contracts';
33
import { DominantField } from './DominantField';
44

5+
interface JsonFeedItem {
6+
title?: string;
7+
content_text?: string;
8+
}
9+
10+
interface JsonFeedResponse {
11+
items?: JsonFeedItem[];
12+
}
13+
514
interface ResultDisplayProps {
615
result: FeedRecord;
716
onCreateAnother: () => void;
@@ -27,25 +36,16 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
2736

2837
const loadPreview = async () => {
2938
try {
30-
const response = await window.fetch(fullUrl);
31-
const xml = await response.text();
32-
const document = new DOMParser().parseFromString(xml, 'application/xml');
33-
const explicitTitles = Array.from(document.querySelectorAll('item > title, entry > title')).map(
34-
(node) => node.textContent?.trim()
35-
);
36-
const derivedDescriptions = Array.from(document.querySelectorAll('item > description'))
37-
.map((node) => node.textContent?.trim())
38-
.filter((description): description is string => Boolean(description))
39-
.filter((description) => /^\d+\.\s+/.test(description))
40-
.map((description) =>
41-
description
42-
.replace(/^\d+\.\s+/, '')
43-
.replace(/\s+\([^)]*\)\s*$/, '')
44-
.trim()
45-
);
46-
const itemTitles = [...explicitTitles, ...derivedDescriptions]
47-
.filter((title): title is string => Boolean(title))
48-
.slice(0, 3);
39+
const response = await window.fetch(fullUrl, {
40+
headers: { Accept: 'application/feed+json' },
41+
});
42+
if (!response.ok) throw new Error('Preview request failed');
43+
const payload = (await response.json()) as JsonFeedResponse;
44+
const itemTitles =
45+
payload.items
46+
?.map((item) => normalizePreviewText(item.title || item.content_text))
47+
.filter((title): title is string => Boolean(title))
48+
.slice(0, 3) || [];
4949

5050
if (!isCancelled) setPreviewItems(itemTitles);
5151
} catch {
@@ -84,9 +84,19 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
8484
readOnly
8585
actionLabel="Copy feed URL"
8686
actionText="Copy"
87+
actionVariant="soft"
8788
onAction={() => void copyToClipboard(fullUrl)}
8889
/>
8990

91+
<div class="result-actions result-actions--quiet">
92+
<a href={fullUrl} class="btn btn--ghost btn--linkish" target="_blank" rel="noopener noreferrer">
93+
Open feed
94+
</a>
95+
<button type="button" class="btn btn--quiet btn--linkish" onClick={onCreateAnother}>
96+
Create another feed
97+
</button>
98+
</div>
99+
90100
{previewItems.length > 0 && (
91101
<section class="result-preview" aria-label="Feed preview">
92102
<p class="result-preview__label">Latest items</p>
@@ -98,15 +108,6 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
98108
</section>
99109
)}
100110

101-
<div class="result-actions result-actions--quiet">
102-
<a href={fullUrl} class="btn btn--ghost btn--linkish" target="_blank" rel="noopener noreferrer">
103-
Open feed
104-
</a>
105-
<button type="button" class="btn btn--quiet btn--linkish" onClick={onCreateAnother}>
106-
Create another feed
107-
</button>
108-
</div>
109-
110111
{copyNotice && (
111112
<div class="notice notice--success" role="status">
112113
<p>{copyNotice}</p>
@@ -115,3 +116,15 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
115116
</section>
116117
);
117118
}
119+
120+
function normalizePreviewText(value?: string): string | null {
121+
if (!value) return null;
122+
123+
const normalized = value
124+
.replace(/\s+/g, ' ')
125+
.replace(/^\d+\.\s+/, '')
126+
.replace(/\s+\([^)]*\)\s*$/, '')
127+
.trim();
128+
129+
return normalized || null;
130+
}

frontend/src/main.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { render } from 'preact';
22
import { App } from './components/App';
3-
import { Footer } from './components/Footer';
43
import './styles/main.css';
54

65
function Root() {
@@ -9,7 +8,6 @@ function Root() {
98
<main class="page-main">
109
<App />
1110
</main>
12-
<Footer />
1311
</div>
1412
);
1513
}

frontend/src/styles/main.css

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ textarea {
198198
gap: var(--space-4);
199199
}
200200

201+
.result-shell {
202+
gap: var(--space-3);
203+
}
204+
201205
.dominant-field {
202206
position: relative;
203207
width: 100%;
@@ -346,11 +350,21 @@ a:focus-visible {
346350
text-transform: uppercase;
347351
}
348352

353+
.dominant-field__action--soft {
354+
background: rgba(255, 255, 255, 0.045);
355+
color: var(--text-muted);
356+
}
357+
349358
.dominant-field__action:hover:not(:disabled) {
350359
transform: translateX(0.04rem);
351360
background: rgba(255, 255, 255, 0.11);
352361
}
353362

363+
.dominant-field__action--soft:hover:not(:disabled) {
364+
background: rgba(255, 255, 255, 0.08);
365+
color: var(--text-strong);
366+
}
367+
354368
.dominant-field__action:disabled {
355369
opacity: 0.5;
356370
cursor: not-allowed;
@@ -484,20 +498,26 @@ a:focus-visible {
484498
}
485499

486500
.result-copy {
487-
text-align: center;
501+
width: min(100%, 40rem);
502+
margin: 0 auto;
503+
justify-items: start;
504+
text-align: left;
488505
}
489506

490507
.result-meta {
491-
max-width: 32rem;
492-
font-size: var(--font-size-1);
508+
max-width: 36rem;
509+
font-size: clamp(1.15rem, 2vw, 1.45rem);
510+
line-height: 1.2;
511+
color: var(--text-body);
493512
}
494513

495514
.result-preview {
496-
width: min(100%, 34rem);
515+
width: min(100%, 40rem);
497516
margin: 0 auto;
498517
display: grid;
499518
gap: var(--space-2);
500-
justify-items: center;
519+
justify-items: start;
520+
padding-top: var(--space-1);
501521
}
502522

503523
.result-preview__label {
@@ -514,16 +534,29 @@ a:focus-visible {
514534
width: 100%;
515535
list-style: none;
516536
display: grid;
517-
gap: 0.45rem;
537+
gap: 0.6rem;
518538
}
519539

520540
.result-preview__list li {
521541
color: var(--text-body);
522-
text-align: center;
542+
max-width: 32rem;
543+
font-family: var(--font-family-display);
544+
font-size: clamp(1.08rem, 1.8vw, 1.42rem);
545+
line-height: 1.24;
546+
text-align: left;
523547
}
524548

525549
.result-actions--quiet {
526-
gap: var(--space-4);
550+
width: min(100%, 40rem);
551+
margin: 0 auto;
552+
justify-items: start;
553+
justify-content: start;
554+
gap: var(--space-5);
555+
}
556+
557+
.btn--linkish {
558+
color: var(--text-body);
559+
font-weight: 600;
527560
}
528561

529562
.status-card {
@@ -579,19 +612,6 @@ a:focus-visible {
579612
text-transform: uppercase;
580613
}
581614

582-
.app-footer {
583-
padding: 0 var(--space-4) var(--space-4);
584-
}
585-
586-
.app-footer__inner {
587-
width: min(100%, var(--page-max-width));
588-
margin: 0 auto;
589-
padding-top: var(--space-3);
590-
border-top: var(--border-width) solid rgba(255, 255, 255, 0.08);
591-
color: var(--text-muted);
592-
font-size: var(--font-size-00);
593-
}
594-
595615
@media (min-width: 48rem) {
596616
.page-main {
597617
padding-top: clamp(1rem, 6vh, 4rem);
@@ -622,6 +642,17 @@ a:focus-visible {
622642
padding-right: 4.2rem;
623643
}
624644

645+
.result-copy,
646+
.result-preview,
647+
.result-actions--quiet {
648+
width: 100%;
649+
}
650+
651+
.result-actions--quiet {
652+
grid-template-columns: 1fr;
653+
gap: var(--space-3);
654+
}
655+
625656
.field-block--select {
626657
gap: 0;
627658
}

0 commit comments

Comments
 (0)