Redesign UI with shadcn
This commit is contained in:
94
packages/frontend/src/hooks/useStore.ts
Normal file
94
packages/frontend/src/hooks/useStore.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { UserId } from "common";
|
||||
import * as Function from "common/Function";
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
export type Update<T> = T | ((prev: T) => T);
|
||||
export type Updater<T> = (action: Update<T>) => void;
|
||||
|
||||
export const mapProp = <const K extends string, T>(prop: K, action: Update<T>) => <O extends { readonly [_ in K]: T }>(object: O): O => {
|
||||
return Object.freeze({ ...object, [prop]: typeof action === "function" ? (action as (prev: T) => T)(object[prop]) : action });
|
||||
};
|
||||
|
||||
export namespace Store {
|
||||
export interface User {
|
||||
readonly userId: UserId;
|
||||
readonly username: string;
|
||||
readonly admin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Store {
|
||||
readonly loginUsername: string;
|
||||
readonly loginPassword: string;
|
||||
|
||||
readonly user: Store.User | null;
|
||||
|
||||
readonly setLoginUsername: Updater<string>;
|
||||
readonly setLoginPassword: Updater<string>;
|
||||
|
||||
readonly setUser: Updater<Store.User | null>;
|
||||
}
|
||||
|
||||
let store: Store = Object.freeze<Store>({
|
||||
loginUsername: "",
|
||||
loginPassword: "",
|
||||
|
||||
user: null,
|
||||
|
||||
setLoginUsername: (action) => set(mapProp("loginUsername", action)),
|
||||
setLoginPassword: (action) => set(mapProp("loginPassword", action)),
|
||||
|
||||
setUser: (action) => set(mapProp("user", action)),
|
||||
});
|
||||
|
||||
// --- STORE IMPLEMENTATION ----------------------------------------------------
|
||||
|
||||
class Listener<T> {
|
||||
|
||||
private lastState: T;
|
||||
|
||||
constructor(
|
||||
readonly selector: Selector<T>,
|
||||
readonly setState: (state: T) => void,
|
||||
) {
|
||||
this.lastState = selector(store);
|
||||
}
|
||||
|
||||
react() {
|
||||
const state = this.selector(store);
|
||||
if (!Object.is(this.lastState, state)) {
|
||||
this.lastState = state;
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners = new Set<Listener<any>>();
|
||||
|
||||
export type Selector<T> = (store: Store) => T;
|
||||
|
||||
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), replace?: false): void;
|
||||
function set(action: Update<Store>, replace: true): void;
|
||||
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), replace: boolean = false): void {
|
||||
const nextPartial = typeof action === "function" ? action(store) : action;
|
||||
if (Object.is(store, nextPartial)) return;
|
||||
store = Object.freeze(replace ? nextPartial as Store : Object.assign({}, store, nextPartial));
|
||||
for (const listener of listeners) {
|
||||
listener.react();
|
||||
}
|
||||
}
|
||||
|
||||
export function useStore<T = Store>(selector: Selector<T> = Function.identity as Selector<T>): T {
|
||||
|
||||
const [state, setState] = useState(() => selector(store));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const listener = new Listener(selector, setState);
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
Reference in New Issue
Block a user