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
44 changes: 44 additions & 0 deletions app/college-predictor/ejam-theme-bridge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useLayoutEffect } from "react";

const STORAGE_KEY = "deetnuts_theme";
const DARK_MEDIA_QUERY = "(prefers-color-scheme: dark)";

function readPreference(): "system" | "light" | "dark" {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored === "light" || stored === "dark" ? stored : "system";
} catch {
return "system";
}
}

export function EjamThemeBridge() {
useLayoutEffect(() => {
const mediaQuery = window.matchMedia(DARK_MEDIA_QUERY);
const syncTheme = () => {
const preference = readPreference();
const useDarkTheme =
preference === "dark" ||
(preference === "system" && mediaQuery.matches);

document
.querySelector<HTMLElement>(".deetnuts-ejam-shell")
?.classList.toggle("dark", useDarkTheme);
};

syncTheme();
mediaQuery.addEventListener("change", syncTheme);
window.addEventListener("storage", syncTheme);
window.addEventListener("deetnuts:theme-change", syncTheme);

return () => {
mediaQuery.removeEventListener("change", syncTheme);
window.removeEventListener("storage", syncTheme);
window.removeEventListener("deetnuts:theme-change", syncTheme);
};
}, []);

return null;
}
5 changes: 4 additions & 1 deletion app/college-predictor/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Toaster } from "sonner";
import { COLLEGE_PREDICTOR_LCP_PRELOAD } from "@ejam/ui/lib/static-image";
import { cn } from "@ejam/ui/lib/utils";
import { EjamThemeBridge } from "@/app/college-predictor/ejam-theme-bridge";
import "./route.css";

export default function CollegePredictorLayout({
Expand All @@ -21,10 +22,12 @@ export default function CollegePredictorLayout({
/>
<div
className={cn(
"deetnuts-ejam-shell dark min-h-screen bg-background text-foreground antialiased",
"deetnuts-ejam-shell min-h-screen bg-background text-foreground antialiased",
"font-sans",
)}
suppressHydrationWarning
>
<EjamThemeBridge />
{children}
<Toaster
position="bottom-right"
Expand Down
5 changes: 3 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import "./globals.css";

import SiteJsonLd from "@/components/SiteJsonLd";
import StandardSiteShell from "@/components/StandardSiteShell";
import { InlineScript } from "@/components/inline-script";
import { PRODUCTION_SITE_URL } from "@/lib/site-url";

const themeBootScript = `try{var t=localStorage.getItem("deetnuts_theme");var r=document.documentElement;if(t==="light"||t==="dark"){r.classList.add(t);r.classList.remove(t==="light"?"dark":"light")}}catch(e){}`;
const themeBootScript = `try{var t=null;try{t=localStorage.getItem("deetnuts_theme")}catch(e){}var r=document.documentElement;var d=t==="dark"||(t!=="light"&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches);if(t==="light"||t==="dark"){r.classList.add(t);r.classList.remove(t==="light"?"dark":"light")}var sync=function(){var s=document.querySelector(".deetnuts-ejam-shell");if(!s)return false;s.classList.toggle("dark",Boolean(d));return true};if(!sync()&&window.MutationObserver){var o=new MutationObserver(function(){if(sync())o.disconnect()});o.observe(document.documentElement,{childList:true,subtree:true})}}catch(e){}`;

export const metadata: Metadata = {
applicationName: "DEETNUTS",
Expand Down Expand Up @@ -74,7 +75,7 @@ export default function RootLayout({
return (
<html lang="en" className="bg-[#E4DFF2]" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeBootScript }} />
<InlineScript html={themeBootScript} />
<SiteJsonLd />
</head>
<body className="relative min-h-screen overflow-x-hidden font-sans">
Expand Down
126 changes: 64 additions & 62 deletions components/account-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
"use client";

import Link from "next/link";
import {
forwardRef,
type ComponentPropsWithoutRef,
useSyncExternalStore,
} from "react";
import { ChevronDown, LogOut, SunMoon, UserRound } from "lucide-react";
import { forwardRef, type ComponentPropsWithoutRef } from "react";
import { signOut } from "@/app/login/actions";
import { openThemeSettings } from "@/components/theme/ThemeSettings";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer-2";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";

const subscribeToHydration = () => () => {};
function getFirstName(displayName: string) {
return displayName.trim().split(/\s+/)[0] || "Account";
}

interface AccountMenuTriggerProps
extends ComponentPropsWithoutRef<"button"> {
interface AccountMenuTriggerProps extends ComponentPropsWithoutRef<"button"> {
displayName: string;
avatarUrl: string;
}
Expand All @@ -36,7 +32,8 @@ const AccountMenuTrigger = forwardRef<
{ displayName, avatarUrl, className, ...props },
ref,
) {
const initial = displayName.trim().charAt(0).toUpperCase() || "A";
const firstName = getFirstName(displayName);
const initial = firstName.charAt(0).toUpperCase() || "A";

return (
<button
Expand All @@ -53,7 +50,10 @@ const AccountMenuTrigger = forwardRef<
<AvatarImage src={avatarUrl} alt="" />
<AvatarFallback>{initial}</AvatarFallback>
</Avatar>
<span className="hidden sm:inline">Hey, {displayName}!</span>
<span className="hidden max-w-[140px] truncate sm:inline">
{firstName}
</span>
<ChevronDown className="hidden h-4 w-4 sm:inline" aria-hidden="true" />
</button>
);
});
Expand All @@ -65,50 +65,52 @@ export function AccountMenu({
name: string;
avatarUrl: string;
}) {
const hydrated = useSyncExternalStore(
subscribeToHydration,
() => true,
() => false,
);

if (!hydrated) {
return <AccountMenuTrigger displayName={name} avatarUrl={avatarUrl} />;
}
const firstName = getFirstName(name);

return (
<Drawer>
<DrawerTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<AccountMenuTrigger displayName={name} avatarUrl={avatarUrl} />
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-[320px]">
<DrawerHeader>
<DrawerTitle>Account</DrawerTitle>
<DrawerDescription>Signed in as {name}</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose asChild>
<Button asChild>
<Link href="/account">View account</Link>
</Button>
</DrawerClose>
<form action={signOut}>
<Button
type="submit"
variant="noShadow"
className="w-full rounded-md"
>
Log out
</Button>
</form>
<DrawerClose asChild>
<Button type="button" variant="neutral">
Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8} className="w-56">
<DropdownMenuLabel className="px-2 py-2">
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8 outline-1">
<AvatarImage src={avatarUrl} alt="" />
<AvatarFallback>
{firstName.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="truncate text-sm font-heading">{firstName}</p>
<p className="text-xs font-base text-black/60">Account</p>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/account" className="flex w-full items-center gap-2">
<UserRound className="h-4 w-4" aria-hidden="true" />
<span>View account</span>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onSelect={openThemeSettings}>
<SunMoon className="mr-2 h-4 w-4" aria-hidden="true" />
<span>Theme settings</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<form action={signOut}>
<DropdownMenuItem asChild>
<button
type="submit"
className="flex w-full items-center gap-2 text-left"
>
<LogOut className="h-4 w-4" aria-hidden="true" />
<span>Log out</span>
</button>
</DropdownMenuItem>
</form>
</DropdownMenuContent>
</DropdownMenu>
);
}
15 changes: 11 additions & 4 deletions components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ export default function Footer() {
<div className="lg:col-span-6">
<div className="text-black text-4xl font-heading">DeetNuts</div>

<p className="mt-4 max-w-xl text-gray-500 leading-snug" data-nosnippet>
DEETNUTS is an independent educational-data project. College and
<p
className="mt-4 max-w-lg text-gray-500 leading-snug"
data-nosnippet
>
DEETNUTS is an independent educational-data project and is not
affiliated with or endorsed by any examination authority,
counselling body, college, or listed institution. College and
counselling data is compiled from published sources and may
contain processing errors. Always verify admission decisions on
the applicable official portal.
contain transcription or processing errors. Figures, listings,
eligibility details, seat availability, and deadlines may
change. Always verify admission decisions on the applicable
official portal.
</p>
<a
href="https://discord.gg/xbtqGcQ6SF"
Expand Down
11 changes: 11 additions & 0 deletions components/inline-script.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

export function InlineScript({ html }: { html: string }) {
return (
<script
type={typeof window === "undefined" ? "text/javascript" : "text/plain"}
suppressHydrationWarning
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
13 changes: 10 additions & 3 deletions components/theme/ThemeSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { useEffect, useRef, useState } from "react";

const STORAGE_KEY = "deetnuts_theme";
const OPEN_EVENT = "deetnuts:open-theme-settings";
const CHANGE_EVENT = "deetnuts:theme-change";
type ThemePreference = "system" | "light" | "dark";

function readPreference(): ThemePreference {
Expand All @@ -21,7 +23,11 @@ function applyPreference(preference: ThemePreference) {
}

export function ThemeSettingsButton({ className = "" }: { className?: string }) {
return <button type="button" className={className} onClick={() => window.dispatchEvent(new Event("deetnuts:open-theme-settings"))}>Theme settings</button>;
return <button type="button" className={className} onClick={openThemeSettings}>Theme settings</button>;
}

export function openThemeSettings() {
window.dispatchEvent(new Event(OPEN_EVENT));
}

export default function ThemeSettings() {
Expand All @@ -40,10 +46,10 @@ export default function ThemeSettings() {
setPreference(next);
applyPreference(next);
};
window.addEventListener("deetnuts:open-theme-settings", show);
window.addEventListener(OPEN_EVENT, show);
window.addEventListener("storage", sync);
return () => {
window.removeEventListener("deetnuts:open-theme-settings", show);
window.removeEventListener(OPEN_EVENT, show);
window.removeEventListener("storage", sync);
};
}, []);
Expand All @@ -67,6 +73,7 @@ export default function ThemeSettings() {
}
applyPreference(next);
setPreference(next);
window.dispatchEvent(new Event(CHANGE_EVENT));
};

if (!open) return null;
Expand Down