Skip to content

Commit e747b23

Browse files
authored
feat: unify web and feed result surfaces (#896)
## Summary - align the web result state and rendered feed surface around a shared visual language - move shared presentation rules into `public/shared-ui.css` and simplify feed/result action copy - update frontend and smoke coverage for the revised menu and success-state layout ## Verification - docker compose -f .devcontainer/docker-compose.yml run --rm app make setup - docker compose -f .devcontainer/docker-compose.yml run --rm app make ready - browser smoke in chrome-devtools at http://127.0.0.1:4001/ covering utility menu, token gate, and success/result state
1 parent 377cff0 commit e747b23

14 files changed

Lines changed: 938 additions & 330 deletions

app/web/api/v1/strategies.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def index(_request)
3333

3434
def display_name_for(name)
3535
case name.to_s
36-
when 'faraday' then 'Standard rendering'
36+
when 'faraday' then 'Default'
3737
when 'browserless' then 'JavaScript pages (recommended)'
3838
else name.to_s.split('_').map(&:capitalize).join(' ')
3939
end

frontend/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
name="description"
99
content="html2rss converts fixed demo pages or operator-submitted URLs into feed endpoints."
1010
/>
11+
<link rel="stylesheet" href="/shared-ui.css" />
1112
<link rel="icon" href="/favicon.ico" />
1213
<title>html2rss</title>
1314
</head>

frontend/src/__tests__/App.contract.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,18 @@ describe('App contract', () => {
6464
fireEvent.click(screen.getByRole('button', { name: 'Generate feed URL' }));
6565

6666
await waitFor(() => {
67+
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
6768
expect(screen.getByText('Example Feed')).toBeInTheDocument();
6869
expect(screen.getByLabelText('Feed URL')).toBeInTheDocument();
6970
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();
7071
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
71-
expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute(
72+
expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute(
7273
'href',
7374
'http://localhost:3000/api/v1/feeds/generated-token.json'
7475
);
7576
expect(screen.getByRole('button', { name: 'Create another feed' })).toBeInTheDocument();
76-
expect(screen.getByText('Feed preview')).toBeInTheDocument();
77+
expect(screen.getByText('Preview')).toBeInTheDocument();
78+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
7779
expect(screen.getByText('Contract Item')).toBeInTheDocument();
7880
});
7981
});

frontend/src/__tests__/App.test.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,36 @@ describe('App', () => {
268268
expect(mockClearToken).toHaveBeenCalled();
269269
});
270270

271+
it('keeps the Docker Hub link before token clear when a token is saved', () => {
272+
mockUseAccessToken.mockReturnValue({
273+
token: 'saved-token',
274+
hasToken: true,
275+
saveToken: mockSaveToken,
276+
clearToken: mockClearToken,
277+
isLoading: false,
278+
error: null,
279+
});
280+
281+
render(<App />);
282+
283+
fireEvent.click(screen.getByRole('button', { name: 'More' }));
284+
285+
const utilityItems = Array.from(
286+
screen
287+
.getByLabelText('Utilities')
288+
.querySelectorAll('.utility-strip__items > a, .utility-strip__items > button')
289+
).map((element) => element.textContent);
290+
291+
expect(utilityItems).toEqual([
292+
'Try included feeds',
293+
'Bookmarklet',
294+
'OpenAPI spec',
295+
'Source code',
296+
'Install from Docker Hub',
297+
'Clear saved token',
298+
]);
299+
});
300+
271301
it('saves access token and resumes feed creation from the inline prompt', async () => {
272302
render(<App />);
273303

@@ -375,7 +405,13 @@ describe('App', () => {
375405
fireEvent.click(screen.getByRole('button', { name: 'More' }));
376406

377407
const utilityLinks = screen.getAllByRole('link').map((link) => link.textContent);
378-
expect(utilityLinks).toEqual(['Try included feeds', 'Bookmarklet', 'OpenAPI spec', 'Source code']);
408+
expect(utilityLinks).toEqual([
409+
'Try included feeds',
410+
'Bookmarklet',
411+
'OpenAPI spec',
412+
'Source code',
413+
'Install from Docker Hub',
414+
]);
379415

380416
expect(screen.getByRole('link', { name: 'OpenAPI spec' })).toHaveAttribute(
381417
'href',
@@ -385,5 +421,9 @@ describe('App', () => {
385421
'href',
386422
'https://html2rss.github.io/web-application/how-to/use-included-configs/'
387423
);
424+
expect(screen.getByRole('link', { name: 'Install from Docker Hub' })).toHaveAttribute(
425+
'href',
426+
'https://hub.docker.com/r/html2rss/web'
427+
);
388428
});
389429
});

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('ResultDisplay', () => {
99
id: 'test-id',
1010
name: 'Test Feed',
1111
url: 'https://example.com',
12-
strategy: 'ssrf_filter',
12+
strategy: 'faraday',
1313
feed_token: 'test-feed-token',
1414
public_url: 'https://example.com/feed.xml',
1515
json_public_url: 'https://example.com/feed.json',
@@ -21,28 +21,44 @@ describe('ResultDisplay', () => {
2121
ok: true,
2222
json: async () => ({
2323
items: [
24-
{ title: 'Item One' },
25-
{ content_text: '56 points by canpan 1 hour ago | hide | 18&nbsp;comments' },
26-
{ content_text: '2. Item Two ( example.com )' },
24+
{
25+
title: 'Item One',
26+
content_text: '<p>First preview item with <strong>markup</strong>.</p>',
27+
url: 'https://example.com/item-one',
28+
date_published: '2024-01-01T00:00:00Z',
29+
},
30+
{
31+
content_text: '56 points by canpan 1 hour ago | hide | 18&nbsp;comments',
32+
date_published: '2024-01-02T00:00:00Z',
33+
},
34+
{
35+
content_text: '2. Item Two ( example.com )',
36+
url: 'https://example.com/item-two',
37+
date_published: '2024-01-03T00:00:00Z',
38+
},
2739
],
2840
}),
2941
} as Response);
3042
});
3143

32-
it('renders the simplified result actions and preview', async () => {
44+
it('renders the success state actions and richer preview cards', async () => {
3345
render(<ResultDisplay result={mockResult} onCreateAnother={mockOnCreateAnother} />);
3446

47+
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
3548
expect(screen.getByText('Test Feed')).toBeInTheDocument();
3649
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();
3750
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
38-
expect(screen.getByRole('link', { name: 'JSON Feed' })).toHaveAttribute(
51+
expect(screen.getByRole('link', { name: 'Open JSON Feed' })).toHaveAttribute(
3952
'href',
4053
'https://example.com/feed.json'
4154
);
4255
await waitFor(() => {
4356
expect(screen.getByText('Item One')).toBeInTheDocument();
57+
expect(screen.getByText('First preview item with markup.')).toBeInTheDocument();
58+
expect(screen.getAllByText('Open original')).toHaveLength(2);
4459
expect(screen.getByText(/points by canpan/i)).toBeInTheDocument();
4560
expect(screen.getByText('Item Two')).toBeInTheDocument();
61+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
4662
});
4763
expect(window.fetch).toHaveBeenCalledWith('https://example.com/feed.xml', {
4864
headers: { Accept: 'application/feed+json' },
@@ -59,6 +75,7 @@ describe('ResultDisplay', () => {
5975

6076
await waitFor(() => {
6177
expect(screen.getByText('Preview unavailable right now.')).toBeInTheDocument();
78+
expect(screen.getByText('Latest items from this feed')).toBeInTheDocument();
6279
});
6380
});
6481

frontend/src/__tests__/useFeedConversion.contract.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('useFeedConversion contract', () => {
1313
const body = (await request.json()) as { url: string; strategy: string };
1414
receivedAuthorization = request.headers.get('authorization');
1515

16-
expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'ssrf_filter' });
16+
expect(body).toEqual({ url: 'https://example.com/articles', strategy: 'faraday' });
1717

1818
return HttpResponse.json(
1919
buildFeedResponse({
@@ -30,7 +30,7 @@ describe('useFeedConversion contract', () => {
3030
const { result } = renderHook(() => useFeedConversion());
3131

3232
await act(async () => {
33-
await result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'test-token-123');
33+
await result.current.convertFeed('https://example.com/articles', 'faraday', 'test-token-123');
3434
});
3535

3636
expect(receivedAuthorization).toBe('Bearer test-token-123');
@@ -54,7 +54,7 @@ describe('useFeedConversion contract', () => {
5454

5555
await act(async () => {
5656
await expect(
57-
result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token')
57+
result.current.convertFeed('https://example.com/articles', 'faraday', 'token')
5858
).rejects.toThrow('URL parameter is required');
5959
});
6060

@@ -76,7 +76,7 @@ describe('useFeedConversion contract', () => {
7676

7777
await act(async () => {
7878
await expect(
79-
result.current.convertFeed('https://example.com/articles', 'ssrf_filter', 'token')
79+
result.current.convertFeed('https://example.com/articles', 'faraday', 'token')
8080
).rejects.toThrow('Invalid response format from feed creation API');
8181
});
8282

frontend/src/__tests__/useFeedConversion.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('useFeedConversion', () => {
2727
id: 'test-id',
2828
name: 'Test Feed',
2929
url: 'https://example.com',
30-
strategy: 'ssrf_filter',
30+
strategy: 'faraday',
3131
feed_token: 'test-token',
3232
public_url: 'https://example.com/feed.xml',
3333
json_public_url: 'https://example.com/feed.json',
@@ -51,7 +51,7 @@ describe('useFeedConversion', () => {
5151
const { result } = renderHook(() => useFeedConversion());
5252

5353
await act(async () => {
54-
await result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken');
54+
await result.current.convertFeed('https://example.com', 'faraday', 'testtoken');
5555
});
5656

5757
expect(result.current.isConverting).toBe(false);
@@ -77,9 +77,9 @@ describe('useFeedConversion', () => {
7777
const { result } = renderHook(() => useFeedConversion());
7878

7979
await act(async () => {
80-
await expect(
81-
result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken')
82-
).rejects.toThrow('Bad Request');
80+
await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow(
81+
'Bad Request'
82+
);
8383
});
8484

8585
expect(result.current.isConverting).toBe(false);
@@ -93,9 +93,9 @@ describe('useFeedConversion', () => {
9393
const { result } = renderHook(() => useFeedConversion());
9494

9595
await act(async () => {
96-
await expect(
97-
result.current.convertFeed('https://example.com', 'ssrf_filter', 'testtoken')
98-
).rejects.toThrow('Network error');
96+
await expect(result.current.convertFeed('https://example.com', 'faraday', 'testtoken')).rejects.toThrow(
97+
'Network error'
98+
);
9999
});
100100

101101
expect(result.current.isConverting).toBe(false);

frontend/src/components/AppPanels.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,14 @@ export function UtilityStrip({
292292
>
293293
Source code
294294
</a>
295+
<a
296+
href="https://hub.docker.com/r/html2rss/web"
297+
target="_blank"
298+
rel="noopener noreferrer"
299+
class="utility-link"
300+
>
301+
Install from Docker Hub
302+
</a>
295303
{hasAccessToken && (
296304
<button type="button" class="utility-button" onClick={onClearToken}>
297305
Clear saved token

frontend/src/components/DominantField.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSX, Ref } from 'preact';
22

33
interface DominantFieldProps {
4+
className?: string;
45
id: string;
56
label: string;
67
value: string;
@@ -19,6 +20,7 @@ interface DominantFieldProps {
1920
}
2021

2122
export function DominantField({
23+
className,
2224
id,
2325
label,
2426
value,
@@ -36,14 +38,14 @@ export function DominantField({
3638
error,
3739
}: DominantFieldProps) {
3840
return (
39-
<div class="dominant-field">
40-
<label class="field-block field-block--primary field-block--hero" htmlFor={id}>
41+
<div class={className ? `dominant-field ${className}` : 'dominant-field'}>
42+
<label class="field-block field-block--centered" htmlFor={id}>
4143
<span class="field-label field-label--ghost">{label}</span>
4244
<input
4345
id={id}
4446
name={id}
4547
type={type}
46-
class="input input--mono input--hero"
48+
class="input input--mono input--lg"
4749
placeholder={placeholder}
4850
autocomplete={type === 'url' ? 'url' : 'off'}
4951
autoFocus={autoFocus}

0 commit comments

Comments
 (0)