Skip to content

Commit 9e6955b

Browse files
gildesmaraisclaude
andcommitted
refactor(frontend): unify panel layout and purge dead CSS
- Drop surface card from guest panel — all panels now share `workspace` open layout - Replace radio cards with a plain <select> for demo source (consistent with Strategy selector) - Align button vocabulary: btn--ghost for demo run, btn--accent for primary auth/convert actions - "Back to demo" demoted to btn--meta (gray housekeeping, not invitation) - Remove ~95 lines of dead CSS: surface*, onboarding-*, radio-card/list*, fieldset/legend, form--spacious, auth-form-actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 750bece commit 9e6955b

5 files changed

Lines changed: 97 additions & 227 deletions

File tree

frontend/src/components/App.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,7 @@ export function App() {
144144
}
145145

146146
return (
147-
<div
148-
class={`app-shell${mode !== 'result' ? ' app-shell--workspace' : ''}`}
149-
>
147+
<div class="app-shell app-shell--workspace">
150148
{authError && mode !== 'result' && (
151149
<section class="notice notice--error" role="alert">
152150
<h3>Authentication error</h3>

frontend/src/components/AppPanels.tsx

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,51 +75,40 @@ export function GuestOnboardingPanel({
7575
const selectedDemoUrl = demoSources.find((source) => source.id === selectedDemoId)?.url ?? demoSources[0].url;
7676

7777
return (
78-
<section class="surface surface--form">
78+
<section class="workspace">
7979
<div class="panel-meta">
8080
<span />
8181
{mode === 'guest-demo' ? (
8282
<button type="button" class="btn btn--link" onClick={() => onModeChange('guest-auth')}>
8383
Sign in
8484
</button>
8585
) : (
86-
<button type="button" class="btn btn--link" onClick={onBackToDemo}>
87-
Back to demo
86+
<button type="button" class="btn btn--link btn--meta" onClick={onBackToDemo}>
87+
Back to demo
8888
</button>
8989
)}
9090
</div>
9191

92-
<header class="surface-header onboarding-header">
93-
<h2 class="onboarding-title">Convert website to RSS</h2>
94-
<p class="onboarding-subtitle">
95-
{mode === 'guest-demo'
96-
? 'Try a demo source instantly. Sign in to convert your own URLs.'
97-
: 'Sign in to convert any website URL.'}
98-
</p>
99-
</header>
100-
10192
{mode === 'guest-demo' ? (
102-
<div class="form form--spacious form--tight">
103-
<fieldset class="fieldset">
104-
<legend class="legend">Demo source</legend>
105-
<div class="radio-list radio-list--compact">
93+
<div class="form">
94+
<h2 class="workspace-title">Convert website to RSS</h2>
95+
<p class="field-help">Try a demo source instantly. Sign in to convert your own URLs.</p>
96+
97+
<div class="field">
98+
<label for="demo-source" class="label">Demo source</label>
99+
<select
100+
id="demo-source"
101+
class="input"
102+
value={selectedDemoId}
103+
onChange={(e) => setSelectedDemoId((e.target as HTMLSelectElement).value)}
104+
>
106105
{demoSources.map((source) => (
107-
<label key={source.id} class={`radio-card radio-card--compact ${selectedDemoId === source.id ? 'is-selected' : ''}`}>
108-
<input
109-
type="radio"
110-
name="demo-source"
111-
class="radio-card__input"
112-
checked={selectedDemoId === source.id}
113-
onChange={() => setSelectedDemoId(source.id)}
114-
/>
115-
<span class="radio-card__content">
116-
<span class="radio-card__title">{source.label}</span>
117-
<span class="radio-card__hint">{source.hint}</span>
118-
</span>
119-
</label>
106+
<option key={source.id} value={source.id}>
107+
{source.label}{source.hint}
108+
</option>
120109
))}
121-
</div>
122-
</fieldset>
110+
</select>
111+
</div>
123112

124113
{demoError && (
125114
<div class="notice notice--error notice--compact" role="alert">
@@ -129,13 +118,15 @@ export function GuestOnboardingPanel({
129118
)}
130119

131120
<div class="form-actions">
132-
<button type="button" class="btn btn--accent" onClick={() => onConvert(selectedDemoUrl)}>
121+
<button type="button" class="btn btn--ghost" onClick={() => onConvert(selectedDemoUrl)}>
133122
Run demo
134123
</button>
135124
</div>
136125
</div>
137126
) : (
138-
<form id="auth-section" class="form form--spacious form--tight" onSubmit={onAuthSubmit}>
127+
<form id="auth-section" class="form" onSubmit={onAuthSubmit}>
128+
<h2 class="workspace-title">Convert website to RSS</h2>
129+
139130
{authFieldErrors.form && (
140131
<div class="notice notice--error notice--compact" role="alert">
141132
<p>{authFieldErrors.form}</p>
@@ -182,15 +173,15 @@ export function GuestOnboardingPanel({
182173
</div>
183174
</div>
184175

185-
<p class="surface-header__hint">
176+
<p class="field-help">
186177
Need a token? Ask your html2rss-web admin or read the{' '}
187178
<a href="https://html2rss.github.io/" target="_blank" rel="noopener noreferrer">
188179
official docs
189180
</a>
190181
.
191182
</p>
192183

193-
<div class="form-actions auth-form-actions">
184+
<div class="form-actions">
194185
<button type="submit" class="btn btn--accent">
195186
Sign in
196187
</button>
@@ -233,15 +224,16 @@ export function MemberConvertPanel({
233224
const selectedStrategy = strategies.find((strategy) => strategy.id === feedFormData.strategy);
234225

235226
return (
236-
<section class="surface surface--form surface--minimal">
227+
<section class="workspace">
237228
<div class="panel-meta">
238229
<span class="panel-meta__primary">{username}</span>
239230
<button type="button" onClick={onLogout} class="btn btn--link btn--meta">
240231
Log out
241232
</button>
242233
</div>
243234

244-
<form id="feed-section" class="form form--spacious form--tight form--member" onSubmit={onFeedSubmit}>
235+
<form id="feed-section" class="form" onSubmit={onFeedSubmit}>
236+
<h2 class="workspace-title">Convert a website</h2>
245237
<div class="field">
246238
<label for="url" class="label" data-required>URL</label>
247239
<div class="field field--inline">
@@ -257,7 +249,7 @@ export function MemberConvertPanel({
257249
value={feedFormData.url}
258250
onInput={(e) => onFeedFieldChange('url', (e.target as HTMLInputElement).value)}
259251
/>
260-
<button type="submit" class="btn btn--quiet-accent" disabled={isConverting}>
252+
<button type="submit" class="btn btn--accent" disabled={isConverting}>
261253
{isConverting ? 'Converting...' : 'Convert'}
262254
</button>
263255
</div>

frontend/src/components/ResultDisplay.module.css

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
.result {
22
display: grid;
3-
gap: 0.75rem;
3+
gap: 1rem;
44
}
55

66
.hero {
77
display: grid;
8-
gap: 0.25rem;
8+
gap: 0.2rem;
99
}
1010

1111
.heroTitle {
1212
margin: 0;
13-
font-size: 1.35rem;
13+
font-size: 1.5rem;
14+
font-weight: 700;
1415
line-height: 1.2;
1516
}
1617

@@ -26,15 +27,9 @@
2627
}
2728

2829
.heroActions {
29-
display: grid;
30+
display: flex;
3031
gap: 0.75rem;
31-
}
32-
33-
@media (min-width: 640px) {
34-
.heroActions {
35-
grid-auto-flow: column;
36-
grid-auto-columns: 1fr;
37-
}
32+
flex-wrap: wrap;
3833
}
3934

4035
.preview {
@@ -80,6 +75,5 @@
8075
}
8176

8277
.footer {
83-
display: flex;
84-
justify-content: flex-start;
78+
justify-self: start;
8579
}

frontend/src/components/ResultDisplay.tsx

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
import { useState, useEffect } from 'preact/hooks';
2+
import { renderFeedByToken } from '../api/generated';
3+
import { apiClient } from '../api/client';
4+
import type { FeedRecord } from '../api/contracts';
25
import styles from './ResultDisplay.module.css';
36

4-
interface ConversionResult {
5-
id: string;
6-
name: string;
7-
url: string;
8-
username: string;
9-
strategy: string;
10-
public_url: string;
11-
}
12-
137
interface ResultDisplayProps {
14-
result: ConversionResult;
8+
result: FeedRecord;
159
onClose: () => void;
1610
isAuthenticated?: boolean;
1711
username?: string;
@@ -49,8 +43,18 @@ export function ResultDisplay({
4943

5044
const loadPreview = async () => {
5145
try {
52-
const response = await fetch(fullUrl, { signal: controller.signal });
53-
const content = await response.text();
46+
const token = extractFeedToken(result.public_url);
47+
if (!token) throw new Error('Missing feed token');
48+
49+
const content = await renderFeedByToken({
50+
client: apiClient,
51+
path: { token },
52+
parseAs: 'text',
53+
signal: controller.signal,
54+
responseStyle: 'data',
55+
});
56+
if (typeof content !== 'string') throw new Error('Invalid feed preview response');
57+
5458
const xmlDoc = new DOMParser().parseFromString(content, 'text/xml');
5559

5660
const parsedTitle =
@@ -80,7 +84,7 @@ export function ResultDisplay({
8084
return () => {
8185
controller.abort();
8286
};
83-
}, [fullUrl]);
87+
}, [result.public_url]);
8488

8589
const copyToClipboard = async (text: string) => {
8690
try {
@@ -95,16 +99,19 @@ export function ResultDisplay({
9599
const shouldShowPreview = isLoadingPreview || Boolean(feedTitle) || feedItems.length > 0;
96100

97101
return (
98-
<section id="result-display" class={`surface ${styles.result}`} aria-live="polite">
102+
<section id="result-display" class={styles.result} aria-live="polite">
99103
<div class="panel-meta">
100104
<span class="panel-meta__primary">{isAuthenticated ? (username ?? '') : ''}</span>
101-
{isAuthenticated && onLogout ? (
102-
<button type="button" class="btn btn--link btn--meta" onClick={onLogout}>
103-
Log out
105+
<span class="panel-meta__actions">
106+
<button type="button" class="btn btn--link btn--meta" onClick={onClose}>
107+
← Convert another
104108
</button>
105-
) : (
106-
<span />
107-
)}
109+
{isAuthenticated && onLogout && (
110+
<button type="button" class="btn btn--link btn--meta" onClick={onLogout}>
111+
Log out
112+
</button>
113+
)}
114+
</span>
108115
</div>
109116

110117
<header class={styles.hero}>
@@ -161,10 +168,21 @@ export function ResultDisplay({
161168
)}
162169

163170
<div class={styles.footer}>
164-
<button type="button" class="btn btn--accent" onClick={onClose}>
165-
Convert another website
171+
<button type="button" class="btn btn--link btn--meta" onClick={onClose}>
172+
Convert another website
166173
</button>
167174
</div>
168175
</section>
169176
);
170177
}
178+
179+
const extractFeedToken = (publicUrl: string): string | null => {
180+
const path = publicUrl.startsWith('http')
181+
? new URL(publicUrl).pathname
182+
: publicUrl;
183+
const segments = path.split('/').filter(Boolean);
184+
const feedIndex = segments.findIndex((segment) => segment === 'feeds');
185+
if (feedIndex < 0) return null;
186+
187+
return segments[feedIndex + 1] ?? null;
188+
};

0 commit comments

Comments
 (0)