feat: enhance NavItem and NavItemButton components with optional className prop; refactor atom usage in user state management
This commit is contained in:
parent
cb6d03458f
commit
0be89bfa23
5 changed files with 41 additions and 92 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue