first commit
linter / quality (push) Canceled after 0s
tests / ci (8.3) (push) Canceled after 0s
tests / ci (8.4) (push) Canceled after 0s
tests / ci (8.5) (push) Canceled after 0s

This commit is contained in:
salam committed 2026-10-06 18:53:03 +02:00
commit e687b4437b
418 files changed
+33577

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

+80
View File
@@ -0,0 +1,80 @@
import type { LinkComponentBaseProps } from '@inertiajs/core';
import { page } from '@inertiajs/svelte';
import { toUrl } from '@/lib/utils';
export type CurrentUrlState = {
readonly currentUrl: string;
isCurrentUrl: (
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
currentUrl: string,
) => boolean;
isCurrentOrParentUrl: (
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
currentUrl: string,
) => boolean;
whenCurrentUrl: <TIfTrue, TIfFalse>(
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
currentUrl: string,
ifTrue: TIfTrue,
ifFalse: TIfFalse,
) => TIfTrue | TIfFalse;
};
export function currentUrlState(): CurrentUrlState {
const currentUrl = $derived.by(() => {
const origin =
typeof window === 'undefined'
? 'http://localhost'
: window.location.origin;
try {
return new URL(page.url, origin).pathname;
} catch {
return page.url;
}
});
function isCurrentUrl(
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
current: string,
): boolean {
const resolved = toUrl(urlToCheck);
if (typeof resolved !== 'string') {
return false;
}
return current === resolved;
}
function isCurrentOrParentUrl(
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
current: string,
): boolean {
const resolved = toUrl(urlToCheck);
if (typeof resolved !== 'string') {
return false;
}
return current.startsWith(resolved);
}
function whenCurrentUrl<TIfTrue, TIfFalse>(
urlToCheck: NonNullable<LinkComponentBaseProps['href']>,
current: string,
ifTrue: TIfTrue,
ifFalse: TIfFalse,
): TIfTrue | TIfFalse {
return isCurrentUrl(urlToCheck, current) ? ifTrue : ifFalse;
}
return {
get currentUrl() {
return currentUrl;
},
isCurrentUrl,
isCurrentOrParentUrl,
whenCurrentUrl,
};
}
+16
View File
@@ -0,0 +1,16 @@
import { router } from '@inertiajs/svelte';
import { toast } from 'svelte-sonner';
import type { FlashToast } from '@/types/ui';
export function initializeFlashToast(): void {
router.on('flash', (event) => {
const flash = (event as CustomEvent).detail?.flash;
const data = flash?.toast as FlashToast | undefined;
if (!data) {
return;
}
toast[data.type](data.message);
});
}
+25
View File
@@ -0,0 +1,25 @@
export type InitialsApi = {
getInitials: (fullName?: string) => string;
};
export function getInitials(fullName?: string): string {
if (!fullName) {
return '';
}
const names = fullName.trim().split(' ');
if (names.length === 0) {
return '';
}
if (names.length === 1) {
return names[0].charAt(0).toUpperCase();
}
return `${names[0].charAt(0)}${names[names.length - 1].charAt(0)}`.toUpperCase();
}
export function initialsApi(): InitialsApi {
return { getInitials };
}
+117
View File
@@ -0,0 +1,117 @@
import type { Appearance, ResolvedAppearance } from '@/types';
export type { Appearance, ResolvedAppearance };
export type ThemeState = {
appearance: {
value: Appearance;
};
resolvedAppearance: () => ResolvedAppearance;
updateAppearance: (value: Appearance) => void;
};
const appearance = $state<{ value: Appearance }>({ value: 'system' });
let themeChangeMediaQuery: MediaQueryList | null = null;
const prefersDark = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const isDarkMode = (value: Appearance): boolean => {
return value === 'dark' || (value === 'system' && prefersDark());
};
const getResolvedAppearance = (): ResolvedAppearance => {
return isDarkMode(appearance.value) ? 'dark' : 'light';
};
const setCookie = (name: string, value: string, days = 365): void => {
if (typeof document === 'undefined') {
return;
}
const maxAge = days * 24 * 60 * 60;
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
const applyTheme = (value: Appearance): void => {
if (typeof document === 'undefined') {
return;
}
const isDark = isDarkMode(value);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
};
const getStoredAppearance = (): Appearance => {
if (typeof window === 'undefined') {
return 'system';
}
const stored = localStorage.getItem('appearance');
return stored === 'light' || stored === 'dark' || stored === 'system'
? stored
: 'system';
};
const handleSystemThemeChange = (): void => {
applyTheme(appearance.value);
};
const detachThemeChangeListener = (): void => {
if (!themeChangeMediaQuery) {
return;
}
themeChangeMediaQuery.removeEventListener(
'change',
handleSystemThemeChange,
);
themeChangeMediaQuery = null;
};
export function initializeTheme(): () => void {
if (typeof window === 'undefined') {
return () => {};
}
if (!localStorage.getItem('appearance')) {
localStorage.setItem('appearance', 'system');
setCookie('appearance', 'system');
}
appearance.value = getStoredAppearance();
applyTheme(appearance.value);
detachThemeChangeListener();
themeChangeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
themeChangeMediaQuery.addEventListener('change', handleSystemThemeChange);
return detachThemeChangeListener;
}
export function updateAppearance(value: Appearance): void {
appearance.value = value;
if (typeof window !== 'undefined') {
localStorage.setItem('appearance', value);
}
setCookie('appearance', value);
applyTheme(value);
}
export function themeState(): ThemeState {
return {
appearance,
resolvedAppearance: getResolvedAppearance,
updateAppearance,
};
}
+112
View File
@@ -0,0 +1,112 @@
import { useHttp } from '@inertiajs/svelte';
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
type TwoFactorAuthState = {
qrCodeSvg: string | null;
manualSetupKey: string | null;
recoveryCodesList: string[];
errors: string[];
};
export type TwoFactorAuthStateApi = {
state: TwoFactorAuthState;
hasSetupData: () => boolean;
clearSetupData: () => void;
clearErrors: () => void;
clearTwoFactorAuthData: () => void;
fetchQrCode: () => Promise<void>;
fetchSetupKey: () => Promise<void>;
fetchSetupData: () => Promise<void>;
fetchRecoveryCodes: () => Promise<void>;
};
const state = $state<TwoFactorAuthState>({
qrCodeSvg: null,
manualSetupKey: null,
recoveryCodesList: [],
errors: [],
});
const hasSetupData = (): boolean =>
state.qrCodeSvg !== null && state.manualSetupKey !== null;
export function twoFactorAuthState(): TwoFactorAuthStateApi {
const http = useHttp();
const fetchQrCode = async (): Promise<void> => {
try {
const { svg } = (await http.submit(qrCode())) as {
svg: string;
url: string;
};
state.qrCodeSvg = svg;
} catch {
state.errors = [...state.errors, 'Failed to fetch QR code'];
state.qrCodeSvg = null;
}
};
const fetchSetupKey = async (): Promise<void> => {
try {
const { secretKey: key } = (await http.submit(secretKey())) as {
secretKey: string;
};
state.manualSetupKey = key;
} catch {
state.errors = [...state.errors, 'Failed to fetch a setup key'];
state.manualSetupKey = null;
}
};
const clearErrors = (): void => {
state.errors = [];
};
const clearSetupData = (): void => {
state.manualSetupKey = null;
state.qrCodeSvg = null;
clearErrors();
};
const clearTwoFactorAuthData = (): void => {
clearSetupData();
state.recoveryCodesList = [];
clearErrors();
};
const fetchRecoveryCodes = async (): Promise<void> => {
try {
clearErrors();
state.recoveryCodesList = (await http.submit(
recoveryCodes(),
)) as string[];
} catch {
state.errors = [...state.errors, 'Failed to fetch recovery codes'];
state.recoveryCodesList = [];
}
};
const fetchSetupData = async (): Promise<void> => {
try {
clearErrors();
await Promise.all([fetchQrCode(), fetchSetupKey()]);
} catch {
state.qrCodeSvg = null;
state.manualSetupKey = null;
}
};
return {
state,
hasSetupData,
clearSetupData,
clearErrors,
clearTwoFactorAuthData,
fetchQrCode,
fetchSetupKey,
fetchSetupData,
fetchRecoveryCodes,
};
}
+14
View File
@@ -0,0 +1,14 @@
import type { LinkComponentBaseProps } from '@inertiajs/core';
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function toUrl(
href: NonNullable<LinkComponentBaseProps['href']>,
): string {
return typeof href === 'string' ? href : href.url;
}