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 { Plus, Trash, ArrowUp, ArrowDown, Loader2, Image as ImageIcon, X, Check, GripVertical, AlertCircle } from "lucide-react";
import Image from 'next/image'; import Image from 'next/image';
import { RichTextEditor } from "@/components/rich-text-editor"; import { RichTextEditor } from "@/components/rich-text-editor";
import { getErrorMessage } from "@/lib/errors"; import { getErrorMessage } from "@/lib/utils";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
+1 -1
View File
@@ -19,7 +19,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { getErrorMessage } from "@/lib/errors"; import { getErrorMessage } from "@/lib/utils";
import { import {
isValidDiscordWebhookUrl, isValidDiscordWebhookUrl,
previewWebhookTemplate, previewWebhookTemplate,
+1 -1
View File
@@ -1,5 +1,5 @@
import { db } from "@/db"; import { db } from "@/db";
import { cdnImageResponse } from "@/lib/cdn-response"; import { cdnImageResponse } from "@/lib/cdn-images";
export async function GET( export async function GET(
_request: Request, _request: Request,
+1 -1
View File
@@ -35,7 +35,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { DropdownMenuAvatar, DiscordMemberProfile } from "@/components/dropdown-menu-avatar"; import { DropdownMenuAvatar, DiscordMemberProfile } from "@/components/dropdown-menu-avatar";
import { getErrorMessage } from "@/lib/errors"; import { getErrorMessage } from "@/lib/utils";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
+1 -1
View File
@@ -28,7 +28,7 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { getErrorMessage } from "@/lib/errors"; import { getErrorMessage } from "@/lib/utils";
interface DeleteFormButtonProps { interface DeleteFormButtonProps {
formId: string; formId: string;
+1 -1
View File
@@ -3,7 +3,7 @@
import type { ComponentProps, MouseEvent } from "react"; import type { ComponentProps, MouseEvent } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { flushPendingSaves } from "@/lib/admin-form-save-bridge"; import { flushPendingSaves } from "@/hooks/use-debounced-autosave";
export function GuardedLink({ export function GuardedLink({
href, href,
+15 -1
View File
@@ -1,10 +1,24 @@
"use client"; "use client";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { registerPendingSave } from "@/lib/admin-form-save-bridge";
export type AutoSaveStatus = "clean" | "dirty" | "saving" | "saved" | "error"; 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({ export function useDebouncedAutoSave({
save, save,
delay = 1000, 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 { describe, expect, test } from "bun:test";
import { cdnImageResponse } from "@/lib/cdn-response";
import { import {
cdnImageResponse,
MAX_FORM_ANSWER_IMAGE_BYTES, MAX_FORM_ANSWER_IMAGE_BYTES,
validateImageUploadFile, validateImageUploadFile,
} from "@/lib/cdn-images"; } 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) { export function cdnUrl(id: string | null | undefined) {
return id ? `/cdn/${id}` : ""; 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;
}
+5 -1
View File
@@ -5,6 +5,10 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function getErrorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
export const stripHtml = (html: string) => { export const stripHtml = (html: string) => {
return (html || "").replace(/<[^>]*>?/gm, '').trim(); return (html || "").replace(/<[^>]*>?/gm, '').trim();
}; };
@@ -17,4 +21,4 @@ export const copyToClipboard = async (text: string): Promise<boolean> => {
console.error("Copy error:", error); console.error("Copy error:", error);
return false; return false;
} }
}; };
+1 -1
View File
@@ -9,7 +9,7 @@
"start": "next start -H 0.0.0.0", "start": "next start -H 0.0.0.0",
"lint": "eslint", "lint": "eslint",
"up": "docker compose up -d --build", "up": "docker compose up -d --build",
"logs": "docker compose logs -f erika", "logs": "docker compose logs -f app",
"log": "docker compose logs -f", "log": "docker compose logs -f",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",