mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-19 05:30:44 -05:00
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
import * as React from "react";
|
|
import { useLoaderData, useMatches, useNavigate } from "remix";
|
|
import type { SSETarget } from "server/events";
|
|
import { z } from "zod";
|
|
import { LoggedInUserSchema } from "~/utils/schemas";
|
|
|
|
export const useUser = () => {
|
|
const [root] = useMatches();
|
|
|
|
const parsed = LoggedInUserSchema.parse(root.data);
|
|
return parsed?.user;
|
|
};
|
|
|
|
export const useBaseURL = () => {
|
|
const [root] = useMatches();
|
|
|
|
const parsed = z.object({ baseURL: z.string() }).parse(root.data);
|
|
return parsed.baseURL;
|
|
};
|
|
|
|
export const useEvents = (
|
|
target: SSETarget,
|
|
handleEvent: (data: unknown) => void
|
|
) => {
|
|
React.useEffect(() => {
|
|
const source = new EventSource(
|
|
`/events?${new URLSearchParams(target).toString()}`
|
|
);
|
|
|
|
source.addEventListener("open", () => {
|
|
console.log("SSE opened!");
|
|
});
|
|
|
|
source.addEventListener("message", (e) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
handleEvent(JSON.parse(e.data));
|
|
});
|
|
|
|
source.addEventListener("error", (e) => {
|
|
console.error("SSE error: ", e);
|
|
});
|
|
|
|
return () => {
|
|
source.close();
|
|
};
|
|
}, []);
|
|
};
|
|
|
|
// TODO: fix causes memory leak
|
|
/** @link https://stackoverflow.com/a/64983274 */
|
|
export const useTimeoutState = <T>(
|
|
defaultState: T
|
|
): [
|
|
T,
|
|
(action: React.SetStateAction<T>, opts?: { timeout: number }) => void
|
|
] => {
|
|
const [state, _setState] = React.useState<T>(defaultState);
|
|
const [currentTimeoutId, setCurrentTimeoutId] = React.useState<
|
|
NodeJS.Timeout | undefined
|
|
>();
|
|
|
|
const setState = React.useCallback(
|
|
(action: React.SetStateAction<T>, opts?: { timeout: number }) => {
|
|
if (currentTimeoutId != null) {
|
|
clearTimeout(currentTimeoutId);
|
|
}
|
|
|
|
_setState(action);
|
|
|
|
const id = setTimeout(
|
|
() => _setState(defaultState),
|
|
opts?.timeout ?? 4000
|
|
);
|
|
setCurrentTimeoutId(id);
|
|
},
|
|
[currentTimeoutId, defaultState]
|
|
);
|
|
return [state, setState];
|
|
};
|
|
|
|
/** @link https://usehooks.com/useOnClickOutside/ */
|
|
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
|
ref: React.RefObject<T>,
|
|
handler: (event: MouseEvent | TouchEvent) => void
|
|
) {
|
|
React.useEffect(() => {
|
|
const listener = (event: MouseEvent | TouchEvent) => {
|
|
if (!ref.current || ref.current.contains(event.target as Node)) {
|
|
return;
|
|
}
|
|
handler(event);
|
|
};
|
|
document.addEventListener("mousedown", listener);
|
|
document.addEventListener("touchstart", listener);
|
|
return () => {
|
|
document.removeEventListener("mousedown", listener);
|
|
document.removeEventListener("touchstart", listener);
|
|
};
|
|
}, [ref, handler]);
|
|
}
|
|
|
|
/** Refreshed loader data of the current route in an interval.
|
|
* @returns Timestamp last updated
|
|
*/
|
|
export function usePolling(pollingActive = true) {
|
|
const [lastUpdated, setLastUpdated] = React.useState(new Date());
|
|
const data = useLoaderData<unknown>();
|
|
const navigate = useNavigate();
|
|
|
|
const INTERVAL = 30_000; // 30 seconds
|
|
|
|
React.useEffect(() => {
|
|
if (!pollingActive) return;
|
|
const timer = setTimeout(() => {
|
|
navigate(".");
|
|
}, INTERVAL);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [pollingActive, navigate, data]);
|
|
|
|
React.useEffect(() => {
|
|
setLastUpdated(new Date());
|
|
}, [data]);
|
|
|
|
return lastUpdated;
|
|
}
|