refactor: switch to using Bun (#718)
This commit is contained in:
@@ -15,7 +15,7 @@ export function useBrowserFeatureDetection(): BrowserSupport {
|
||||
[
|
||||
"Secure Context",
|
||||
globalThis.location.protocol === "https:" ||
|
||||
globalThis.location.hostname === "localhost",
|
||||
globalThis.location.hostname === "localhost",
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ interface UseCopyToClipboardProps {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export function useCopyToClipboard(
|
||||
{ timeout = 2000 }: UseCopyToClipboardProps = {},
|
||||
) {
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000,
|
||||
}: UseCopyToClipboardProps = {}) {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useFavoriteNode } from "./useFavoriteNode.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
const mockNode = {
|
||||
num: 1234,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface FavoriteNodeOptions {
|
||||
@@ -16,7 +16,9 @@ export function useFavoriteNode() {
|
||||
const updateFavoriteCB = useCallback(
|
||||
({ nodeNum, isFavorite }: FavoriteNodeOptions) => {
|
||||
const node = getNode(nodeNum);
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateFavorite(nodeNum, isFavorite);
|
||||
|
||||
@@ -32,7 +34,7 @@ export function useFavoriteNode() {
|
||||
}),
|
||||
});
|
||||
},
|
||||
[updateFavorite, getNode],
|
||||
[updateFavorite, getNode, t, toast],
|
||||
);
|
||||
|
||||
return { updateFavorite: updateFavoriteCB };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Protobuf } from "@meshtastic/core";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useIgnoreNode } from "./useIgnoreNode.ts";
|
||||
import { Protobuf } from "@meshtastic/core";
|
||||
|
||||
const mockNode = {
|
||||
num: 1234,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from "react";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface IgnoreNodeOptions {
|
||||
@@ -17,7 +17,9 @@ export function useIgnoreNode() {
|
||||
const updateIgnoredCB = useCallback(
|
||||
({ nodeNum, isIgnored }: IgnoreNodeOptions) => {
|
||||
const node = getNode(nodeNum);
|
||||
if (!node) return;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateIgnored(nodeNum, isIgnored);
|
||||
|
||||
@@ -33,7 +35,7 @@ export function useIgnoreNode() {
|
||||
}),
|
||||
});
|
||||
},
|
||||
[updateIgnored, getNode],
|
||||
[updateIgnored, getNode, t, toast],
|
||||
);
|
||||
|
||||
return { updateIgnored: updateIgnoredCB };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import useLocalStorage from "@core/hooks/useLocalStorage.ts";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UseBackupReminderOptions {
|
||||
@@ -23,9 +23,13 @@ const REMINDER_DAYS_FOREVER = 3650;
|
||||
const STORAGE_KEY = "key_backup_reminder";
|
||||
|
||||
function isReminderExpired(expires?: string): boolean {
|
||||
if (!expires) return true;
|
||||
if (!expires) {
|
||||
return true;
|
||||
}
|
||||
const expiryDate = new Date(expires);
|
||||
if (isNaN(expiryDate.getTime())) return true; // Invalid date passed
|
||||
if (Number.isNaN(expiryDate.getTime())) {
|
||||
return true; // Invalid date passed
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
return now.getTime() >= expiryDate.getTime();
|
||||
@@ -41,23 +45,26 @@ export function useBackupReminder({
|
||||
|
||||
const { toast } = useToast();
|
||||
const toastShownRef = useRef(false);
|
||||
const [reminderState, setReminderState] = useLocalStorage<
|
||||
ReminderState | null
|
||||
>(
|
||||
STORAGE_KEY,
|
||||
null,
|
||||
const [reminderState, setReminderState] =
|
||||
useLocalStorage<ReminderState | null>(STORAGE_KEY, null);
|
||||
|
||||
const setReminderExpiry = useCallback(
|
||||
(days: number) => {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + days);
|
||||
setReminderState({ expires: expiryDate.toISOString() });
|
||||
},
|
||||
[setReminderState],
|
||||
);
|
||||
|
||||
const setReminderExpiry = useCallback((days: number) => {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + days);
|
||||
setReminderState({ expires: expiryDate.toISOString() });
|
||||
}, [setReminderState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || toastShownRef.current) return;
|
||||
if (!enabled || toastShownRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReminderExpired(reminderState?.expires)) return;
|
||||
if (!isReminderExpired(reminderState?.expires)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastShownRef.current = true;
|
||||
|
||||
@@ -116,5 +123,10 @@ export function useBackupReminder({
|
||||
enabled,
|
||||
message,
|
||||
onAccept,
|
||||
reminderInDays,
|
||||
reminderState?.expires,
|
||||
setReminderExpiry,
|
||||
t,
|
||||
toast,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FALLBACK_LANGUAGE_CODE,
|
||||
Lang,
|
||||
LangCode,
|
||||
type Lang,
|
||||
type LangCode,
|
||||
supportedLanguages,
|
||||
} from "@app/i18n-config.ts";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLocalStorage from "./useLocalStorage.ts";
|
||||
|
||||
const STORAGE_KEY = "language";
|
||||
@@ -47,7 +47,7 @@ function useLang() {
|
||||
console.warn("Failed to change language:", e);
|
||||
}
|
||||
},
|
||||
[i18n, setLanguageInStorage],
|
||||
[i18n.language, i18n.changeLanguage, setLanguageInStorage],
|
||||
);
|
||||
|
||||
const compare = useCallback(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import useLocalStorage from "./useLocalStorage.ts";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import useLocalStorage from "./useLocalStorage.ts";
|
||||
|
||||
describe("useLocalStorage", () => {
|
||||
const key = "test-key";
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
// taken from https://react-hooked.vercel.app/docs/useLocalStorage/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface WindowEventMap {
|
||||
"local-storage": CustomEvent;
|
||||
"local-storage": CustomEvent<{ key: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +15,6 @@ type UseLocalStorageOptions<T> = {
|
||||
|
||||
const IS_SERVER = typeof window === "undefined";
|
||||
|
||||
/**
|
||||
* Hook for persisting state to localStorage.
|
||||
*
|
||||
* @param {string} key - The key to use for localStorage.
|
||||
* @param {T | (() => T)} initialValue - The initial value to use, if not found in localStorage.
|
||||
* @param {UseLocalStorageOptions<T>} options - Options for the hook.
|
||||
* @returns A tuple of [storedValue, setValue, removeValue].
|
||||
*/
|
||||
export default function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T | (() => T),
|
||||
@@ -34,149 +22,114 @@ export default function useLocalStorage<T>(
|
||||
): [T, Dispatch<SetStateAction<T>>, () => void] {
|
||||
const { initializeWithValue = true } = options;
|
||||
|
||||
const serializer = useCallback<(value: T) => string>(
|
||||
(value) => {
|
||||
if (options.serializer) {
|
||||
return options.serializer(value);
|
||||
}
|
||||
const serializerRef = useRef(options.serializer ?? JSON.stringify);
|
||||
const deserializerRef = useRef(options.deserializer ?? JSON.parse);
|
||||
|
||||
return JSON.stringify(value);
|
||||
},
|
||||
[options],
|
||||
const initialValueRef = useRef<T>(
|
||||
typeof initialValue === "function"
|
||||
? (initialValue as () => T)()
|
||||
: initialValue,
|
||||
);
|
||||
|
||||
const deserializer = useCallback<(value: string) => T>(
|
||||
(value) => {
|
||||
if (options.deserializer) {
|
||||
return options.deserializer(value);
|
||||
}
|
||||
// Support 'undefined' as a value
|
||||
if (value === "undefined") {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
const getInitialValue = useCallback(() => initialValueRef.current, []);
|
||||
|
||||
const defaultValue = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch (error) {
|
||||
console.error("Error parsing JSON:", error);
|
||||
return defaultValue; // Return initialValue if parsing fails
|
||||
}
|
||||
|
||||
return parsed as T;
|
||||
},
|
||||
[options, initialValue],
|
||||
);
|
||||
|
||||
// Get from local storage then
|
||||
// parse stored json or return initialValue
|
||||
const readValue = useCallback((): T => {
|
||||
const initialValueToUse = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
|
||||
// Prevent build error "window is undefined" but keep working
|
||||
if (IS_SERVER) {
|
||||
return initialValueToUse;
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
if (!initializeWithValue || IS_SERVER) {
|
||||
return getInitialValue();
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = globalThis.localStorage.getItem(key);
|
||||
return raw ? deserializer(raw) : initialValueToUse;
|
||||
} catch (error) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, error);
|
||||
return initialValueToUse;
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? deserializerRef.current(item) : getInitialValue();
|
||||
} catch (err) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, err);
|
||||
return getInitialValue();
|
||||
}
|
||||
}, [initialValue, key, deserializer]);
|
||||
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
if (initializeWithValue) {
|
||||
return readValue();
|
||||
}
|
||||
|
||||
return initialValue instanceof Function ? initialValue() : initialValue;
|
||||
});
|
||||
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue: Dispatch<SetStateAction<T>> = useCallback(
|
||||
(value) => {
|
||||
// Prevent build error "window is undefined" but keeps working
|
||||
if (IS_SERVER) {
|
||||
console.warn(
|
||||
`Tried setting localStorage key “${key}” even though environment is not a client`,
|
||||
`Tried setting localStorage key “${key}” in a non-client environment.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Allow value to be a function so we have the same API as useState
|
||||
const newValue = value instanceof Function ? value(readValue()) : value;
|
||||
|
||||
// Save to local storage
|
||||
globalThis.localStorage.setItem(key, serializer(newValue));
|
||||
|
||||
// Save state
|
||||
setStoredValue(newValue);
|
||||
|
||||
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
||||
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
} catch (error) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, error);
|
||||
setStoredValue((prev) => {
|
||||
const newValue =
|
||||
typeof value === "function"
|
||||
? (value as (prev: T) => T)(prev)
|
||||
: value;
|
||||
window.localStorage.setItem(key, serializerRef.current(newValue));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("local-storage", { detail: { key } }),
|
||||
);
|
||||
return newValue;
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, err);
|
||||
}
|
||||
},
|
||||
[key, serializer, readValue],
|
||||
[key],
|
||||
);
|
||||
|
||||
const removeValue = useCallback(() => {
|
||||
// Prevent build error "window is undefined" but keeps working
|
||||
if (IS_SERVER) {
|
||||
console.warn(
|
||||
`Tried removing localStorage key “${key}” even though environment is not a client`,
|
||||
`Tried removing localStorage key “${key}” in a non-client environment.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(getInitialValue());
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("local-storage", { detail: { key } }),
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Error removing localStorage key “${key}”:`, err);
|
||||
}
|
||||
}, [key, getInitialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_SERVER) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultValue = initialValue instanceof Function
|
||||
? initialValue()
|
||||
: initialValue;
|
||||
const handleStorageChange = (
|
||||
event: StorageEvent | CustomEvent<{ key: string }>,
|
||||
) => {
|
||||
const eventKey = "key" in event ? event.key : event.detail?.key;
|
||||
|
||||
// Remove the key from local storage
|
||||
globalThis.localStorage.removeItem(key);
|
||||
|
||||
// Save state with default value
|
||||
setStoredValue(defaultValue);
|
||||
|
||||
// We dispatch a custom event so every similar useLocalStorage hook is notified
|
||||
globalThis.dispatchEvent(new StorageEvent("local-storage", { key }));
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
setStoredValue(readValue());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [key]);
|
||||
|
||||
const handleStorageChange = useCallback(
|
||||
(event: StorageEvent | CustomEvent) => {
|
||||
if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
|
||||
return;
|
||||
if (eventKey === key) {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
setStoredValue(
|
||||
item ? deserializerRef.current(item) : getInitialValue(),
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Error syncing localStorage key “${key}”:`, err);
|
||||
setStoredValue(getInitialValue());
|
||||
}
|
||||
}
|
||||
setStoredValue(readValue());
|
||||
},
|
||||
[key, readValue],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
addEventListener("storage", handleStorageChange);
|
||||
// this is a custom event, triggered in writeValueToLocalStorage
|
||||
addEventListener("local-storage", handleStorageChange);
|
||||
return () => {
|
||||
removeEventListener("storage", handleStorageChange);
|
||||
removeEventListener("local-storage", handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
window.addEventListener(
|
||||
"local-storage",
|
||||
handleStorageChange as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
window.removeEventListener(
|
||||
"local-storage",
|
||||
handleStorageChange as EventListener,
|
||||
);
|
||||
};
|
||||
}, [key, getInitialValue]);
|
||||
|
||||
return [storedValue, setValue, removeValue];
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
|
||||
it("should initialize with visibility set to true if initialVisible is true", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true })
|
||||
usePasswordVisibilityToggle({ initialVisible: true }),
|
||||
);
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
|
||||
it("should toggle visibility from true to false when toggleVisibility is called", () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordVisibilityToggle({ initialVisible: true })
|
||||
usePasswordVisibilityToggle({ initialVisible: true }),
|
||||
);
|
||||
expect(result.current.isVisible).toBe(true);
|
||||
act(() => {
|
||||
@@ -55,7 +55,7 @@ describe("usePasswordVisibilityToggle Hook", () => {
|
||||
|
||||
it("should return a stable toggleVisibility function reference (due to useCallback)", () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
usePasswordVisibilityToggle()
|
||||
usePasswordVisibilityToggle(),
|
||||
);
|
||||
const initialToggleFunc = result.current.toggleVisibility;
|
||||
rerender();
|
||||
|
||||
@@ -9,9 +9,9 @@ interface UsePasswordVisibilityToggleProps {
|
||||
* @param {boolean} [options.initialVisible=false]
|
||||
* @returns {{isVisible: boolean, toggleVisibility: () => void}}
|
||||
*/
|
||||
export function usePasswordVisibilityToggle(
|
||||
{ initialVisible = false }: UsePasswordVisibilityToggleProps = {},
|
||||
) {
|
||||
export function usePasswordVisibilityToggle({
|
||||
initialVisible = false,
|
||||
}: UsePasswordVisibilityToggleProps = {}) {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(initialVisible);
|
||||
|
||||
const toggleVisibility = useCallback(() => {
|
||||
|
||||
@@ -7,11 +7,16 @@ export function usePinnedItems({ storageName }: { storageName: string }) {
|
||||
[],
|
||||
);
|
||||
|
||||
const togglePinnedItem = useCallback((label: string) => {
|
||||
setPinnedItems((prev) =>
|
||||
prev.includes(label) ? prev.filter((g) => g !== label) : [...prev, label]
|
||||
);
|
||||
}, []);
|
||||
const togglePinnedItem = useCallback(
|
||||
(label: string) => {
|
||||
setPinnedItems((prev) =>
|
||||
prev.includes(label)
|
||||
? prev.filter((g) => g !== label)
|
||||
: [...prev, label],
|
||||
);
|
||||
},
|
||||
[setPinnedItems],
|
||||
);
|
||||
|
||||
return {
|
||||
pinnedItems,
|
||||
|
||||
@@ -54,12 +54,14 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
const FLAGS_BITMASKS = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(FLAGS_CONFIG).map(([key, conf]) => [key, conf.value]),
|
||||
) as { [K in FlagName]: typeof FLAGS_CONFIG[K]["value"] };
|
||||
) as { [K in FlagName]: (typeof FLAGS_CONFIG)[K]["value"] };
|
||||
}, []);
|
||||
|
||||
const utils = useMemo(() => {
|
||||
const decode = (value: number): FlagName[] => {
|
||||
if (value === FLAGS_CONFIG.UNSET.value) return ["UNSET"];
|
||||
if (value === FLAGS_CONFIG.UNSET.value) {
|
||||
return ["UNSET"];
|
||||
}
|
||||
|
||||
const activeFlags: FlagName[] = [];
|
||||
for (const key in FLAGS_CONFIG) {
|
||||
@@ -80,14 +82,17 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
return FLAGS_CONFIG.UNSET.value;
|
||||
}
|
||||
return flagNames.reduce((acc, name) => {
|
||||
if (name === "UNSET") return acc;
|
||||
if (name === "UNSET") {
|
||||
return acc;
|
||||
}
|
||||
return acc | FLAGS_CONFIG[name].value;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const hasFlag = (value: number, flagName: FlagName): boolean => {
|
||||
return (value & FLAGS_CONFIG[flagName].value) ===
|
||||
FLAGS_CONFIG[flagName].value;
|
||||
return (
|
||||
(value & FLAGS_CONFIG[flagName].value) === FLAGS_CONFIG[flagName].value
|
||||
);
|
||||
};
|
||||
|
||||
const getAllFlags = (): typeof FLAGS_CONFIG => {
|
||||
@@ -117,7 +122,7 @@ export const usePositionFlags = (initialValue = 0): UsePositionFlagsProps => {
|
||||
const setFlag = useCallback((flagName: FlagName, enabled: boolean) => {
|
||||
const currentFlagValue = FLAGS_CONFIG[flagName].value;
|
||||
setFlagsValue((prev) =>
|
||||
enabled ? prev | currentFlagValue : prev & ~currentFlagValue
|
||||
enabled ? prev | currentFlagValue : prev & ~currentFlagValue,
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useTheme() {
|
||||
);
|
||||
|
||||
const [preference, setPreference] = useState<Theme>(() =>
|
||||
typeof window !== "undefined" ? getStoredPreference() : "light"
|
||||
typeof window !== "undefined" ? getStoredPreference() : "light",
|
||||
);
|
||||
|
||||
const theme = preference === "system" ? getSystemTheme() : preference;
|
||||
@@ -24,7 +24,9 @@ export function useTheme() {
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preference !== "system") return;
|
||||
if (preference !== "system") {
|
||||
return;
|
||||
}
|
||||
|
||||
const media = globalThis.matchMedia("(prefers-color-scheme: dark)");
|
||||
const updateTheme = () => setPreference(getStoredPreference());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("useToast", () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type ReactNode, useSyncExternalStore } from "react";
|
||||
|
||||
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.tsx";
|
||||
import { type ReactNode, useSyncExternalStore } from "react";
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
@@ -31,21 +30,21 @@ type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
@@ -81,7 +80,7 @@ export const reducer = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
@@ -103,10 +102,10 @@ export const reducer = (state: State, action: Action): State => {
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -156,7 +155,9 @@ function toast({ delay = 0, ...props }: Toast) {
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open: boolean) => {
|
||||
if (!open) dismiss();
|
||||
if (!open) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
useDevice,
|
||||
type ValidConfigType,
|
||||
type ValidModuleConfigType,
|
||||
} from "@core/stores/deviceStore.ts";
|
||||
|
||||
type UseWaitForConfigProps =
|
||||
| { configCase: ValidConfigType; moduleConfigCase?: never }
|
||||
| { configCase?: never; moduleConfigCase: ValidModuleConfigType };
|
||||
|
||||
export function useWaitForConfig({
|
||||
configCase,
|
||||
moduleConfigCase,
|
||||
}: UseWaitForConfigProps): void {
|
||||
const { config, moduleConfig } = useDevice();
|
||||
|
||||
const isDataDefined = configCase
|
||||
? config[configCase] !== undefined
|
||||
: moduleConfig[moduleConfigCase as ValidModuleConfigType] !== undefined;
|
||||
|
||||
if (!isDataDefined) {
|
||||
throw new Promise<void>(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user