feat: dashboard UI/UX overhaul from full review
Shared foundation: - New Modal component on native <dialog>.showModal() (focus trap, inert background, Escape, focus restore, scroll lock); all eight dialogs and both globalThis.confirm calls migrated to it - Design tokens for success/warning/danger surfaces; pill danger variant; .sr-only utility; toast layering above dialog backdrops; not-allowed cursor for validity-disabled buttons Conversations/attention: auto-scroll with jump-to-latest, per-conversation draft persistence, suggestion undo, 30s auto-refresh with refresh controls, unread emphasis and counts, blur-consistent share dialog, empty-accounts guidance, collapsed user lookup, relative timestamps, danger pills for critical items Media/library/outreach: click-blurred-to-reveal, lightbox with arrow navigation and captions, dirty-form preservation across refetches, upload validation with thumbnail preview, save feedback, result counts with load-more and clear-filters, ticking countdowns from a single clock Overview/performance/operations/settings: mutation errors inside the confirm dialog, live sending-window countdown, offline-aware loading states, per-panel loading/error with retry, 7/30/90-day range toggle, console scroll yielding with shown/retained counts, risky-save matching by subreddit name instead of index, useBlocker unsaved-changes guard, single-pass validation with focus-to-first-error, access panel JSON validation and input reset Tests: 806 -> 844 passing; both typechecks clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82be197b79
commit
e984bf20ac
46 files changed
+2462
-495
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
|
||||
import {
|
||||
useImportRedditSessionMutation,
|
||||
@@ -27,16 +27,27 @@ export function AccessPanel() {
|
||||
const accountsQuery = useRedditAccountsQuery();
|
||||
const importMutation = useImportRedditSessionMutation();
|
||||
const importInput = useRef<HTMLInputElement>(null);
|
||||
const [importFileError, setImportFileError] = useState<string | null>(null);
|
||||
const resetImportMutation = importMutation.reset;
|
||||
|
||||
useEffect(() => {
|
||||
if (!importMutation.isSuccess) return;
|
||||
const timer = globalThis.setTimeout(() => resetImportMutation(), 5_000);
|
||||
return () => globalThis.clearTimeout(timer);
|
||||
}, [importMutation.isSuccess, resetImportMutation]);
|
||||
|
||||
async function importSession(file: File | undefined) {
|
||||
if (!file) return;
|
||||
setImportFileError(null);
|
||||
let exported: unknown;
|
||||
try {
|
||||
const exported: unknown = JSON.parse(await file.text());
|
||||
importMutation.mutate(exported);
|
||||
exported = JSON.parse(await file.text());
|
||||
} catch {
|
||||
importMutation.reset();
|
||||
importMutation.mutate({});
|
||||
setImportFileError("That file isn't valid JSON. Export a fresh session dump from the extension and try again.");
|
||||
return;
|
||||
}
|
||||
importMutation.mutate(exported);
|
||||
}
|
||||
|
||||
if (statusQuery.isPending) {
|
||||
@@ -60,6 +71,13 @@ export function AccessPanel() {
|
||||
<p className="settings-copy">Add each Reddit account with its own extension session dump. Content and sync state stay owned by that account in the database. Any account with an expired login pauses while waiting for a fresh session dump; other accounts keep running.</p>
|
||||
{username ? <p className="settings-copy">Verified account: <strong><RedditUsernameLink username={username} /></strong>.</p> : null}
|
||||
<div aria-label="Reddit accounts" className="reddit-account-list">
|
||||
{accountsQuery.isPending ? <p className="settings-copy" role="status"><span className="spinner" /> Loading Reddit accounts…</p> : null}
|
||||
{accountsQuery.isError ? (
|
||||
<div className="settings-notice error" role="alert">
|
||||
<span>Could not load Reddit accounts: {errorMessage(accountsQuery.error)}</span>{' '}
|
||||
<button className="button secondary" onClick={() => void accountsQuery.refetch()} type="button">Retry accounts</button>
|
||||
</div>
|
||||
) : null}
|
||||
{(accountsQuery.data ?? []).map((account) => (
|
||||
<div className={account.isRunning ? 'reddit-account-row is-active' : 'reddit-account-row'} key={account.accountId}>
|
||||
<div><strong><RedditUsernameLink username={account.redditUsername} /></strong><span>{account.isRunning ? 'Running independently' : 'Waiting for a refreshed session'}</span></div>
|
||||
@@ -69,11 +87,24 @@ export function AccessPanel() {
|
||||
{accountsQuery.isSuccess && accountsQuery.data.length === 0 ? <p className="settings-copy">No Reddit accounts have been added yet.</p> : null}
|
||||
</div>
|
||||
<div className="access-actions">
|
||||
<input accept="application/json" aria-label="Reddit session export" className="sr-only" onChange={(event) => void importSession(event.target.files?.[0])} ref={importInput} type="file" />
|
||||
<input
|
||||
accept="application/json"
|
||||
aria-label="Reddit session export"
|
||||
className="sr-only"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
// Reset the input so picking the same file again re-triggers change.
|
||||
event.target.value = '';
|
||||
void importSession(file);
|
||||
}}
|
||||
ref={importInput}
|
||||
type="file"
|
||||
/>
|
||||
<button className="button primary" disabled={importMutation.isPending} onClick={() => importInput.current?.click()} type="button">{importMutation.isPending ? 'Adding account...' : 'Add or refresh account'}</button>
|
||||
<button className="button secondary" disabled={statusQuery.isFetching} onClick={() => void statusQuery.refetch()} type="button">{statusQuery.isFetching ? 'Checking sessions...' : 'Recheck accounts'}</button>
|
||||
<button aria-busy={statusQuery.isFetching ? 'true' : undefined} className="button secondary" disabled={statusQuery.isFetching} onClick={() => void statusQuery.refetch()} type="button">{statusQuery.isFetching ? 'Checking sessions...' : 'Recheck accounts'}</button>
|
||||
</div>
|
||||
<p className="settings-copy">Open Reddit Chat in Chrome for the account you want to add, wait for the extension badge to show OK, then upload its JSON dump. It contains login credentials; do not share it.</p>
|
||||
{importFileError ? <div className="settings-notice error" role="alert">{importFileError}</div> : null}
|
||||
{importMutation.isSuccess ? <div className="settings-notice success" role="status"><span>Reddit session imported and verified.</span> <span>Account runtime is running independently.</span></div> : null}
|
||||
{importMutation.isError ? <div className="settings-notice error" role="alert">{errorMessage(importMutation.error)}</div> : null}
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user