refactor(ui): restructure settings modal navigation and changelog panel logic

Update SettingsModal to use conditional rendering for the changelog panel,
displaying the main settings navigation only when the changelog is closed.
Refactor mobile and desktop navigation layout to improve maintainability.
Add missing ChevronRightIcon import for icon consistency.
This commit is contained in:
Richard R 2026-05-14 10:01:51 -06:00
parent 9e09f80067
commit 5efb29cda9

View file

@ -17,7 +17,7 @@ import {
import Link from 'next/link';
import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext';
import { ChevronUpDownIcon, CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon } from '@/components/icons/Icons';
import { ChevronUpDownIcon, CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon, ChevronRightIcon } from '@/components/icons/Icons';
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie';
import { useDocuments } from '@/contexts/DocumentContext';
import { ConfirmDialog } from '@/components/ConfirmDialog';
@ -574,36 +574,36 @@ export function SettingsModal({ className = '' }: { className?: string }) {
</div>
</div>
{isChangelogOpen && (
{isChangelogOpen ? (
<SettingsChangelogPanel
appVersion={runtimeConfig.appVersion}
manifestUrl={runtimeConfig.changelogFeedUrl}
onClose={() => setIsChangelogOpen(false)}
/>
)}
) : (
<>
{/* Mobile: 2x2 grid nav */}
<div className="grid grid-cols-2 gap-1 sm:hidden border-b border-offbase bg-background p-2">
{visibleSections.map((section) => {
const Icon = section.icon;
return (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-medium transition-colors ${
activeSection === section.id
? 'bg-accent text-background'
: 'text-foreground hover:bg-offbase hover:text-accent'
}`}
>
<Icon className="w-3.5 h-3.5" />
{section.label}
</button>
);
})}
</div>
{/* Mobile: 2x2 grid nav */}
<div className="grid grid-cols-2 gap-1 sm:hidden border-b border-offbase bg-background p-2">
{visibleSections.map((section) => {
const Icon = section.icon;
return (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-medium transition-colors ${
activeSection === section.id
? 'bg-accent text-background'
: 'text-foreground hover:bg-offbase hover:text-accent'
}`}
>
<Icon className="w-3.5 h-3.5" />
{section.label}
</button>
);
})}
</div>
<div className="flex flex-row h-[490px]">
<div className="flex flex-row h-[490px]">
{/* Desktop: vertical sidebar */}
<nav className="hidden sm:block w-44 shrink-0 border-r border-offbase bg-background p-2">
<div className="flex flex-col gap-1">
@ -1314,7 +1314,9 @@ export function SettingsModal({ className = '' }: { className?: string }) {
</div>
)}
</div>
</div>
</div>
</>
)}
</DialogPanel>
</TransitionChild>
</div>
@ -1391,6 +1393,11 @@ function SettingsChangelogPanel({
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [bodies, setBodies] = useState<Record<string, ChangelogReleaseBody>>({});
const normalizedAppVersion = normalizeVersion(appVersion || '');
const isAbortError = (err: unknown): boolean => {
return err instanceof DOMException
? err.name === 'AbortError'
: !!(typeof err === 'object' && err && 'name' in err && (err as { name?: string }).name === 'AbortError');
};
useEffect(() => {
const controller = new AbortController();
@ -1406,9 +1413,10 @@ function SettingsChangelogPanel({
setExpanded((prev) => ({ ...prev, [entry.tag_name]: true }));
}
} catch (err) {
if (isAbortError(err)) return;
setError(err instanceof Error ? err.message : 'Failed to load changelog');
} finally {
setLoading(false);
if (!controller.signal.aborted) setLoading(false);
}
}
void loadManifest();
@ -1429,7 +1437,8 @@ function SettingsChangelogPanel({
try {
const body = await fetchChangelogReleaseBody(manifestUrl, entry.body_path, controller.signal);
setBodies((prev) => ({ ...prev, [tag]: body }));
} catch {
} catch (err) {
if (isAbortError(err)) return;
// Keep entry expanded; inline fallback appears below.
}
}));
@ -1439,8 +1448,18 @@ function SettingsChangelogPanel({
}, [expanded, manifest, manifestUrl, bodies]);
return (
<div className="absolute inset-x-0 top-[65px] bottom-0 z-20 bg-base border-t border-offbase">
<div className="flex items-center justify-between px-4 py-3 border-b border-offbase bg-background">
<div className="h-[490px] flex flex-col bg-base">
<div className="flex items-center gap-3 px-4 py-3 border-b border-offbase bg-background">
<Button
onClick={onClose}
className="inline-flex items-center justify-center rounded-md text-muted hover:text-accent hover:bg-base transition-all duration-200 ease-in-out transform hover:scale-[1.08]"
aria-label="Back to settings"
title="Back"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</Button>
<div className="min-w-0">
<h4 className="text-sm font-semibold text-foreground">Changelog</h4>
<p className="text-xs text-muted truncate">
@ -1449,30 +1468,24 @@ function SettingsChangelogPanel({
: 'Release history from GitHub'}
</p>
</div>
<Button
onClick={onClose}
className="text-sm font-medium text-muted hover:text-accent transition-colors"
>
Back to settings
</Button>
</div>
<div className="h-[calc(100%-57px)] overflow-y-auto p-3 space-y-2">
<div className="flex-1 overflow-y-auto px-3 pb-3">
{loading && (
<div className="rounded-lg border border-offbase bg-background p-3 text-sm text-muted">
<div className="py-3 text-sm text-muted">
Loading changelog
</div>
)}
{!loading && error && (
<div className="rounded-lg border border-offbase bg-background p-3 space-y-2">
<div className="py-3 space-y-2 border-b border-offbase">
<p className="text-sm text-foreground">Could not load changelog right now.</p>
<p className="text-xs text-muted break-words">{error}</p>
<a
href="https://github.com/richardr1126/openreader/releases"
target="_blank"
rel="noreferrer"
className="text-xs font-medium text-accent hover:underline"
className="inline-flex text-xs font-medium text-accent hover:underline transition-all duration-200 ease-in-out transform hover:scale-[1.02]"
>
Open GitHub Releases
</a>
@ -1480,7 +1493,7 @@ function SettingsChangelogPanel({
)}
{!loading && !error && manifest.length === 0 && (
<div className="rounded-lg border border-offbase bg-background p-3 text-sm text-muted">
<div className="py-3 text-sm text-muted">
No releases found.
</div>
)}
@ -1489,42 +1502,46 @@ function SettingsChangelogPanel({
const isCurrent = normalizedAppVersion && normalizeVersion(entry.tag_name) === normalizedAppVersion;
const body = bodies[entry.tag_name];
const isExpanded = !!expanded[entry.tag_name];
const normalizedTag = normalizeVersion(entry.tag_name);
const normalizedName = normalizeVersion(entry.name || '');
const showName = Boolean(entry.name) && normalizedName !== normalizedTag;
return (
<div
key={entry.tag_name}
className={`rounded-lg border bg-background overflow-hidden ${
isCurrent ? 'border-accent' : 'border-offbase'
}`}
>
<div key={entry.tag_name} className="border-b border-offbase">
<button
type="button"
onClick={() => setExpanded((prev) => ({ ...prev, [entry.tag_name]: !isExpanded }))}
className="w-full text-left px-3 py-2 flex items-center justify-between gap-3 hover:bg-base transition-colors"
className="w-full text-left py-2 flex items-center gap-2 hover:bg-base transition-all duration-200 ease-in-out transform hover:scale-[1.01]"
>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-foreground truncate">{entry.tag_name}</span>
{entry.prerelease && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-muted">
prerelease
</span>
)}
{isCurrent && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-accent">
current
</span>
)}
</div>
<p className="text-xs text-muted truncate">{entry.name}</p>
<p className="text-[11px] text-muted">
<ChevronRightIcon
className={`w-3.5 h-3.5 shrink-0 text-muted transition-transform ${
isExpanded ? 'rotate-90 text-foreground' : ''
}`}
/>
<div className="min-w-0 flex items-center gap-2 text-sm w-full">
<span className="font-semibold text-foreground shrink-0">{entry.tag_name}</span>
{entry.prerelease && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-muted shrink-0">
prerelease
</span>
)}
{isCurrent && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-accent shrink-0">
current
</span>
)}
{showName && (
<span className="text-xs text-muted truncate">
{entry.name}
</span>
)}
<span className="text-[11px] text-muted shrink-0">
{new Date(entry.published_at).toLocaleDateString()}
</p>
</span>
</div>
<span className="text-xs text-muted">{isExpanded ? 'Hide' : 'Show'}</span>
</button>
{isExpanded && (
<div className="px-3 pb-3 pt-1 border-t border-offbase space-y-2">
<div className="pl-6 pr-1 pb-3 pt-1 space-y-2">
{body ? (
<div className="text-sm text-foreground leading-6 space-y-2 [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_ul]:pl-5 [&_ol]:pl-5 [&_code]:bg-offbase [&_code]:rounded [&_code]:px-1 [&_pre]:bg-offbase [&_pre]:rounded [&_pre]:p-2 [&_pre]:overflow-x-auto">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
@ -1538,7 +1555,7 @@ function SettingsChangelogPanel({
href={entry.html_url}
target="_blank"
rel="noreferrer"
className="text-xs font-medium text-accent hover:underline"
className="inline-flex text-xs font-medium text-accent hover:underline transition-all duration-200 ease-in-out transform hover:scale-[1.02]"
>
View on GitHub
</a>