refactor: consolidate shared application helpers
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,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
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function getErrorMessage(error: unknown, fallback: string) {
|
|
||||||
return error instanceof Error ? error.message : fallback;
|
|
||||||
}
|
|
||||||
+5
-1
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user