feat: add guide page
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { SaveIcon, Trash2 } from "lucide-react";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
||||
import { CdnChooser } from "@/components/chooser";
|
||||
import {
|
||||
FormAction,
|
||||
FormInput,
|
||||
FormProvider,
|
||||
FormRow,
|
||||
} from "@/components/form";
|
||||
import { ModalBase } from "@/components/modal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogClose, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { actionLog } from "@/lib/api";
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { guides } from "@/lib/db/schema";
|
||||
|
||||
export default async function GuideEditPage({
|
||||
params,
|
||||
}: PageProps<"/admin/guide/[id]">) {
|
||||
const { id } = await params;
|
||||
const [guide] = await db.select().from(guides).where(eq(guides.id, id));
|
||||
if (!guide) notFound();
|
||||
|
||||
async function submit(
|
||||
form: TypedFormData<{
|
||||
name: string;
|
||||
link: string;
|
||||
image: string;
|
||||
order: string;
|
||||
}>,
|
||||
) {
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
for (const field of ["name", "link"] as const) {
|
||||
if (!form.get(field)) {
|
||||
return { error: `Field "${field}" is required.` };
|
||||
}
|
||||
}
|
||||
|
||||
let data: Partial<typeof guides.$inferSelect>;
|
||||
|
||||
try {
|
||||
data = {
|
||||
name: form.get("name")!,
|
||||
image: form.get("image")!,
|
||||
link: form.get("link")!,
|
||||
order: parseInt(form.get("order")!, 10),
|
||||
};
|
||||
try {
|
||||
new URL(data.link!);
|
||||
} catch {
|
||||
return { error: "Invalid link." };
|
||||
}
|
||||
await db.update(guides).set(data).where(eq(guides.id, guide.id));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const err = e as Error & { cause: { detail: string; message: string } };
|
||||
return {
|
||||
error:
|
||||
err?.cause?.detail ||
|
||||
err?.cause?.message ||
|
||||
"Failed to update guide.",
|
||||
};
|
||||
}
|
||||
|
||||
await actionLog(`Updated guide ${data.name}`, data);
|
||||
|
||||
revalidatePath("/admin/guide");
|
||||
return { toast: "Guide updated successfully.", close: true };
|
||||
}
|
||||
|
||||
async function deleteGuide() {
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
await db.delete(guides).where(eq(guides.id, id));
|
||||
|
||||
await actionLog(`Deleted guide ${id}`);
|
||||
|
||||
revalidatePath("/admin/guide");
|
||||
return { toast: "Guide deleted.", close: true };
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalBase title="Add Guide">
|
||||
<FormProvider id="guide-create" onSubmit={submit} values={guide}>
|
||||
<FormRow>
|
||||
<FormInput name="name" label="Name">
|
||||
<Input placeholder="[6.3] Flins Guide" autoFocus />
|
||||
</FormInput>
|
||||
<FormInput name="order" label="Order">
|
||||
<Input placeholder={`${guide.order}`} />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormInput name="link" label="Link">
|
||||
<Input placeholder="https://docs.google.com/spreadsheets/..." />
|
||||
</FormInput>
|
||||
<FormInput name="image" label="Image" subLabel="optional">
|
||||
<CdnChooser />
|
||||
</FormInput>
|
||||
<DialogFooter>
|
||||
<Button asChild variant="destructive">
|
||||
<FormAction type="action" action={deleteGuide}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
</FormAction>
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button asChild>
|
||||
<FormAction
|
||||
type="submit"
|
||||
loading={
|
||||
<>
|
||||
<Spinner />
|
||||
Saving...
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SaveIcon />
|
||||
Save
|
||||
</FormAction>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</FormProvider>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
||||
import { CdnChooser } from "@/components/chooser";
|
||||
import {
|
||||
FormAction,
|
||||
FormInput,
|
||||
FormProvider,
|
||||
FormRow,
|
||||
} from "@/components/form";
|
||||
import { ModalBase } from "@/components/modal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogClose, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { actionLog } from "@/lib/api";
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { guides } from "@/lib/db/schema";
|
||||
|
||||
export default async function GuideCreatePage() {
|
||||
const [{ maxOrder }] = await db
|
||||
.select({
|
||||
maxOrder: sql<number>`
|
||||
MAX(${guides.order})`,
|
||||
})
|
||||
.from(guides);
|
||||
async function submit(
|
||||
form: TypedFormData<{
|
||||
name: string;
|
||||
link: string;
|
||||
image: string;
|
||||
order: string;
|
||||
}>,
|
||||
) {
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
for (const field of ["name", "link"] as const) {
|
||||
if (!form.get(field)) {
|
||||
return { error: `Field "${field}" is required.` };
|
||||
}
|
||||
}
|
||||
|
||||
let data: typeof guides.$inferInsert;
|
||||
|
||||
try {
|
||||
data = {
|
||||
name: form.get("name")!,
|
||||
image: form.get("image")!,
|
||||
link: form.get("link")!,
|
||||
order: parseInt(form.get("order")!, 10),
|
||||
};
|
||||
try {
|
||||
new URL(data.link);
|
||||
} catch {
|
||||
return { error: "Invalid link." };
|
||||
}
|
||||
await db.insert(guides).values(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const err = e as Error & { cause: { detail: string; message: string } };
|
||||
return {
|
||||
error:
|
||||
err?.cause?.detail || err?.cause?.message || "Failed to add guide.",
|
||||
};
|
||||
}
|
||||
|
||||
await actionLog(`Added guide ${data.name}`, data);
|
||||
|
||||
revalidatePath("/admin/guide");
|
||||
return { toast: "Guide added successfully.", close: true };
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalBase title="Add Guide">
|
||||
<FormProvider id="guide-create" onSubmit={submit}>
|
||||
<FormRow>
|
||||
<FormInput name="name" label="Name">
|
||||
<Input placeholder="[6.3] Flins Guide" autoFocus />
|
||||
</FormInput>
|
||||
<FormInput name="order" label="Order">
|
||||
<Input
|
||||
placeholder={`${maxOrder + 10}`}
|
||||
defaultValue={maxOrder + 10}
|
||||
/>
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormInput name="link" label="Link">
|
||||
<Input placeholder="https://docs.google.com/spreadsheets/..." />
|
||||
</FormInput>
|
||||
<FormInput name="image" label="Image" subLabel="optional">
|
||||
<CdnChooser />
|
||||
</FormInput>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button asChild>
|
||||
<FormAction
|
||||
type="submit"
|
||||
loading={
|
||||
<>
|
||||
<Spinner />
|
||||
Creating...
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
Create
|
||||
</FormAction>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</FormProvider>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Pencil, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "@/components/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { searchGuide } from "../../guide/api";
|
||||
|
||||
export default async function AdminGuidePage() {
|
||||
const list = await searchGuide();
|
||||
|
||||
return (
|
||||
<div className="grow-0 flex flex-col pt-5 gap-2 mx-4">
|
||||
<Button className="w-fit" asChild>
|
||||
<Link href={`/admin/guide/create`}>
|
||||
<PlusIcon />
|
||||
<span>Add guide</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(min(300px,calc(100svw-2rem-2px)),1fr))]">
|
||||
{list.map((card) => (
|
||||
<Card
|
||||
key={card.id}
|
||||
className="bg-card/50 hover:bg-border backdrop-blur-sm transition-colors py-3 sm:py-6 rounded-sm sm:rounded-xl group relative"
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="absolute opacity-0 group-hover:opacity-100 transition-opacity top-0 right-0 m-2"
|
||||
asChild
|
||||
>
|
||||
<Link href={`/admin/guide/${card.id}`}>
|
||||
<Pencil />
|
||||
</Link>
|
||||
</Button>
|
||||
<CardHeader className="px-3 sm:px-6">
|
||||
<CardTitle>{card.name}</CardTitle>
|
||||
<div className="relative w-full aspect-square mt-2 rounded-sm sm:rounded-lg border overflow-hidden">
|
||||
{card.image && (
|
||||
<Image
|
||||
src={`/cdn/${card.image}`}
|
||||
alt={card.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BadgeDollarSign,
|
||||
BookUser,
|
||||
Columns3Cog,
|
||||
Compass,
|
||||
Computer,
|
||||
Database,
|
||||
ExternalLink,
|
||||
@@ -151,6 +152,19 @@ export default async function AdminLayout({
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Guide</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarLink href="/admin/guide">
|
||||
<Compass />
|
||||
Editor
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Versions [WIP]</SidebarGroupLabel>
|
||||
<Dialog>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BadgeDollarSign,
|
||||
ChevronRight,
|
||||
Columns3Cog,
|
||||
Compass,
|
||||
ExternalLink,
|
||||
Form,
|
||||
GitGraph,
|
||||
@@ -184,6 +185,7 @@ export function AdminNavbar({
|
||||
{ name: "Layout", icon: Columns3Cog, href: "/admin/tl/layout" },
|
||||
],
|
||||
},
|
||||
{ name: "Guide", icon: Compass, href: "/admin/guide" },
|
||||
{
|
||||
name: "Versions",
|
||||
icon: List,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
CircleDollarSign,
|
||||
PencilIcon,
|
||||
SendHorizonal,
|
||||
SquarePlay,
|
||||
} from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
@@ -24,12 +23,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"use server";
|
||||
|
||||
import { ilike } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { guides } from "@/lib/db/schema";
|
||||
|
||||
export async function searchGuide(search: string = "") {
|
||||
return await db
|
||||
.select()
|
||||
.from(guides)
|
||||
.where(ilike(guides.name, `%${search}%`))
|
||||
.orderBy(guides.id);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { Compass, Search } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useDebounce } from "react-use";
|
||||
import Image from "@/components/image";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/components/ui/input-group";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { guides } from "@/lib/db/schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { searchGuide } from "./api";
|
||||
|
||||
export function GuideList({
|
||||
initialList,
|
||||
}: {
|
||||
initialList: (typeof guides.$inferSelect)[];
|
||||
}) {
|
||||
const [list, setList] = useState(initialList);
|
||||
const [loading, setLoading] = useState(2);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (loading === 2) return setLoading(0);
|
||||
setLoading(1);
|
||||
searchGuide(search)
|
||||
.then(setList)
|
||||
.finally(() => setLoading(0));
|
||||
},
|
||||
200,
|
||||
[search],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center h-svh">
|
||||
<div className="w-full xl:max-w-2/3 border flex flex-col">
|
||||
<div className="border-b p-3 text-lg font-semibold leading-none flex items-center gap-2">
|
||||
<Compass className="opacity-50" />
|
||||
ไกด์ตัวละคร
|
||||
</div>
|
||||
<div className="p-4 pb-2">
|
||||
<InputGroup className="w-full h-12">
|
||||
<InputGroupInput
|
||||
placeholder="Search..."
|
||||
className="text-xl!"
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
{loading === 1 ? <Spinner /> : <Search />}
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 p-4 grid-cols-[repeat(auto-fill,minmax(min(300px,calc(100svw-2rem-2px)),1fr))] overflow-auto transition-opacity",
|
||||
loading === 1 && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{list.map((card) => (
|
||||
<Link href={card.link} key={card.id} target="_blank">
|
||||
<Card className="bg-card/50 hover:bg-border backdrop-blur-sm transition-colors py-3 sm:py-6 rounded-sm sm:rounded-xl">
|
||||
<CardHeader className="px-3 sm:px-6">
|
||||
<CardTitle>{card.name}</CardTitle>
|
||||
<div className="relative w-full aspect-square mt-2 rounded-sm sm:rounded-lg border overflow-hidden">
|
||||
{card.image && (
|
||||
<Image
|
||||
src={`/cdn/${card.image}`}
|
||||
alt={card.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { searchGuide } from "./api";
|
||||
import { GuideList } from "./client";
|
||||
|
||||
export default async function GuidePage() {
|
||||
const initialData = await searchGuide();
|
||||
return <GuideList initialList={initialData} />;
|
||||
}
|
||||
+9
-2
@@ -4,15 +4,22 @@ import { Feather, Home, LoaderPinwheel, StickyNote, Table } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Navbar() {
|
||||
const pathname = usePathname();
|
||||
const mobile = useIsMobile();
|
||||
|
||||
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact)$/gm.test(pathname))
|
||||
if (!/^\/(?:tl(?:\/[a-zA-Z0-9]+)?|rubgram|artifact|guide)$/gm.test(pathname))
|
||||
return "";
|
||||
return (
|
||||
<div className="flex gap-2 p-2 rounded-lg absolute top-3 left-3 border bg-card">
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 p-2 rounded-lg absolute border bg-card",
|
||||
mobile && pathname === "/guide" ? "right-2 top-2" : "left-3 top-3",
|
||||
)}
|
||||
>
|
||||
<Item icon={<Home size={16} />} name="หน้าหลัก" href="/" divide />
|
||||
<Item
|
||||
icon={<Feather size={16} />}
|
||||
|
||||
@@ -20,15 +20,15 @@ import {
|
||||
} from "@/components/ui/empty";
|
||||
import { db } from "@/lib/db";
|
||||
import { endgameArchive, endgameDiscord, endgameSlips } from "@/lib/db/schema";
|
||||
import { parseParamNumber } from "@/lib/utils";
|
||||
import { parseSearchNumber } from "@/lib/utils";
|
||||
import { DataViewer } from "./client";
|
||||
|
||||
export default async function ({
|
||||
params,
|
||||
}: PageProps<"/rubgram/admin/slip/[round]/[queue]">) {
|
||||
const { round: r, queue: q } = await params;
|
||||
const round = parseParamNumber(r) || 1;
|
||||
const queue = parseParamNumber(q) || 1;
|
||||
const round = parseSearchNumber(r) || 1;
|
||||
const queue = parseSearchNumber(q) || 1;
|
||||
const { slip, ...slipColumns } = getTableColumns(endgameSlips);
|
||||
const [entry] = await db
|
||||
.select({
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { db } from "@/lib/db";
|
||||
import { endgameArchive } from "@/lib/db/schema";
|
||||
import { parseParamNumber } from "@/lib/utils";
|
||||
import { parseSearchNumber } from "@/lib/utils";
|
||||
import { Watcher } from "../../../../admin/client";
|
||||
|
||||
export default async function ({
|
||||
@@ -40,7 +40,7 @@ export default async function ({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { round: r } = await params;
|
||||
const round = parseParamNumber(r);
|
||||
const round = parseSearchNumber(r);
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<Suspense fallback={<Sidebar />}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/components/ui/empty";
|
||||
import { db } from "@/lib/db";
|
||||
import { endgameArchive } from "@/lib/db/schema";
|
||||
import { parseParamNumber } from "@/lib/utils";
|
||||
import { parseSearchNumber } from "@/lib/utils";
|
||||
|
||||
export default async function ({
|
||||
params,
|
||||
@@ -17,7 +17,7 @@ export default async function ({
|
||||
params: Promise<{ round: string }>;
|
||||
}) {
|
||||
const { round: r } = await params;
|
||||
const round = parseParamNumber(r) || 1;
|
||||
const round = parseSearchNumber(r) || 1;
|
||||
const [first] = await db
|
||||
.select({ queue: endgameArchive.queue })
|
||||
.from(endgameArchive)
|
||||
|
||||
@@ -46,5 +46,6 @@ export const cdnReferences = pgView("cdn_references").as((qb) => {
|
||||
if (prev) return prev.unionAll(query);
|
||||
return query;
|
||||
}, null);
|
||||
// console.dir(queries, { depth: 5 });
|
||||
return queries;
|
||||
});
|
||||
|
||||
@@ -326,6 +326,21 @@ export const tierlistStates = tierlist.table("states", {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Guide
|
||||
export const guides = pgTable("guides", {
|
||||
id: text().primaryKey().$defaultFn(uuidv7),
|
||||
image: text().references(() => cdn.id, {
|
||||
onDelete: "set null",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
order: integer().notNull(),
|
||||
link: text().notNull(),
|
||||
hidden: boolean().notNull().default(false),
|
||||
name: text().notNull(),
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Better Auth
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
|
||||
+16
-9
@@ -170,13 +170,20 @@ export function shortestPrefixes(values: readonly string[]) {
|
||||
|
||||
return res;
|
||||
}
|
||||
export function parseParamNumber(param: string | string[] | undefined) {
|
||||
if (Array.isArray(param)) {
|
||||
if (param.length < 1) return 0;
|
||||
return parseParamNumber(param[0]);
|
||||
}
|
||||
if (!param) return 0;
|
||||
const int = parseInt(param, 10);
|
||||
if (Number.isNaN(int)) return 0;
|
||||
return int;
|
||||
|
||||
export function parseSearchNumber(
|
||||
param: string | string[] | undefined,
|
||||
def: number = 0,
|
||||
) {
|
||||
if (Array.isArray(param)) return parseSearchNumber(param[0], def);
|
||||
const parsed = parseFloat(param ?? "");
|
||||
return Number.isNaN(parsed) ? def : parsed;
|
||||
}
|
||||
|
||||
export function parseSearchString(
|
||||
param: string | string[] | undefined,
|
||||
def: string = "",
|
||||
) {
|
||||
if (Array.isArray(param)) return parseSearchString(param[0], def);
|
||||
return param ?? def;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user