Two related avatar bugs: 1. getAvatarUrl() returned empty string for S3 keys, so uploaded avatars never displayed. Now constructs the full URL using NEXT_PUBLIC_STORAGE_URL and NEXT_PUBLIC_AVATAR_BUCKET_NAME, with support for both path-style (MinIO) and virtual-hosted (Tigris/AWS S3) URLs. 2. Avatar crop scaled canvas by devicePixelRatio (4x pixels on Retina), producing blobs that exceeded the 2MB upload limit even for small source images. Now caps output at 512x512 and uses quality=0.85 for toBlob(). Closes #440, closes #441 Co-authored-by: Claude <noreply@anthropic.com>
73 lines
1.8 KiB
TypeScript
73 lines
1.8 KiB
TypeScript
import { env } from "next-runtime-env";
|
|
|
|
export const formatToArray = (
|
|
value: string | string[] | undefined,
|
|
): string[] => {
|
|
if (Array.isArray(value)) {
|
|
return value.filter((item) => item !== undefined);
|
|
}
|
|
return value ? [value] : [];
|
|
};
|
|
|
|
export const inferInitialsFromEmail = (email: string) => {
|
|
const localPart = email.split("@")[0];
|
|
if (!localPart) return "";
|
|
const separators = /[._-]/;
|
|
const parts = localPart.split(separators);
|
|
|
|
if (parts.length > 1) {
|
|
return (
|
|
(parts[0]?.[0] ?? "") + (parts[parts.length - 1]?.[0] ?? "")
|
|
).toUpperCase();
|
|
} else {
|
|
return localPart.slice(0, 2).toUpperCase();
|
|
}
|
|
};
|
|
|
|
export const getInitialsFromName = (name: string) => {
|
|
return name
|
|
.split(" ")
|
|
.map((namePart) => namePart.charAt(0).toUpperCase())
|
|
.join("");
|
|
};
|
|
|
|
export const formatMemberDisplayName = (
|
|
name: string | null,
|
|
email: string | null,
|
|
) => {
|
|
if (name) return name;
|
|
if (!email) return "";
|
|
|
|
const localPart = email.split("@")[0];
|
|
|
|
if (!localPart) return "";
|
|
|
|
return localPart.replace(/[_-]/g, ".");
|
|
};
|
|
|
|
export const getAvatarUrl = (imageOrKey: string | null) => {
|
|
if (!imageOrKey) return "";
|
|
|
|
if (imageOrKey.startsWith("http://") || imageOrKey.startsWith("https://")) {
|
|
return imageOrKey;
|
|
}
|
|
|
|
// Construct URL from S3 key
|
|
const useVirtualHosted = env("NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS") === "true";
|
|
const storageDomain = env("NEXT_PUBLIC_STORAGE_DOMAIN");
|
|
const storageUrl = env("NEXT_PUBLIC_STORAGE_URL");
|
|
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
|
|
|
|
if (useVirtualHosted && storageDomain && bucket) {
|
|
// Virtual-hosted style: https://{bucket}.{domain}/{key}
|
|
return `https://${bucket}.${storageDomain}/${imageOrKey}`;
|
|
}
|
|
|
|
if (storageUrl && bucket) {
|
|
// Path-style: {storageUrl}/{bucket}/{key}
|
|
return `${storageUrl}/${bucket}/${imageOrKey}`;
|
|
}
|
|
|
|
return "";
|
|
};
|