Skip to content

Commit ce58ba2

Browse files
Move help and settings buttons to the footer
Extract footer into a client component that owns the help/settings modals. Chooser no longer renders them. useSettings syncs across multiple hook instances via a custom DOM event so the footer's settings modal updates Chooser's timespanFormat in real time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a709cb commit ce58ba2

5 files changed

Lines changed: 112 additions & 50 deletions

File tree

src/app/layout.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
22
import { Source_Serif_4 } from "next/font/google";
33
import "@/styles/globals.css";
44
import Header from "@/components/Header";
5+
import Footer from "@/components/Footer";
56

67
const sourceSerif = Source_Serif_4({
78
subsets: ["latin"],
@@ -58,23 +59,7 @@ export default function RootLayout({
5859
<a href="#main-content" className="skip-link">Skip to content</a>
5960
<Header />
6061
<main id="main-content">{children}</main>
61-
<footer
62-
style={{
63-
textAlign: "center",
64-
padding: "32px 16px 24px",
65-
fontSize: "0.8rem",
66-
color: "var(--color-text-light)",
67-
}}
68-
>
69-
A progressive web app from{" "}
70-
<a href="https://lopo.it" rel="author noopener" target="_blank" style={{ color: "var(--color-primary)", textDecoration: "underline" }}>
71-
Lopo.it
72-
</a>
73-
{" "}&middot;{" "}
74-
<a href="https://github.com/enricobattocchi/closerintime-node" rel="noopener" target="_blank" style={{ color: "var(--color-primary)", textDecoration: "underline" }}>
75-
GitHub
76-
</a>
77-
</footer>
62+
<Footer />
7863
</body>
7964
</html>
8065
);

src/components/Chooser/Chooser.tsx

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,8 @@ import Timeline from "@/components/Timeline/Timeline";
1616

1717
const AddEventForm = dynamic(() => import("./AddEventForm"));
1818
import Sentence from "@/components/Sentence";
19-
import { HelpOutline, SettingsOutlined } from "@/components/Icon";
2019
import styles from "@/styles/Chooser.module.css";
2120

22-
const HelpModal = dynamic(() => import("@/components/HelpModal"));
23-
const SettingsModal = dynamic(() => import("@/components/SettingsModal"));
2421
const BrowseModal = dynamic(() => import("@/components/BrowseModal"));
2522

2623
interface ChooserProps {
@@ -45,13 +42,11 @@ export default function Chooser({
4542
const router = useRouter();
4643
const cachedEvents = useCachedEvents(allEvents);
4744
const { localEvents, addEvent, updateEvent: updateLocalEvent, deleteEvent: deleteLocalEvent } = useLocalEvents();
48-
const { timespanFormat, updateTimespanFormat, theme, updateTheme } = useSettings();
45+
const { timespanFormat } = useSettings();
4946
const [selectedLocalEvents, setSelectedLocalEvents] = useState<Event[]>(urlCustomEvents);
5047
const [offlineServerEvents, setOfflineServerEvents] = useState<Event[]>([]);
5148
const [showAddForm, setShowAddForm] = useState(false);
5249
const [editingSlot, setEditingSlot] = useState<number | null>(null);
53-
const [showSettings, setShowSettings] = useState(false);
54-
const [showHelp, setShowHelp] = useState(false);
5550
const [showBrowse, setShowBrowse] = useState(false);
5651
const [isOffline, setIsOffline] = useState(false);
5752

@@ -231,22 +226,6 @@ export default function Chooser({
231226
<div className={styles.chooser}>
232227
<div className={styles.headingRow}>
233228
<p className={styles.heading}>Pick some events or <button className={styles.browseLink} onClick={() => setShowBrowse(true)}>browse</button></p>
234-
<button
235-
className={styles.iconButton}
236-
onClick={() => setShowHelp(true)}
237-
aria-label="Help"
238-
title="Help"
239-
>
240-
<HelpOutline size={20} />
241-
</button>
242-
<button
243-
className={styles.iconButton}
244-
onClick={() => setShowSettings(true)}
245-
aria-label="Settings"
246-
title="Settings"
247-
>
248-
<SettingsOutlined size={20} />
249-
</button>
250229
</div>
251230
{slots.map((event, i) => (
252231
<div key={event ? event.id : `empty-${i}`} className={styles.slot}>
@@ -377,16 +356,6 @@ export default function Chooser({
377356
onClose={() => setShowBrowse(false)}
378357
/>
379358
)}
380-
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
381-
{showSettings && (
382-
<SettingsModal
383-
timespanFormat={timespanFormat}
384-
onSave={updateTimespanFormat}
385-
theme={theme}
386-
onThemeChange={updateTheme}
387-
onClose={() => setShowSettings(false)}
388-
/>
389-
)}
390359
</>
391360
);
392361
}

src/components/Footer.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import dynamic from "next/dynamic";
5+
import { useSettings } from "@/hooks/useSettings";
6+
import { HelpOutline, SettingsOutlined } from "@/components/Icon";
7+
import styles from "@/styles/Footer.module.css";
8+
9+
const HelpModal = dynamic(() => import("@/components/HelpModal"));
10+
const SettingsModal = dynamic(() => import("@/components/SettingsModal"));
11+
12+
export default function Footer() {
13+
const [showHelp, setShowHelp] = useState(false);
14+
const [showSettings, setShowSettings] = useState(false);
15+
const { timespanFormat, updateTimespanFormat, theme, updateTheme } = useSettings();
16+
17+
return (
18+
<>
19+
<footer className={styles.footer}>
20+
<div className={styles.actions}>
21+
<button
22+
className={styles.iconButton}
23+
onClick={() => setShowHelp(true)}
24+
aria-label="Help"
25+
title="Help"
26+
>
27+
<HelpOutline size={18} />
28+
</button>
29+
<button
30+
className={styles.iconButton}
31+
onClick={() => setShowSettings(true)}
32+
aria-label="Settings"
33+
title="Settings"
34+
>
35+
<SettingsOutlined size={18} />
36+
</button>
37+
</div>
38+
<p className={styles.credit}>
39+
A progressive web app from{" "}
40+
<a href="https://lopo.it" rel="author noopener" target="_blank">
41+
Lopo.it
42+
</a>
43+
{" "}&middot;{" "}
44+
<a href="https://github.com/enricobattocchi/closerintime-node" rel="noopener" target="_blank">
45+
GitHub
46+
</a>
47+
</p>
48+
</footer>
49+
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
50+
{showSettings && (
51+
<SettingsModal
52+
timespanFormat={timespanFormat}
53+
onSave={updateTimespanFormat}
54+
theme={theme}
55+
onThemeChange={updateTheme}
56+
onClose={() => setShowSettings(false)}
57+
/>
58+
)}
59+
</>
60+
);
61+
}

src/hooks/useSettings.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import type { TimespanFormat } from "@/lib/types";
55

66
const STORAGE_KEY = "timespanformat";
77
const THEME_KEY = "theme";
8+
const SETTINGS_CHANGE_EVENT = "settings-change";
89

910
export type Theme = "system" | "light" | "dark";
1011

12+
function notifySettingsChange() {
13+
window.dispatchEvent(new Event(SETTINGS_CHANGE_EVENT));
14+
}
15+
1116
export function useSettings() {
1217
const [timespanFormat, setTimespanFormat] = useState<TimespanFormat>(2);
1318
const [theme, setTheme] = useState<Theme>("system");
1419

15-
useEffect(() => {
20+
const readFromStorage = useCallback(() => {
1621
try {
1722
const stored = localStorage.getItem(STORAGE_KEY);
1823
if (stored !== null) {
@@ -21,15 +26,24 @@ export function useSettings() {
2126
const storedTheme = localStorage.getItem(THEME_KEY);
2227
if (storedTheme === "light" || storedTheme === "dark") {
2328
setTheme(storedTheme);
29+
} else {
30+
setTheme("system");
2431
}
2532
} catch {}
2633
}, []);
2734

35+
useEffect(() => {
36+
readFromStorage();
37+
window.addEventListener(SETTINGS_CHANGE_EVENT, readFromStorage);
38+
return () => window.removeEventListener(SETTINGS_CHANGE_EVENT, readFromStorage);
39+
}, [readFromStorage]);
40+
2841
const updateTimespanFormat = useCallback((format: TimespanFormat) => {
2942
setTimespanFormat(format);
3043
try {
3144
localStorage.setItem(STORAGE_KEY, String(format));
3245
} catch {}
46+
notifySettingsChange();
3347
}, []);
3448

3549
const updateTheme = useCallback((t: Theme) => {
@@ -43,6 +57,7 @@ export function useSettings() {
4357
document.documentElement.setAttribute("data-theme", t);
4458
}
4559
} catch {}
60+
notifySettingsChange();
4661
}, []);
4762

4863
return { timespanFormat, updateTimespanFormat, theme, updateTheme };

src/styles/Footer.module.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.footer {
2+
text-align: center;
3+
padding: 24px 16px;
4+
font-size: 0.8rem;
5+
color: var(--color-text-light);
6+
}
7+
8+
.actions {
9+
display: flex;
10+
justify-content: center;
11+
gap: 4px;
12+
margin-bottom: 8px;
13+
}
14+
15+
.iconButton {
16+
background: none;
17+
border: none;
18+
cursor: pointer;
19+
color: var(--color-text-light);
20+
padding: 6px;
21+
display: inline-flex;
22+
align-items: center;
23+
}
24+
25+
.iconButton:hover {
26+
color: var(--color-primary);
27+
}
28+
29+
.credit a {
30+
color: var(--color-primary);
31+
text-decoration: underline;
32+
}

0 commit comments

Comments
 (0)