refactor: consolidate shared application helpers

This commit is contained in:
2026-07-29 12:28:51 +07:00 Unverified
parent 1fb154b465
commit 6b0440602b
14 changed files with 45 additions and 45 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ import {
import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Image as ImageIcon, X, Check, GripVertical, AlertCircle } from "lucide-react";
import Image from 'next/image';
import { RichTextEditor } from "@/components/rich-text-editor";
import { getErrorMessage } from "@/lib/errors";
import { getErrorMessage } from "@/lib/utils";
import {
Accordion,
AccordionContent,
+1 -1
View File
@@ -19,7 +19,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area"
import { getErrorMessage } from "@/lib/errors";
import { getErrorMessage } from "@/lib/utils";
import {
isValidDiscordWebhookUrl,
previewWebhookTemplate,
+1 -1
View File
@@ -1,5 +1,5 @@
import { db } from "@/db";
import { cdnImageResponse } from "@/lib/cdn-response";
import { cdnImageResponse } from "@/lib/cdn-images";
export async function GET(
_request: Request,
+1 -1
View File
@@ -35,7 +35,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { DropdownMenuAvatar, DiscordMemberProfile } from "@/components/dropdown-menu-avatar";
import { getErrorMessage } from "@/lib/errors";
import { getErrorMessage } from "@/lib/utils";
import {
Accordion,
AccordionContent,
+1 -1
View File
@@ -28,7 +28,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Spinner } from "@/components/ui/spinner";
import { getErrorMessage } from "@/lib/errors";
import { getErrorMessage } from "@/lib/utils";
interface DeleteFormButtonProps {
formId: string;
+1 -1
View File
@@ -3,7 +3,7 @@
import type { ComponentProps, MouseEvent } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { flushPendingSaves } from "@/lib/admin-form-save-bridge";
import { flushPendingSaves } from "@/hooks/use-debounced-autosave";
export function GuardedLink({
href,
+15 -1
View File
@@ -1,10 +1,24 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { registerPendingSave } from "@/lib/admin-form-save-bridge";
export type AutoSaveStatus = "clean" | "dirty" | "saving" | "saved" | "error";
type FlushSave = () => Promise<void>;
const pendingSaves = new Set<FlushSave>();
export function registerPendingSave(flush: FlushSave) {
pendingSaves.add(flush);
return () => {
pendingSaves.delete(flush);
};
}
export async function flushPendingSaves() {
await Promise.all([...pendingSaves].map((flush) => flush()));
}
export function useDebouncedAutoSave({
save,
delay = 1000,
-16
View File
@@ -1,16 +0,0 @@
"use client";
type FlushSave = () => Promise<void>;
const pendingSaves = new Set<FlushSave>();
export function registerPendingSave(flush: FlushSave) {
pendingSaves.add(flush);
return () => {
pendingSaves.delete(flush);
};
}
export async function flushPendingSaves() {
await Promise.all([...pendingSaves].map((flush) => flush()));
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { cdnImageResponse } from "@/lib/cdn-response";
import {
cdnImageResponse,
MAX_FORM_ANSWER_IMAGE_BYTES,
validateImageUploadFile,
} from "@/lib/cdn-images";
+17
View File
@@ -27,3 +27,20 @@ export function validateImageUploadFile(file: Pick<File, "name" | "type" | "size
export function cdnUrl(id: string | null | undefined) {
return id ? `/cdn/${id}` : "";
}
export type CdnResponseRecord = {
data: Buffer;
type: string;
size: number;
};
export function cdnImageResponse(record: CdnResponseRecord) {
return new Response(new Uint8Array(record.data), {
headers: {
"Content-Type": record.type,
"Content-Length": String(record.size),
"Cache-Control": "public, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
});
}
-16
View File
@@ -1,16 +0,0 @@
export type CdnResponseRecord = {
data: Buffer;
type: string;
size: number;
};
export function cdnImageResponse(record: CdnResponseRecord) {
return new Response(new Uint8Array(record.data), {
headers: {
"Content-Type": record.type,
"Content-Length": String(record.size),
"Cache-Control": "public, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
});
}
-3
View File
@@ -1,3 +0,0 @@
export function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
+4
View File
@@ -5,6 +5,10 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
export const stripHtml = (html: string) => {
return (html || "").replace(/<[^>]*>?/gm, '').trim();
};
+1 -1
View File
@@ -9,7 +9,7 @@
"start": "next start -H 0.0.0.0",
"lint": "eslint",
"up": "docker compose up -d --build",
"logs": "docker compose logs -f erika",
"logs": "docker compose logs -f app",
"log": "docker compose logs -f",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",