since3-portal/modules/shared/schemas/documents.ts
2026-09-29 21:47:44 +02:00

103 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as z from "zod";
export const MAX_FILE_BYTES = 50 * 1024 * 1024;
export const MAX_FILES_PER_UPLOAD = 20;
export const DOCUMENTS_BUCKET = "client-documents";
export const MAX_THUMBNAIL_BYTES = 512 * 1024;
export const THUMBNAIL_MAX_EDGE = 480;
export type Folder = {
id: string;
name: string;
documentCount: number;
};
export type DocumentItem = {
id: string;
name: string;
mimeType: string | null;
sizeBytes: number | null;
createdAt: string;
thumbnailUrl: string | null;
};
export type FileKind = "image" | "pdf" | "text" | "spreadsheet" | "presentation" | "document" | "archive" | "video" | "audio" | "other";
const EXTENSION_KINDS: Record<string, FileKind> = {
png: "image", jpg: "image", jpeg: "image", gif: "image", webp: "image", svg: "image",
pdf: "pdf",
txt: "text", md: "text", csv: "spreadsheet", json: "text",
xls: "spreadsheet", xlsx: "spreadsheet", ods: "spreadsheet",
ppt: "presentation", pptx: "presentation", odp: "presentation",
doc: "document", docx: "document", odt: "document", rtf: "document",
zip: "archive", rar: "archive", "7z": "archive", gz: "archive",
mp4: "video", mov: "video", webm: "video",
mp3: "audio", wav: "audio", m4a: "audio",
};
export function fileKind(name: string, mimeType: string | null): FileKind {
const extension = name.split(".").pop()?.toLowerCase() ?? "";
if (EXTENSION_KINDS[extension]) return EXTENSION_KINDS[extension];
if (mimeType?.startsWith("image/")) return "image";
if (mimeType?.startsWith("video/")) return "video";
if (mimeType?.startsWith("audio/")) return "audio";
if (mimeType === "application/pdf") return "pdf";
if (mimeType?.startsWith("text/")) return "text";
return "other";
}
export type PreviewKind = "image" | "pdf" | "text" | "video" | "audio";
// Vorschau richtet sich nach dem Content-Type, mit dem der Storage die Datei ausliefert – nicht nach der Endung.
// So landet z. B. ein als ".pdf" benanntes HTML nie im Iframe. SVG (kann Skripte enthalten) nur als Download.
const PREVIEW_TYPES: Record<string, PreviewKind> = {
"image/png": "image",
"image/jpeg": "image",
"image/gif": "image",
"image/webp": "image",
"application/pdf": "pdf",
"text/plain": "text",
"text/markdown": "text",
"text/csv": "text",
"application/json": "text",
"video/mp4": "video",
"video/webm": "video",
"video/quicktime": "video",
"audio/mpeg": "audio",
"audio/wav": "audio",
"audio/mp4": "audio",
"audio/x-m4a": "audio",
};
export function previewKind(mimeType: string | null): PreviewKind | null {
if (!mimeType) return null;
return PREVIEW_TYPES[mimeType.split(";")[0].trim().toLowerCase()] ?? null;
}
export function formatFileSize(bytes: number | null) {
if (bytes === null) return "";
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB"];
let value = bytes / 1024;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit++;
}
return `${value.toLocaleString("de-DE", { maximumFractionDigits: value < 10 ? 1 : 0 })} ${units[unit]}`;
}
export const UploadFileSchema = z.object({
name: z.string().trim().min(1).max(255),
size: z.number().int().positive({ error: "Leere Dateien können nicht hochgeladen werden." }).max(MAX_FILE_BYTES, {
error: "Dateien dürfen maximal 50 MB groß sein.",
}),
type: z.string().max(255),
});
export const FolderNameSchema = z
.string()
.trim()
.min(1, { error: "Name ist erforderlich." })
.max(100, { error: "Name ist zu lang." })
.refine((value) => !/[\\/]/.test(value), { error: "Name darf keine Schrägstriche enthalten." });