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:
parent
9e09f80067
commit
5efb29cda9
1 changed files with 85 additions and 68 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue