Redesign UI with shadcn

This commit is contained in:
2024-12-01 13:46:34 +01:00
parent 8c13f50378
commit 27581d0057
25 changed files with 1758 additions and 376 deletions

View 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;
}