Clean up
This commit is contained in:
parent
d487282fa5
commit
dc6053fc71
@ -121,9 +121,9 @@ Checks and fix:
|
|||||||
`_fresh/server.js` and `_fresh/static/` are found (e.g. in systemd set
|
`_fresh/server.js` and `_fresh/static/` are found (e.g. in systemd set
|
||||||
`WorkingDirectory=` to the app directory).
|
`WorkingDirectory=` to the app directory).
|
||||||
|
|
||||||
3. For reproducible builds across machines, consider re-enabling the lockfile
|
3. For reproducible builds across machines, consider re-enabling the lockfile in
|
||||||
in `deno.json` (`"lock": true`) and committing `deno.lock`, then using the
|
`deno.json` (`"lock": true`) and committing `deno.lock`, then using the same
|
||||||
same lock on the server.
|
lock on the server.
|
||||||
|
|
||||||
4. Do not use `esm.sh` for Preact (this project already uses `npm:preact`);
|
4. Do not use `esm.sh` for Preact (this project already uses `npm:preact`);
|
||||||
duplicate Preact from mixed sources is a common cause.
|
duplicate Preact from mixed sources is a common cause.
|
||||||
|
|||||||
11
deno.json
11
deno.json
@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"lock": false,
|
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt && deno fmt --check . && deno lint . && deno check",
|
"check": "deno fmt && deno fmt --check . && deno lint . && deno check",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "deno install -r --allow-scripts && vite build",
|
||||||
"start": "deno serve -A --port 8080 _fresh/server.js",
|
"start": "deno serve -A --port 8080 _fresh/server.js",
|
||||||
"update": "deno run -A -r jsr:@fresh/update .",
|
"update": "deno run -A -r jsr:@fresh/update .",
|
||||||
"e2e": "deno run -A npm:playwright test",
|
"e2e": "deno run -A npm:playwright test",
|
||||||
@ -75,12 +74,8 @@
|
|||||||
],
|
],
|
||||||
"types": ["vite/client"]
|
"types": ["vite/client"]
|
||||||
},
|
},
|
||||||
"test": {
|
"test": { "exclude": ["node_modules/", "_fresh/"] },
|
||||||
"exclude": ["node_modules/", "_fresh/"]
|
"fmt": { "exclude": ["node_modules/", "_fresh/"] },
|
||||||
},
|
|
||||||
"fmt": {
|
|
||||||
"exclude": ["node_modules/", "_fresh/"]
|
|
||||||
},
|
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"unstable": ["kv"]
|
"unstable": ["kv"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,8 @@ interface MemberHomeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MemberHome(
|
export default function MemberHome(
|
||||||
{ onboardingStatus, serverCalendarYear, serverCalendarMonth }: MemberHomeProps,
|
{ onboardingStatus, serverCalendarYear, serverCalendarMonth }:
|
||||||
|
MemberHomeProps,
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lockRoute = localStorage.getItem("lock-route");
|
const lockRoute = localStorage.getItem("lock-route");
|
||||||
|
|||||||
71
islands/ThemeHydration.tsx
Normal file
71
islands/ThemeHydration.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
const THEME_KEY = "cyberhybrid-theme-preference";
|
||||||
|
|
||||||
|
function applyTheme(root: Element, mode: string): void {
|
||||||
|
root.setAttribute("data-theme-mode", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTheme(): string {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(THEME_KEY);
|
||||||
|
if (stored === "light" || stored === "dark") return stored;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncToggleLabels(root: Element): void {
|
||||||
|
const isDark = root.getAttribute("data-theme-mode") !== "light";
|
||||||
|
const label = isDark ? "Switch to light mode" : "Switch to dark mode";
|
||||||
|
const toggles = root.querySelectorAll("[data-theme-toggle]");
|
||||||
|
toggles.forEach((el) => {
|
||||||
|
el.setAttribute("aria-label", label);
|
||||||
|
el.setAttribute("title", label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistTheme(mode: string): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(THEME_KEY, mode);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-only island that applies saved theme and wires up theme toggle clicks.
|
||||||
|
* Renders nothing; runs theme logic in useEffect to avoid dangerouslySetInnerHTML.
|
||||||
|
*/
|
||||||
|
export default function ThemeHydration() {
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
applyTheme(root, readTheme());
|
||||||
|
syncToggleLabels(root);
|
||||||
|
|
||||||
|
const handleClick = (event: Event) => {
|
||||||
|
const target = event.target as Node;
|
||||||
|
if (!target || !(target instanceof Element)) return;
|
||||||
|
const toggle = target.closest("[data-theme-toggle]");
|
||||||
|
if (!toggle) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const current = root.getAttribute("data-theme-mode") === "light"
|
||||||
|
? "light"
|
||||||
|
: "dark";
|
||||||
|
const next = current === "dark" ? "light" : "dark";
|
||||||
|
applyTheme(root, next);
|
||||||
|
persistTheme(next);
|
||||||
|
syncToggleLabels(root);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", handleClick, true);
|
||||||
|
return () => document.removeEventListener("click", handleClick, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -13,7 +13,8 @@ import {
|
|||||||
* Native-rendered dropdown content for <details> menu.
|
* Native-rendered dropdown content for <details> menu.
|
||||||
*/
|
*/
|
||||||
export default function NavigationMenu(
|
export default function NavigationMenu(
|
||||||
{ isLoggedIn, currentRoute, navigationGroups, serverCalendarPath }: NavigationMenuProps,
|
{ isLoggedIn, currentRoute, navigationGroups, serverCalendarPath }:
|
||||||
|
NavigationMenuProps,
|
||||||
) {
|
) {
|
||||||
function getItemStateClass(item: NavigationItem): string {
|
function getItemStateClass(item: NavigationItem): string {
|
||||||
if (item.disabled) return "navigation-menu-item--disabled";
|
if (item.disabled) return "navigation-menu-item--disabled";
|
||||||
|
|||||||
@ -16,7 +16,14 @@ function normalizeRoute(path: string): string {
|
|||||||
* Main navigation bar with user info, encryption status, and menu.
|
* Main navigation bar with user info, encryption status, and menu.
|
||||||
*/
|
*/
|
||||||
export default function WelcomeBar(
|
export default function WelcomeBar(
|
||||||
{ publicId, user_name, currentRoute, isEncrypted, stateId, serverCalendarPath }: WelcomeBarProps,
|
{
|
||||||
|
publicId,
|
||||||
|
user_name,
|
||||||
|
currentRoute,
|
||||||
|
isEncrypted,
|
||||||
|
stateId,
|
||||||
|
serverCalendarPath,
|
||||||
|
}: WelcomeBarProps,
|
||||||
) {
|
) {
|
||||||
const [activeRoute, setActiveRoute] = useState(normalizeRoute(currentRoute));
|
const [activeRoute, setActiveRoute] = useState(normalizeRoute(currentRoute));
|
||||||
const [state] = useState<WelcomeBarState>({
|
const [state] = useState<WelcomeBarState>({
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// _app.tsx
|
// _app.tsx
|
||||||
import { define, type PageProps } from "../utils.ts";
|
import { define, type PageProps } from "../utils.ts";
|
||||||
import WelcomeBar from "../islands/WelcomeBar/WelcomeBar.tsx";
|
import WelcomeBar from "../islands/WelcomeBar/WelcomeBar.tsx";
|
||||||
|
import ThemeHydration from "../islands/ThemeHydration.tsx";
|
||||||
import { createPublicId } from "../helpers.ts";
|
import { createPublicId } from "../helpers.ts";
|
||||||
import { SessionCookie } from "./auth/callback.ts";
|
import { SessionCookie } from "./auth/callback.ts";
|
||||||
|
|
||||||
@ -24,10 +25,12 @@ export default define.page(async function App(props: PageProps) {
|
|||||||
: "";
|
: "";
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const serverCalendarPath =
|
const serverCalendarPath =
|
||||||
`/calendar/financial-calendar/${now.getFullYear()}/${(now.getMonth() + 1).toString().padStart(2, "0")}`;
|
`/calendar/financial-calendar/${now.getFullYear()}/${
|
||||||
|
(now.getMonth() + 1).toString().padStart(2, "0")
|
||||||
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html>
|
<html data-theme-mode="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@ -45,63 +48,7 @@ export default define.page(async function App(props: PageProps) {
|
|||||||
<div class="page-offset">
|
<div class="page-offset">
|
||||||
<Component />
|
<Component />
|
||||||
</div>
|
</div>
|
||||||
<script
|
<ThemeHydration />
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `(function () {
|
|
||||||
var KEY = "cyberhybrid-theme-preference";
|
|
||||||
var root = document.documentElement;
|
|
||||||
|
|
||||||
function applyTheme(mode) {
|
|
||||||
root.setAttribute("data-theme-mode", mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readTheme() {
|
|
||||||
try {
|
|
||||||
var stored = localStorage.getItem(KEY);
|
|
||||||
if (stored === "light" || stored === "dark") return stored;
|
|
||||||
} catch (_error) {}
|
|
||||||
return "dark";
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncToggleLabels() {
|
|
||||||
var isDark = root.getAttribute("data-theme-mode") !== "light";
|
|
||||||
var label = isDark ? "Switch to light mode" : "Switch to dark mode";
|
|
||||||
var toggles = document.querySelectorAll("[data-theme-toggle]");
|
|
||||||
for (var i = 0; i < toggles.length; i++) {
|
|
||||||
toggles[i].setAttribute("aria-label", label);
|
|
||||||
toggles[i].setAttribute("title", label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistTheme(mode) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(KEY, mode);
|
|
||||||
} catch (_error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme(readTheme());
|
|
||||||
syncToggleLabels();
|
|
||||||
|
|
||||||
document.addEventListener("click", function (event) {
|
|
||||||
var target = event.target;
|
|
||||||
if (!(target instanceof Element)) return;
|
|
||||||
var toggle = target.closest("[data-theme-toggle]");
|
|
||||||
if (!toggle) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
var current = root.getAttribute("data-theme-mode") === "light"
|
|
||||||
? "light"
|
|
||||||
: "dark";
|
|
||||||
var next = current === "dark" ? "light" : "dark";
|
|
||||||
applyTheme(next);
|
|
||||||
persistTheme(next);
|
|
||||||
syncToggleLabels();
|
|
||||||
}, true);
|
|
||||||
})();`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user