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;
to: string;
name: string;
className?: string;
}
export function NavItem({
icon,
to,
name
name,
className
}: NavItemProps) {
return <Tooltip>
<TooltipTrigger asChild>
<Link
href={to}
className={buttonVariants({ variant: "ghost" })}
className={buttonVariants({ variant: "ghost", className })}
>
{icon}
<span className="sr-only">{name}</span>

View file

@ -1,7 +1,7 @@
import { atom, useAtomValue, setAtomValue, getAtomState } from "@/lib/atom";
import { useLayoutEffect } from "react";
import { atom, useAtomValue, useSetAtom } from "@/lib/atom";
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
export function useNavItems() {
@ -9,14 +9,19 @@ export function useNavItems() {
}
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(() => {
const prev = getAtomState(NavItems).value;
const setter = setAtomValue(NavItems);
setter(items);
// Store current value before setting new one
prevValueRef.current = currentNavItems;
setNavItems(items);
return () => {
setter(prev);
setNavItems(prevValueRef.current);
};
}, [items]);
}, [items, currentNavItems, setNavItems]);
return children;
}

View file

@ -1,70 +1,2 @@
import { useEffect, useReducer, useState } from "react";
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());
};
}
// Re-export jotai functions to maintain compatibility
export { atom, useAtom, useAtomValue, useSetAtom, useSetAtom as setAtomValue } from 'jotai';

View file

@ -146,12 +146,20 @@ export default function ComicPage({
return (
<PageNavItem items={<>
<NavItem to={`/doc/${params.id}`} name="Back" icon={<ExitIcon />} />
<NavItemButton name={isFullScreen ? "Exit Fullscreen" : "Enter Fullscreen"} icon={isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />} onClick={() => {
toggleFullScreen();
}} />
<NavItem
className="flex-1"
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>
<PopoverTrigger>
<PopoverTrigger
className="flex-1"
>
<span className="text-sm text-ellipsis" >{curPage + 1}/{data.additional.page as number}</span>
</PopoverTrigger>
<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 {
ApiError,
@ -9,6 +10,9 @@ import {
resetPasswordService,
} from "./api.ts";
// Create a store for setting atom values outside components
const store = createStore();
let localObj: LoginResponse | null = null;
function getUserSessions() {
if (localObj === null) {
@ -55,7 +59,6 @@ export async function refresh() {
}
export const doLogout = async () => {
const setVal = setAtomValue(userLoginStateAtom);
try {
const res = await logoutService();
localObj = {
@ -64,7 +67,7 @@ export const doLogout = async () => {
permission: res.permission,
};
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
setVal(localObj);
store.set(userLoginStateAtom, localObj);
return {
username: localObj.username,
permission: localObj.permission,
@ -74,7 +77,7 @@ export const doLogout = async () => {
// Even if logout fails, clear client-side session
localObj = { accessExpired: 0, username: "", permission: [] };
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
setVal(localObj);
store.set(userLoginStateAtom, localObj);
return {
username: "",
permission: [],
@ -85,9 +88,8 @@ export const doLogout = async () => {
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginResponse> => {
try {
const b = await loginService(userLoginInfo);
const setVal = setAtomValue(userLoginStateAtom);
localObj = b;
setVal(b);
store.set(userLoginStateAtom, b);
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
return b;
} catch (e) {
@ -123,7 +125,7 @@ export async function getInitialValue() {
return refresh();
}
export const userLoginStateAtom = atom("userLoginState", getUserSessions());
export const userLoginStateAtom = atom(getUserSessions());
export function useLogin() {
const val = useAtomValue(userLoginStateAtom);