refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes';
const DEFAULT_AVATAR_PRIMARY_COLORS = [0x4641d9, 0xf0b100, 0x00bba7, 0x2b7fff, 0xad46ff, 0x6a7282];
export const DEFAULT_AVATAR_COUNT = BigInt(DEFAULT_AVATAR_PRIMARY_COLORS.length);
export const normalizeEndpoint = (endpoint: string): string => endpoint.replace(/\/$/, '');
export const parseAvatarHash = (value: string) => {
const animated = value.startsWith('a_');
const hash = animated ? value.slice(2) : value;
return {animated, hash};
};
export const buildMediaUrl = ({
endpoint,
path,
id,
hash,
size,
animated,
}: {
endpoint: string;
path: string;
id: string;
hash: string;
size: MediaProxyImageSize;
animated?: boolean;
}) => {
const normalizedEndpoint = normalizeEndpoint(endpoint);
const params = new URLSearchParams();
params.set('size', size.toString());
if (animated) {
params.set('animated', 'true');
}
const query = params.toString();
return `${normalizedEndpoint}/${path}/${id}/${hash}.webp${query ? `?${query}` : ''}`;
};
export const getDefaultAvatarIndex = (id: string): number => Number(BigInt(id) % DEFAULT_AVATAR_COUNT);
export const getDefaultAvatarPrimaryColor = (id: string): number =>
DEFAULT_AVATAR_PRIMARY_COLORS[getDefaultAvatarIndex(id)];

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export type ClassNameInput = string | boolean | undefined | null;
export function cn(...inputs: Array<ClassNameInput>): string {
const classes: Array<string> = [];
for (const input of inputs) {
if (typeof input === 'string' && input.length > 0) {
classes.push(input);
}
}
return classes.join(' ');
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export type ColorTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'primary' | 'purple' | 'orange';
export type ColorIntensity = 'subtle' | 'normal' | 'strong';
export interface ColorVariant {
bg: string;
text: string;
border?: string;
}
export const colorVariants: Record<ColorTone, Record<ColorIntensity, ColorVariant>> = {
neutral: {
subtle: {bg: 'bg-neutral-50', text: 'text-neutral-700', border: 'border-neutral-200'},
normal: {bg: 'bg-neutral-100', text: 'text-neutral-700', border: 'border-neutral-200'},
strong: {bg: 'bg-neutral-900', text: 'text-white'},
},
info: {
subtle: {bg: 'bg-blue-50', text: 'text-blue-800', border: 'border-blue-200'},
normal: {bg: 'bg-blue-100', text: 'text-blue-700', border: 'border-blue-200'},
strong: {bg: 'bg-blue-600', text: 'text-white'},
},
success: {
subtle: {bg: 'bg-green-50', text: 'text-green-800', border: 'border-green-200'},
normal: {bg: 'bg-green-100', text: 'text-green-700', border: 'border-green-200'},
strong: {bg: 'bg-green-600', text: 'text-white'},
},
warning: {
subtle: {bg: 'bg-yellow-50', text: 'text-yellow-800', border: 'border-yellow-200'},
normal: {bg: 'bg-yellow-100', text: 'text-yellow-700', border: 'border-yellow-200'},
strong: {bg: 'bg-yellow-600', text: 'text-white'},
},
danger: {
subtle: {bg: 'bg-red-50', text: 'text-red-800', border: 'border-red-200'},
normal: {bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-200'},
strong: {bg: 'bg-red-600', text: 'text-white'},
},
primary: {
subtle: {bg: 'bg-neutral-100', text: 'text-neutral-700'},
normal: {bg: 'bg-neutral-900', text: 'text-white'},
strong: {bg: 'bg-neutral-900', text: 'text-white'},
},
purple: {
subtle: {bg: 'bg-purple-50', text: 'text-purple-800', border: 'border-purple-200'},
normal: {bg: 'bg-purple-100', text: 'text-purple-700', border: 'border-purple-200'},
strong: {bg: 'bg-purple-600', text: 'text-white'},
},
orange: {
subtle: {bg: 'bg-orange-50', text: 'text-orange-800', border: 'border-orange-200'},
normal: {bg: 'bg-orange-100', text: 'text-orange-700', border: 'border-orange-200'},
strong: {bg: 'bg-orange-600', text: 'text-white'},
},
};
export function getColorClasses(tone: ColorTone, intensity: ColorIntensity = 'normal'): string {
const variant = colorVariants[tone][intensity];
const classes = [variant.bg, variant.text];
if (variant.border) {
classes.push(variant.border);
}
return classes.join(' ');
}
export type AlertTone = 'error' | 'warning' | 'success' | 'info';
export function getAlertClasses(tone: AlertTone): string {
const toneMapping: Record<AlertTone, ColorTone> = {
error: 'danger',
warning: 'warning',
success: 'success',
info: 'info',
};
return getColorClasses(toneMapping[tone], 'subtle');
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export function formatNumber(value: number): string {
const digits = Math.max(0, Math.trunc(value)).toString();
return formatNumberDigits(digits);
}
function formatNumberDigits(digits: string): string {
const len = digits.length;
if (len <= 3) return digits;
const headLength = len % 3 === 0 ? 3 : len % 3;
const head = digits.slice(0, headLength);
const tail = digits.slice(headLength);
return `${head}${chunkDigits(tail)}`;
}
function chunkDigits(digits: string): string {
if (digits.length <= 3) return digits;
const head = digits.slice(0, 3);
const tail = digits.slice(3);
return `${head},${chunkDigits(tail)}`;
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export function formatFileSize(bytes: number, decimals = 2): string {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
import {
MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
MEDIA_PROXY_GUILD_BANNER_SIZE_DEFAULT,
MEDIA_PROXY_GUILD_EMBED_SPLASH_SIZE_DEFAULT,
MEDIA_PROXY_GUILD_SPLASH_SIZE_DEFAULT,
MEDIA_PROXY_ICON_SIZE_DEFAULT,
MEDIA_PROXY_PROFILE_BANNER_SIZE_MODAL,
} from '@fluxer/constants/src/MediaProxyAssetSizes';
import {extractTimestampFromSnowflakeAsDate} from '@fluxer/snowflake/src/SnowflakeUtils';
import {
buildMediaUrl,
getDefaultAvatarIndex,
normalizeEndpoint,
parseAvatarHash,
} from '@fluxer/ui/src/utils/AvatarMediaUtils';
export function formatDiscriminator(discriminator: number | string): string {
const discStr = String(discriminator).padStart(4, '0');
return discStr;
}
export function formatUserTag(username: string, discriminator: string | number): string {
const discStr = typeof discriminator === 'number' ? formatDiscriminator(discriminator) : discriminator;
return `${username}#${discStr}`;
}
export function getInitials(name: string): string {
if (!name || name.trim() === '') {
return '?';
}
const words = name.trim().split(/\s+/);
const firstWord = words[0];
const lastWord = words[words.length - 1];
if (!firstWord) {
return '?';
}
if (words.length === 1 || !lastWord) {
return firstWord.charAt(0).toUpperCase();
}
return (firstWord.charAt(0) + lastWord.charAt(0)).toUpperCase();
}
export function extractTimestampFromSnowflake(snowflake: string, epoch = '1420070400000'): string {
try {
const date = extractTimestampFromSnowflakeAsDate(snowflake, epoch);
if (Number.isNaN(date.getTime())) {
return 'Unknown';
}
const year = date.getUTCFullYear().toString();
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
const day = date.getUTCDate().toString().padStart(2, '0');
const hour = date.getUTCHours().toString().padStart(2, '0');
const minute = date.getUTCMinutes().toString().padStart(2, '0');
const monthNames: Record<string, string> = {
'01': 'Jan',
'02': 'Feb',
'03': 'Mar',
'04': 'Apr',
'05': 'May',
'06': 'Jun',
'07': 'Jul',
'08': 'Aug',
'09': 'Sep',
'10': 'Oct',
'11': 'Nov',
'12': 'Dec',
};
const monthName = monthNames[month] ?? month;
return `${monthName} ${day}, ${year} at ${hour}:${minute}`;
} catch {
return 'Unknown';
}
}
export function getUserAvatarUrl(
mediaEndpoint: string,
staticCdnEndpoint: string,
userId: string,
avatar: string | null,
forceStatic: boolean,
_assetVersion: string,
): string {
if (avatar) {
const {hash, animated} = parseAvatarHash(avatar);
const shouldAnimate = animated && !forceStatic;
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'avatars',
id: userId,
hash,
size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT,
animated: shouldAnimate,
});
}
const defaultIndex = getDefaultAvatarIndex(userId);
return `${normalizeEndpoint(staticCdnEndpoint)}/avatars/${defaultIndex}.png`;
}
export function getGuildIconUrl(
mediaEndpoint: string,
guildId: string,
icon: string | null,
forceStatic: boolean,
): string | null {
if (!icon) {
return null;
}
const {hash, animated} = parseAvatarHash(icon);
const shouldAnimate = animated && !forceStatic;
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'icons',
id: guildId,
hash,
size: MEDIA_PROXY_ICON_SIZE_DEFAULT,
animated: shouldAnimate,
});
}
export function getUserBannerUrl(
mediaEndpoint: string,
userId: string,
banner: string | null,
forceStatic: boolean,
): string | null {
if (!banner) {
return null;
}
const {hash, animated} = parseAvatarHash(banner);
const shouldAnimate = animated && !forceStatic;
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'banners',
id: userId,
hash,
size: MEDIA_PROXY_PROFILE_BANNER_SIZE_MODAL,
animated: shouldAnimate,
});
}
export function getGuildBannerUrl(
mediaEndpoint: string,
guildId: string,
banner: string | null,
forceStatic: boolean,
): string | null {
if (!banner) {
return null;
}
const {hash, animated} = parseAvatarHash(banner);
const shouldAnimate = animated && !forceStatic;
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'banners',
id: guildId,
hash,
size: MEDIA_PROXY_GUILD_BANNER_SIZE_DEFAULT,
animated: shouldAnimate,
});
}
export function getGuildSplashUrl(mediaEndpoint: string, guildId: string, splash: string | null): string | null {
if (!splash) {
return null;
}
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'splashes',
id: guildId,
hash: splash,
size: MEDIA_PROXY_GUILD_SPLASH_SIZE_DEFAULT,
});
}
export function getGuildEmbedSplashUrl(mediaEndpoint: string, guildId: string, splash: string | null): string | null {
if (!splash) {
return null;
}
return buildMediaUrl({
endpoint: mediaEndpoint,
path: 'embed-splashes',
id: guildId,
hash: splash,
size: MEDIA_PROXY_GUILD_EMBED_SPLASH_SIZE_DEFAULT,
});
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
/** @jsxRuntime automatic */
/** @jsxImportSource hono/jsx */
export function createVariantClasses<T extends string>(
variants: Record<T, string>,
base: string = '',
): (variant: T) => string {
return (variant: T) => {
const classes = variants[variant];
return base ? `${base} ${classes}` : classes;
};
}
export function createCompoundVariantClasses<T extends string, S extends string>(
variants: Record<T, string>,
sizes?: Record<S, string>,
base: string = '',
): {
getVariant: (variant: T) => string;
getSize: (size: S) => string;
getBase: () => string;
} {
return {
getVariant: (variant: T) => {
const classes = variants[variant];
return base ? `${base} ${classes}` : classes;
},
getSize: (size: S) => {
if (!sizes) return '';
return sizes[size];
},
getBase: () => base,
};
}