Skip to content

Commit fbb02e8

Browse files
committed
Align result and feed UI
1 parent aeb414c commit fbb02e8

11 files changed

Lines changed: 567 additions & 157 deletions

File tree

app/web/feeds/responder.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ class << self
1212
# @param identifier [String]
1313
# @return [String] serialized feed body.
1414
def call(request:, target_kind:, identifier:)
15-
feed_request = Request.call(request:, target_kind:, identifier:)
16-
resolved_source = SourceResolver.call(feed_request)
17-
result = Service.call(resolved_source)
18-
normalized_identifier = feed_request.feed_name || identifier
15+
feed_request, resolved_source, result = resolve_request(request:, target_kind:, identifier:)
1916
body = write_response(response: request.response, representation: feed_request.representation, result:)
20-
21-
emit_result(target_kind:, identifier: normalized_identifier, resolved_source:, result:)
17+
emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
2218
body
2319
rescue StandardError => error
2420
emit_failure(target_kind:, identifier:, error:)
@@ -27,6 +23,39 @@ def call(request:, target_kind:, identifier:)
2723

2824
private
2925

26+
# @param request [Rack::Request]
27+
# @param target_kind [Symbol]
28+
# @param identifier [String]
29+
# @return [Array<(Html2rss::Web::Feeds::Contracts::Request, Html2rss::Web::Feeds::Contracts::ResolvedSource, Html2rss::Web::Feeds::Contracts::RenderResult)>]
30+
def resolve_request(request:, target_kind:, identifier:)
31+
feed_request = Request.call(request:, target_kind:, identifier:)
32+
resolved_source = SourceResolver.call(feed_request)
33+
result = Service.call(resolved_source)
34+
[feed_request, resolved_source, result]
35+
end
36+
37+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
38+
# @param identifier [String]
39+
# @return [String]
40+
def normalized_identifier(feed_request, identifier)
41+
feed_request.feed_name || identifier
42+
end
43+
44+
# @param target_kind [Symbol]
45+
# @param identifier [String]
46+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
47+
# @param resolved_source [Html2rss::Web::Feeds::Contracts::ResolvedSource]
48+
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]
49+
# @return [void]
50+
def emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
51+
emit_result(
52+
target_kind:,
53+
identifier: normalized_identifier(feed_request, identifier),
54+
resolved_source:,
55+
result:
56+
)
57+
end
58+
3059
# @param response [Rack::Response]
3160
# @param representation [Symbol]
3261
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ 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();
67+
expect(screen.getByText('Feed ready')).toBeInTheDocument();
6868
expect(screen.getByText('Example Feed')).toBeInTheDocument();
6969
expect(screen.getByLabelText('Feed URL')).toBeInTheDocument();
7070
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ describe('ResultDisplay', () => {
4848
it('renders the success state actions and richer preview cards', async () => {
4949
render(<ResultDisplay result={mockResult} onCreateAnother={mockOnCreateAnother} />);
5050

51-
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
51+
expect(screen.getByText('Feed ready')).toBeInTheDocument();
5252
expect(screen.getByText('Test Feed')).toBeInTheDocument();
5353
expect(screen.getByRole('button', { name: 'Copy feed URL' })).toBeInTheDocument();
54-
expect(screen.getByRole('link', { name: 'Subscribe in reader' })).toHaveAttribute(
54+
expect(screen.getByRole('link', { name: 'Open in feed reader' })).toHaveAttribute(
5555
'href',
5656
'feed:https://example.com/feed.xml'
5757
);
@@ -96,7 +96,7 @@ describe('ResultDisplay', () => {
9696
);
9797

9898
await waitFor(() => {
99-
expect(screen.getByText('Your feed is ready')).toBeInTheDocument();
99+
expect(screen.getByText('Feed ready')).toBeInTheDocument();
100100
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
101101
expect(screen.getByText('Loading preview…')).toBeInTheDocument();
102102
});

frontend/src/components/ResultDisplay.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,28 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
4040
return (
4141
<section class="result-shell layout-stack" aria-live="polite">
4242
<header
43-
class="result-hero layout-rail-reading layout-stack"
43+
class="result-hero ui-card ui-card--roomy ui-hero layout-rail-reading layout-stack"
4444
style={{ '--stack-gap': 'var(--space-3)' }}
4545
>
46-
<p class="result-kicker ui-eyebrow">Feed created</p>
47-
<h1 class="result-title">Your feed is ready</h1>
48-
<p class="result-meta layout-rail-copy">{feed.name}</p>
49-
<p class="result-lede layout-rail-copy">Subscribe to this URL in your RSS reader.</p>
46+
<div class="result-hero__masthead ui-hero__masthead">
47+
<div class="result-hero__icon-wrap ui-hero__icon-wrap" aria-hidden="true">
48+
<img class="result-hero__icon ui-hero__icon" src="/feed.svg" alt="" />
49+
</div>
50+
<div class="layout-stack layout-stack--tight">
51+
<h1 class="result-title ui-display-title">Feed ready</h1>
52+
<p class="result-meta layout-rail-copy">{feed.name}</p>
53+
</div>
54+
</div>
55+
<div class="result-hero__actions ui-hero__actions">
56+
{subscribeUrl && (
57+
<a href={subscribeUrl} class="btn btn--ghost result-hero__reader">
58+
Open in feed reader
59+
</a>
60+
)}
61+
<a href={fullUrl} class="btn btn--ghost" target="_blank" rel="noopener noreferrer">
62+
Open feed
63+
</a>
64+
</div>
5065
{result.retry && (
5166
<p class="field-help">
5267
{`Retried automatically with ${result.retry.to} after ${result.retry.from} could not finish the page.`}
@@ -67,14 +82,6 @@ export function ResultDisplay({ result, onCreateAnother }: ResultDisplayProps) {
6782
/>
6883

6984
<div class="result-actions result-actions--quiet layout-rail-reading">
70-
{subscribeUrl && (
71-
<a href={subscribeUrl} class="btn btn--ghost">
72-
Subscribe in reader
73-
</a>
74-
)}
75-
<a href={fullUrl} class="btn btn--ghost" target="_blank" rel="noopener noreferrer">
76-
Open feed
77-
</a>
7885
<a href={jsonFeedUrl} class="btn btn--ghost" target="_blank" rel="noopener noreferrer">
7986
Open JSON Feed
8087
</a>

frontend/src/styles/main.css

Lines changed: 3 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -263,71 +263,6 @@ a:focus-visible {
263263
box-shadow: var(--focus-ring);
264264
}
265265

266-
.btn {
267-
min-height: 3rem;
268-
padding: 0 1.25rem;
269-
display: inline-flex;
270-
align-items: center;
271-
justify-content: center;
272-
border: var(--border-width) solid transparent;
273-
border-radius: 999px;
274-
background: transparent;
275-
color: var(--text-strong);
276-
text-decoration: none;
277-
cursor: pointer;
278-
font-weight: 600;
279-
transition:
280-
transform var(--transition-fast),
281-
background-color var(--transition-fast),
282-
border-color var(--transition-fast),
283-
color var(--transition-fast),
284-
opacity var(--transition-fast);
285-
}
286-
287-
.btn:hover:not(:disabled) {
288-
transform: translateY(-0.04rem);
289-
}
290-
291-
.btn:disabled {
292-
opacity: 0.5;
293-
cursor: not-allowed;
294-
}
295-
296-
.btn--primary {
297-
background: var(--accent);
298-
color: var(--text-inverse);
299-
}
300-
301-
.btn--primary:hover:not(:disabled) {
302-
background: var(--accent-strong);
303-
}
304-
305-
.btn--ghost {
306-
border-color: var(--border-subtle);
307-
background: var(--surface-elevated);
308-
}
309-
310-
.btn--ghost:hover:not(:disabled) {
311-
border-color: var(--border-strong);
312-
background: rgba(255, 255, 255, 0.08);
313-
}
314-
315-
.btn--quiet,
316-
.btn--linkish {
317-
min-height: auto;
318-
padding: 0;
319-
border: 0;
320-
border-radius: 0;
321-
background: transparent;
322-
color: var(--text-muted);
323-
}
324-
325-
.btn--quiet:hover:not(:disabled),
326-
.btn--linkish:hover:not(:disabled) {
327-
background: transparent;
328-
color: var(--text-strong);
329-
}
330-
331266
.notice {
332267
display: grid;
333268
gap: var(--space-2);
@@ -504,18 +439,9 @@ a:focus-visible {
504439
text-align: left;
505440
}
506441

507-
.result-hero {
508-
justify-items: start;
509-
text-align: left;
510-
}
511-
512-
.result-title {
513-
margin: 0;
514-
color: var(--text-strong);
515-
font-family: var(--font-family-display);
516-
font-size: clamp(1.9rem, 4.2vw, 2.85rem);
517-
line-height: 0.98;
518-
letter-spacing: -0.03em;
442+
.result-hero__reader {
443+
border-color: rgba(255, 147, 0, 0.24);
444+
background: rgba(255, 147, 0, 0.12);
519445
}
520446

521447
.result-meta {
@@ -526,12 +452,6 @@ a:focus-visible {
526452
text-align: left;
527453
}
528454

529-
.result-lede {
530-
margin: 0;
531-
color: var(--text-muted);
532-
font-size: var(--font-size-1);
533-
}
534-
535455
.result-preview {
536456
justify-items: start;
537457
padding-top: var(--section-gap);

public/feed-reader-link.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
document.addEventListener('DOMContentLoaded', function () {
2+
var readerLink = document.querySelector('[data-feed-reader-link]');
3+
if (!readerLink) return;
4+
5+
readerLink.setAttribute('href', 'feed:' + window.location.href);
6+
});

0 commit comments

Comments
 (0)