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 Link from 'next/link';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
import { useTheme } from '@/contexts/ThemeContext';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
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 { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||||
|
|
@ -574,36 +574,36 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isChangelogOpen && (
|
{isChangelogOpen ? (
|
||||||
<SettingsChangelogPanel
|
<SettingsChangelogPanel
|
||||||
appVersion={runtimeConfig.appVersion}
|
appVersion={runtimeConfig.appVersion}
|
||||||
manifestUrl={runtimeConfig.changelogFeedUrl}
|
manifestUrl={runtimeConfig.changelogFeedUrl}
|
||||||
onClose={() => setIsChangelogOpen(false)}
|
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="flex flex-row h-[490px]">
|
||||||
<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]">
|
|
||||||
{/* Desktop: vertical sidebar */}
|
{/* Desktop: vertical sidebar */}
|
||||||
<nav className="hidden sm:block w-44 shrink-0 border-r border-offbase bg-background p-2">
|
<nav className="hidden sm:block w-44 shrink-0 border-r border-offbase bg-background p-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
@ -1314,7 +1314,9 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1391,6 +1393,11 @@ function SettingsChangelogPanel({
|
||||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||||
const [bodies, setBodies] = useState<Record<string, ChangelogReleaseBody>>({});
|
const [bodies, setBodies] = useState<Record<string, ChangelogReleaseBody>>({});
|
||||||
const normalizedAppVersion = normalizeVersion(appVersion || '');
|
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(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -1406,9 +1413,10 @@ function SettingsChangelogPanel({
|
||||||
setExpanded((prev) => ({ ...prev, [entry.tag_name]: true }));
|
setExpanded((prev) => ({ ...prev, [entry.tag_name]: true }));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (isAbortError(err)) return;
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load changelog');
|
setError(err instanceof Error ? err.message : 'Failed to load changelog');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!controller.signal.aborted) setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void loadManifest();
|
void loadManifest();
|
||||||
|
|
@ -1429,7 +1437,8 @@ function SettingsChangelogPanel({
|
||||||
try {
|
try {
|
||||||
const body = await fetchChangelogReleaseBody(manifestUrl, entry.body_path, controller.signal);
|
const body = await fetchChangelogReleaseBody(manifestUrl, entry.body_path, controller.signal);
|
||||||
setBodies((prev) => ({ ...prev, [tag]: body }));
|
setBodies((prev) => ({ ...prev, [tag]: body }));
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
if (isAbortError(err)) return;
|
||||||
// Keep entry expanded; inline fallback appears below.
|
// Keep entry expanded; inline fallback appears below.
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
@ -1439,8 +1448,18 @@ function SettingsChangelogPanel({
|
||||||
}, [expanded, manifest, manifestUrl, bodies]);
|
}, [expanded, manifest, manifestUrl, bodies]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-x-0 top-[65px] bottom-0 z-20 bg-base border-t border-offbase">
|
<div className="h-[490px] flex flex-col bg-base">
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-offbase bg-background">
|
<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">
|
<div className="min-w-0">
|
||||||
<h4 className="text-sm font-semibold text-foreground">Changelog</h4>
|
<h4 className="text-sm font-semibold text-foreground">Changelog</h4>
|
||||||
<p className="text-xs text-muted truncate">
|
<p className="text-xs text-muted truncate">
|
||||||
|
|
@ -1449,30 +1468,24 @@ function SettingsChangelogPanel({
|
||||||
: 'Release history from GitHub'}
|
: 'Release history from GitHub'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-sm font-medium text-muted hover:text-accent transition-colors"
|
|
||||||
>
|
|
||||||
Back to settings
|
|
||||||
</Button>
|
|
||||||
</div>
|
</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 && (
|
{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…
|
Loading changelog…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && error && (
|
{!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-sm text-foreground">Could not load changelog right now.</p>
|
||||||
<p className="text-xs text-muted break-words">{error}</p>
|
<p className="text-xs text-muted break-words">{error}</p>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/richardr1126/openreader/releases"
|
href="https://github.com/richardr1126/openreader/releases"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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
|
Open GitHub Releases
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -1480,7 +1493,7 @@ function SettingsChangelogPanel({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && manifest.length === 0 && (
|
{!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.
|
No releases found.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1489,42 +1502,46 @@ function SettingsChangelogPanel({
|
||||||
const isCurrent = normalizedAppVersion && normalizeVersion(entry.tag_name) === normalizedAppVersion;
|
const isCurrent = normalizedAppVersion && normalizeVersion(entry.tag_name) === normalizedAppVersion;
|
||||||
const body = bodies[entry.tag_name];
|
const body = bodies[entry.tag_name];
|
||||||
const isExpanded = !!expanded[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 (
|
return (
|
||||||
<div
|
<div key={entry.tag_name} className="border-b border-offbase">
|
||||||
key={entry.tag_name}
|
|
||||||
className={`rounded-lg border bg-background overflow-hidden ${
|
|
||||||
isCurrent ? 'border-accent' : 'border-offbase'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded((prev) => ({ ...prev, [entry.tag_name]: !isExpanded }))}
|
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">
|
<ChevronRightIcon
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
className={`w-3.5 h-3.5 shrink-0 text-muted transition-transform ${
|
||||||
<span className="text-sm font-semibold text-foreground truncate">{entry.tag_name}</span>
|
isExpanded ? 'rotate-90 text-foreground' : ''
|
||||||
{entry.prerelease && (
|
}`}
|
||||||
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-muted">
|
/>
|
||||||
prerelease
|
<div className="min-w-0 flex items-center gap-2 text-sm w-full">
|
||||||
</span>
|
<span className="font-semibold text-foreground shrink-0">{entry.tag_name}</span>
|
||||||
)}
|
{entry.prerelease && (
|
||||||
{isCurrent && (
|
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-muted shrink-0">
|
||||||
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-accent">
|
prerelease
|
||||||
current
|
</span>
|
||||||
</span>
|
)}
|
||||||
)}
|
{isCurrent && (
|
||||||
</div>
|
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-accent shrink-0">
|
||||||
<p className="text-xs text-muted truncate">{entry.name}</p>
|
current
|
||||||
<p className="text-[11px] text-muted">
|
</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()}
|
{new Date(entry.published_at).toLocaleDateString()}
|
||||||
</p>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted">{isExpanded ? 'Hide' : 'Show'}</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isExpanded && (
|
{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 ? (
|
{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">
|
<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]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
|
@ -1538,7 +1555,7 @@ function SettingsChangelogPanel({
|
||||||
href={entry.html_url}
|
href={entry.html_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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
|
View on GitHub
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue