Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
<link rel="icon" type="image/svg+xml" href="/assets/cs17_portal/dashboard/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CS17 Portal</title>
<!-- Apply the saved theme before the app bundle runs, so the page doesn't flash light before switching to dark on load. -->
<script>
try {
if (localStorage.getItem("theme") === "dark")
document.documentElement.classList.add("dark");
} catch (e) {}
</script>
Comment on lines +9 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't app JS do it in JS side?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but main.tsx executes just a bit late, and so screen flash is seen when switching to dark mode each time. In case of an inline script, the code executes at once, preventing that from happening. Will add a comment on index.html to explain this better.

<script type="module" src="/src/main.tsx"></script>
</head>
<body>
Expand Down
22 changes: 2 additions & 20 deletions dashboard/src/faculty/FacultySettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,11 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

function getInitialTheme(): "light" | "dark" {
try {
const stored = localStorage.getItem("theme");
if (stored === "dark" || stored === "light") return stored;
} catch {}
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}

function applyTheme(theme: "light" | "dark") {
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
try {
localStorage.setItem("theme", theme);
} catch {}
}
import { applyTheme, getStoredTheme, type Theme } from "@/lib/theme";

export default function FacultySettingsPage() {
const { profile } = useCurrentProfile();
const [theme, setTheme] = useState<"light" | "dark">(getInitialTheme);
const [theme, setTheme] = useState<Theme>(getStoredTheme);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);

function toggleTheme() {
Expand Down
20 changes: 20 additions & 0 deletions dashboard/src/lib/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type Theme = "light" | "dark";

export function getStoredTheme(): Theme {
try {
const stored = localStorage.getItem("theme");
if (stored === "dark" || stored === "light") return stored;
} catch {}
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}

export function applyTheme(theme: Theme): void {
setThemeClass(theme);
try {
localStorage.setItem("theme", theme);
} catch {}
}

function setThemeClass(theme: Theme): void {
document.documentElement.classList.toggle("dark", theme === "dark");
}
22 changes: 2 additions & 20 deletions dashboard/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,11 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

function getInitialTheme(): "light" | "dark" {
try {
const stored = localStorage.getItem("theme");
if (stored === "dark" || stored === "light") return stored;
} catch {}
return document.documentElement.classList.contains("dark") ? "dark" : "light";
}

function applyTheme(theme: "light" | "dark") {
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
try {
localStorage.setItem("theme", theme);
} catch {}
}
import { applyTheme, getStoredTheme, type Theme } from "@/lib/theme";

export default function SettingsPage() {
const { student } = useCurrentStudent();
const [theme, setTheme] = useState<"light" | "dark">(getInitialTheme);
const [theme, setTheme] = useState<Theme>(getStoredTheme);
const [showLogoutDialog, setShowLogoutDialog] = useState(false);

function toggleTheme() {
Expand Down
38 changes: 38 additions & 0 deletions e2e/tests/student-theme.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";

test.describe("Theme persistence", () => {
test("keeps dark mode across a refresh and on other pages", async ({ page }) => {
await page.goto("/dashboard/settings");
await page.getByRole("switch").click();
await expect(page.locator("html")).toHaveClass(/dark/);

await page.reload();
await expect(page.locator("html")).toHaveClass(/dark/);
await expect(page.getByRole("switch")).toHaveAttribute("aria-checked", "true");

await page.goto("/dashboard/projects");
await expect(page.locator("html")).toHaveClass(/dark/);

await page.goto("/dashboard/settings");
await page.getByRole("switch").click();
await page.reload();
await expect(page.locator("html")).not.toHaveClass(/dark/);
});

test("applies dark before the app bundle runs, so there is no flash", async ({
page,
}) => {
await page.goto("/dashboard/settings");
await page.getByRole("switch").click();
await expect(page.locator("html")).toHaveClass(/dark/);

await page.route("**/dashboard/assets/*.js", async (route) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
await route.continue();
});

await page.goto("/dashboard/projects", { waitUntil: "commit" });
await expect(page.locator("html")).toHaveClass(/dark/, { timeout: 1000 });
await expect(page.locator("#root")).toBeEmpty();
});
});
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "path";
const authFile = path.join(__dirname, "e2e", ".auth", "user.json");
const studentAuthFile = path.join(__dirname, "e2e", ".auth", "student.json");
const facultyAuthFile = path.join(__dirname, "e2e", ".auth", "faculty.json");
const studentSpecs = /student-submission\.spec\.ts/;
const studentSpecs = /(student-submission|student-theme)\.spec\.ts/;
const facultySpecs = /faculty-assignments-ui\.spec\.ts/;

const SITE_HOST = process.env.SITE_HOST || "cs17.portal:8000";
Expand Down
Loading