feat: enhance NavItem and NavItemButton components with optional className prop; refactor atom usage in user state management

This commit is contained in:
monoid 2025-10-01 01:53:16 +09:00
parent cb6d03458f
commit 0be89bfa23
5 changed files with 41 additions and 92 deletions

View file

@ -11,18 +11,20 @@ interface NavItemProps {
icon: React.ReactNode; icon: React.ReactNode;
to: string; to: string;
name: string; name: string;
className?: string;
} }
export function NavItem({ export function NavItem({
icon, icon,
to, to,
name name,
className
}: NavItemProps) { }: NavItemProps) {
return <Tooltip> return <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Link <Link
href={to} href={to}
className={buttonVariants({ variant: "ghost" })} className={buttonVariants({ variant: "ghost", className })}
> >
{icon} {icon}
<span className="sr-only">{name}</span> <span className="sr-only">{name}</span>

View file

@ -1,7 +1,7 @@
import { atom, useAtomValue, setAtomValue, getAtomState } from "@/lib/atom"; import { atom, useAtomValue, useSetAtom } from "@/lib/atom";
import { useLayoutEffect } from "react"; import { useLayoutEffect, useRef } from "react";
const NavItems = atom<React.ReactNode>("NavItems", null); const NavItems = atom<React.ReactNode>(null);
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export function useNavItems() { export function useNavItems() {
@ -9,14 +9,19 @@ export function useNavItems() {
} }
export function PageNavItem({items, children}:{items: React.ReactNode, children: React.ReactNode}) { export function PageNavItem({items, children}:{items: React.ReactNode, children: React.ReactNode}) {
const currentNavItems = useAtomValue(NavItems);
const setNavItems = useSetAtom(NavItems);
const prevValueRef = useRef<React.ReactNode>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
const prev = getAtomState(NavItems).value; // Store current value before setting new one
const setter = setAtomValue(NavItems); prevValueRef.current = currentNavItems;
setter(items); setNavItems(items);
return () => { return () => {
setter(prev); setNavItems(prevValueRef.current);
}; };
}, [items]); }, [items, currentNavItems, setNavItems]);
return children; return children;
} }

View file

@ -1,70 +1,2 @@
import { useEffect, useReducer, useState } from "react"; // Re-export jotai functions to maintain compatibility
export { atom, useAtom, useAtomValue, useSetAtom, useSetAtom as setAtomValue } from 'jotai';
interface AtomState<T> {
value: T;
listeners: Set<() => void>;
}
interface Atom<T> {
key: string;
default: T;
}
const atomStateMap = new WeakMap<Atom<unknown>, AtomState<unknown>>();
export function atom<T>(key: string, defaultVal: T): Atom<T> {
return { key, default: defaultVal };
}
export function getAtomState<T>(atom: Atom<T>): AtomState<T> {
let atomState = atomStateMap.get(atom);
if (!atomState) {
atomState = {
value: atom.default,
listeners: new Set(),
};
atomStateMap.set(atom, atomState);
}
return atomState as AtomState<T>;
}
export function useAtom<T>(atom: Atom<T>): [T, (val: T) => void] {
const state = getAtomState(atom);
const [, setState] = useState(state.value);
useEffect(() => {
const listener = () => setState(state.value);
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}, [state]);
return [
state.value as T,
(val: T) => {
state.value = val;
// biome-ignore lint/complexity/noForEach: forEach is used to call each listener
state.listeners.forEach((listener) => listener());
},
];
}
export function useAtomValue<T>(atom: Atom<T>): T {
const state = getAtomState(atom);
const update = useReducer((x) => x + 1, 0)[1];
useEffect(() => {
const listener = () => update();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}, [state, update]);
return state.value;
}
export function setAtomValue<T>(atom: Atom<T>): (val: T) => void {
const state = getAtomState(atom);
return (val: T) => {
state.value = val;
// biome-ignore lint/complexity/noForEach: forEach is used to call each listener
state.listeners.forEach((listener) => listener());
};
}

View file

@ -146,12 +146,20 @@ export default function ComicPage({
return ( return (
<PageNavItem items={<> <PageNavItem items={<>
<NavItem to={`/doc/${params.id}`} name="Back" icon={<ExitIcon />} /> <NavItem
<NavItemButton name={isFullScreen ? "Exit Fullscreen" : "Enter Fullscreen"} icon={isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />} onClick={() => { className="flex-1"
toggleFullScreen(); to={`/doc/${params.id}`} name="Back" icon={<ExitIcon />} />
}} /> <NavItemButton
className="flex-1"
name={isFullScreen ? "Exit Fullscreen" : "Enter Fullscreen"}
icon={isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
onClick={() => {
toggleFullScreen();
}} />
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger
className="flex-1"
>
<span className="text-sm text-ellipsis" >{curPage + 1}/{data.additional.page as number}</span> <span className="text-sm text-ellipsis" >{curPage + 1}/{data.additional.page as number}</span>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-28"> <PopoverContent className="w-28">

View file

@ -1,4 +1,5 @@
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts"; import { atom, useAtomValue } from "../lib/atom.ts";
import { createStore } from 'jotai';
import { LoginRequest } from "dbtype/mod.ts"; import { LoginRequest } from "dbtype/mod.ts";
import { import {
ApiError, ApiError,
@ -9,6 +10,9 @@ import {
resetPasswordService, resetPasswordService,
} from "./api.ts"; } from "./api.ts";
// Create a store for setting atom values outside components
const store = createStore();
let localObj: LoginResponse | null = null; let localObj: LoginResponse | null = null;
function getUserSessions() { function getUserSessions() {
if (localObj === null) { if (localObj === null) {
@ -55,7 +59,6 @@ export async function refresh() {
} }
export const doLogout = async () => { export const doLogout = async () => {
const setVal = setAtomValue(userLoginStateAtom);
try { try {
const res = await logoutService(); const res = await logoutService();
localObj = { localObj = {
@ -64,7 +67,7 @@ export const doLogout = async () => {
permission: res.permission, permission: res.permission,
}; };
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
setVal(localObj); store.set(userLoginStateAtom, localObj);
return { return {
username: localObj.username, username: localObj.username,
permission: localObj.permission, permission: localObj.permission,
@ -74,7 +77,7 @@ export const doLogout = async () => {
// Even if logout fails, clear client-side session // Even if logout fails, clear client-side session
localObj = { accessExpired: 0, username: "", permission: [] }; localObj = { accessExpired: 0, username: "", permission: [] };
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
setVal(localObj); store.set(userLoginStateAtom, localObj);
return { return {
username: "", username: "",
permission: [], permission: [],
@ -85,9 +88,8 @@ export const doLogout = async () => {
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginResponse> => { export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginResponse> => {
try { try {
const b = await loginService(userLoginInfo); const b = await loginService(userLoginInfo);
const setVal = setAtomValue(userLoginStateAtom);
localObj = b; localObj = b;
setVal(b); store.set(userLoginStateAtom, b);
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj)); window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
return b; return b;
} catch (e) { } catch (e) {
@ -123,7 +125,7 @@ export async function getInitialValue() {
return refresh(); return refresh();
} }
export const userLoginStateAtom = atom("userLoginState", getUserSessions()); export const userLoginStateAtom = atom(getUserSessions());
export function useLogin() { export function useLogin() {
const val = useAtomValue(userLoginStateAtom); const val = useAtomValue(userLoginStateAtom);