INit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Chief-spartan-117
2025-09-28 19:55:43 +05:45
commit 2162084b95
236 changed files with 28717 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import { useCallback, useEffect, useState } from 'react';
export type Appearance = 'light' | 'dark' | 'system';
const prefersDark = () => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const setCookie = (name: string, value: string, days = 365) => {
if (typeof document === 'undefined') {
return;
}
const maxAge = days * 24 * 60 * 60;
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
const applyTheme = (appearance: Appearance) => {
const isDark =
appearance === 'dark' || (appearance === 'system' && prefersDark());
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
};
const mediaQuery = () => {
if (typeof window === 'undefined') {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
};
const handleSystemThemeChange = () => {
const currentAppearance = localStorage.getItem('appearance') as Appearance;
applyTheme(currentAppearance || 'system');
};
export function initializeTheme() {
const savedAppearance =
(localStorage.getItem('appearance') as Appearance) || 'system';
applyTheme(savedAppearance);
// Add the event listener for system theme changes...
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
}
export function useAppearance() {
const [appearance, setAppearance] = useState<Appearance>('system');
const updateAppearance = useCallback((mode: Appearance) => {
setAppearance(mode);
// Store in localStorage for client-side persistence...
localStorage.setItem('appearance', mode);
// Store in cookie for SSR...
setCookie('appearance', mode);
applyTheme(mode);
}, []);
useEffect(() => {
const savedAppearance = localStorage.getItem(
'appearance',
) as Appearance | null;
updateAppearance(savedAppearance || 'system');
return () =>
mediaQuery()?.removeEventListener(
'change',
handleSystemThemeChange,
);
}, [updateAppearance]);
return { appearance, updateAppearance } as const;
}

View File

@@ -0,0 +1,32 @@
// Credit: https://usehooks-ts.com/
import { useCallback, useState } from 'react';
type CopiedValue = string | null;
type CopyFn = (text: string) => Promise<boolean>;
export function useClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null);
const copy: CopyFn = useCallback(async (text) => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported');
return false;
}
try {
await navigator.clipboard.writeText(text);
setCopiedText(text);
return true;
} catch (error) {
console.warn('Copy failed', error);
setCopiedText(null);
return false;
}
}, []);
return [copiedText, copy];
}

View File

@@ -0,0 +1,15 @@
import { useCallback } from 'react';
export function useInitials() {
return useCallback((fullName: string): string => {
const names = fullName.trim().split(' ');
if (names.length === 0) return '';
if (names.length === 1) return names[0].charAt(0).toUpperCase();
const firstInitial = names[0].charAt(0);
const lastInitial = names[names.length - 1].charAt(0);
return `${firstInitial}${lastInitial}`.toUpperCase();
}, []);
}

View File

@@ -0,0 +1,8 @@
import { useCallback } from 'react';
export function useMobileNavigation() {
return useCallback(() => {
// Remove pointer-events style from body...
document.body.style.removeProperty('pointer-events');
}, []);
}

View File

@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean>();
useEffect(() => {
const mql = window.matchMedia(
`(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
}

View File

@@ -0,0 +1,104 @@
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
import { useCallback, useMemo, useState } from 'react';
interface TwoFactorSetupData {
svg: string;
url: string;
}
interface TwoFactorSecretKey {
secretKey: string;
}
export const OTP_MAX_LENGTH = 6;
const fetchJson = async <T>(url: string): Promise<T> => {
const response = await fetch(url, {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status}`);
}
return response.json();
};
export const useTwoFactorAuth = () => {
const [qrCodeSvg, setQrCodeSvg] = useState<string | null>(null);
const [manualSetupKey, setManualSetupKey] = useState<string | null>(null);
const [recoveryCodesList, setRecoveryCodesList] = useState<string[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const hasSetupData = useMemo<boolean>(
() => qrCodeSvg !== null && manualSetupKey !== null,
[qrCodeSvg, manualSetupKey],
);
const fetchQrCode = useCallback(async (): Promise<void> => {
try {
const { svg } = await fetchJson<TwoFactorSetupData>(qrCode.url());
setQrCodeSvg(svg);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch QR code']);
setQrCodeSvg(null);
}
}, []);
const fetchSetupKey = useCallback(async (): Promise<void> => {
try {
const { secretKey: key } = await fetchJson<TwoFactorSecretKey>(
secretKey.url(),
);
setManualSetupKey(key);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch a setup key']);
setManualSetupKey(null);
}
}, []);
const clearErrors = useCallback((): void => {
setErrors([]);
}, []);
const clearSetupData = useCallback((): void => {
setManualSetupKey(null);
setQrCodeSvg(null);
clearErrors();
}, [clearErrors]);
const fetchRecoveryCodes = useCallback(async (): Promise<void> => {
try {
clearErrors();
const codes = await fetchJson<string[]>(recoveryCodes.url());
setRecoveryCodesList(codes);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch recovery codes']);
setRecoveryCodesList([]);
}
}, [clearErrors]);
const fetchSetupData = useCallback(async (): Promise<void> => {
try {
clearErrors();
await Promise.all([fetchQrCode(), fetchSetupKey()]);
} catch {
setQrCodeSvg(null);
setManualSetupKey(null);
}
}, [clearErrors, fetchQrCode, fetchSetupKey]);
return {
qrCodeSvg,
manualSetupKey,
recoveryCodesList,
hasSetupData,
errors,
clearErrors,
clearSetupData,
fetchQrCode,
fetchSetupKey,
fetchSetupData,
fetchRecoveryCodes,
};
};