TierList migration finished
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { cdnify } from "@/lib/api";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export async function fetchToCdn(urls: string[]) {
|
||||
async function toFile(res: Response) {
|
||||
return new File(
|
||||
[await res.blob()],
|
||||
res.headers
|
||||
.get("Content-Disposition")
|
||||
?.match(/^attachment;.*filename="([a-zA-Z0-9-_ .]+)"/)?.[1] ||
|
||||
res.url.match(/\/([a-zA-Z0-9-_ .]+)$/)?.[1] ||
|
||||
`file.${res.headers.get("Content-Type")?.split("/")[1] || "dat"}`,
|
||||
);
|
||||
}
|
||||
return db
|
||||
.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
urls.map((url) =>
|
||||
fetch(url, { redirect: "follow" })
|
||||
.then(toFile)
|
||||
.then((f) => cdnify(f, { tx })),
|
||||
),
|
||||
),
|
||||
)
|
||||
.finally(() => revalidatePath("/admin/cdn"));
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import { DataTable } from "@/components/tantable";
|
||||
import { db } from "@/lib/db";
|
||||
import { cdn } from "@/lib/db/schema";
|
||||
import { cdnColumns } from "./table";
|
||||
import { CdnTable } from "./table";
|
||||
|
||||
export default async function CdnManagerPage() {
|
||||
const files = await db
|
||||
.select({ id: cdn.id, name: cdn.name, size: cdn.size })
|
||||
.from(cdn)
|
||||
.orderBy(cdn.name);
|
||||
return <DataTable columns={cdnColumns} data={files} />;
|
||||
|
||||
return (
|
||||
<div className="m-2 ml-0">
|
||||
<CdnTable files={files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+192
-2
@@ -1,12 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { ColumnDef, RowSelectionState } from "@tanstack/react-table";
|
||||
import {
|
||||
CloudUpload,
|
||||
HardDriveDownload,
|
||||
ReplaceAll,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { type ChangeEvent, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DataTable } from "@/components/tantable";
|
||||
import { SimpleTooltip } from "@/components/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cdnDelete, cdnify } from "@/lib/api";
|
||||
import { fetchToCdn } from "./api";
|
||||
|
||||
export const cdnColumns: ColumnDef<{
|
||||
const columns: ColumnDef<{
|
||||
id: string;
|
||||
name: string | null;
|
||||
size: string;
|
||||
}>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.name || `[${row.id}]`,
|
||||
header: "Name",
|
||||
@@ -17,6 +64,149 @@ export const cdnColumns: ColumnDef<{
|
||||
},
|
||||
];
|
||||
|
||||
export function CdnTable({
|
||||
files,
|
||||
}: {
|
||||
files: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
size: string;
|
||||
}[];
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [urls, setUrls] = useState("");
|
||||
const selectedIds = useMemo(
|
||||
() =>
|
||||
Object.entries(rowSelection)
|
||||
.filter((r) => r[1])
|
||||
.map((r) => files[Number(r[0])].id),
|
||||
[rowSelection, files],
|
||||
);
|
||||
|
||||
async function upload(ev: ChangeEvent<HTMLInputElement>) {
|
||||
const { files } = ev.target;
|
||||
if (!files) return;
|
||||
for (const file of files) {
|
||||
toast.promise(cdnify(file), {
|
||||
loading: `Uploading ${file.name}...`,
|
||||
success: () => {
|
||||
return `${file.name} has been uploaded to CDN.`;
|
||||
},
|
||||
error: `Error uploading ${file.name}.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
toast.promise(cdnDelete(selectedIds), {
|
||||
loading: `Delete ${selectedIds.length} files...`,
|
||||
success: (res) => {
|
||||
if (res) {
|
||||
const file = files.find((f) => f.id === res.id);
|
||||
toast.error(
|
||||
`Error deleting ${file ? file.name || `[${res.id}]` : `[${res.id}]`}`,
|
||||
{
|
||||
description: `because it is still being used by ${res.refs.join(", ")}`,
|
||||
},
|
||||
);
|
||||
throw "";
|
||||
}
|
||||
setRowSelection({});
|
||||
return `${selectedIds.length} files has been deleted.`;
|
||||
},
|
||||
error: `Error deleting the files.`,
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchUrls() {
|
||||
const urlList: string[] = urls
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((u) => u);
|
||||
if (!urlList.length) return;
|
||||
toast.promise(fetchToCdn(urlList), {
|
||||
loading: `Fetching ${urlList.length} files...`,
|
||||
success: () => {
|
||||
return `${urlList.length} URLs has been uploaded to CDN.`;
|
||||
},
|
||||
error: "Error fetching the files.",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="h-[calc(100svh-60px)] overflow-auto rounded-md bg-[#2225] backdrop-blur-sm border">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={files}
|
||||
className={"border-none"}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between h-9">
|
||||
<div className="flex gap-2">
|
||||
<SimpleTooltip text="Upload...">
|
||||
<Button size="icon" onClick={() => fileInputRef.current?.click()}>
|
||||
<CloudUpload />
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<SimpleTooltip text="Fetch from URL...">
|
||||
<Button size="icon" variant="outline">
|
||||
<HardDriveDownload />
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Fetch From URLs</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter URLs to be fetched and added to CDN below. One URL per
|
||||
line.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
placeholder="https://cdn.dgnr.us/paimon.png..."
|
||||
value={urls}
|
||||
onChange={(ev) => setUrls(ev.target.value)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={fetchUrls}>Fetch</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<SimpleTooltip text="Replace File (WIP)">
|
||||
<Button size="icon" variant="outline" disabled>
|
||||
<ReplaceAll />
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip text="Delete File">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
disabled={selectedIds.length <= 0}
|
||||
onClick={deleteSelected}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden">
|
||||
<input type="file" multiple ref={fileInputRef} onChange={upload} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const b2s = (t: number) => {
|
||||
let e = (Math.log2(t) / 10) | 0;
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: copied
|
||||
|
||||
+34
-5
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { SidebarMenuButton } from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function VersionCreateDialogForm() {
|
||||
return <form>Todo</form>;
|
||||
@@ -9,11 +13,11 @@ export function VersionCreateDialogForm() {
|
||||
export function RandomWelcomeMessage({ list }: { list?: string[] }) {
|
||||
const messages = list || [
|
||||
"What are we cooking today?",
|
||||
"What are we doing today?",
|
||||
"I'm NOT an EMERGENCY FOOD!!",
|
||||
"Why are you here?",
|
||||
"The food is ready sir.",
|
||||
"The emergency food is ready sir.",
|
||||
"Who left the artifact stats like that?",
|
||||
"Don’t forget to hydrate, boss.",
|
||||
"Don't forget to hydrate, boss.",
|
||||
"Another day, another reroll.",
|
||||
"Still reviewing artifacts? Respect.",
|
||||
"Let's make these builds shine.",
|
||||
@@ -26,7 +30,7 @@ export function RandomWelcomeMessage({ list }: { list?: string[] }) {
|
||||
"Your approval power is unmatched.",
|
||||
"Remember: critique kindly, roast fairly.",
|
||||
"Time to bless—or curse—some artifacts.",
|
||||
"Don’t worry, no 5★ drama yet.",
|
||||
"Don't worry, no 5★ drama yet.",
|
||||
"Admin mode: activated.",
|
||||
];
|
||||
|
||||
@@ -40,3 +44,28 @@ export function RandomWelcomeMessage({ list }: { list?: string[] }) {
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export function SidebarLink({
|
||||
className,
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Link>) {
|
||||
const target = useMemo(
|
||||
() =>
|
||||
href
|
||||
? new URL(href.toString(), "https://example.com").pathname
|
||||
: undefined,
|
||||
[href],
|
||||
);
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Link href={href} {...props}>
|
||||
<SidebarMenuButton
|
||||
className={cn(target === pathname ? "bg-accent" : "", className)}
|
||||
>
|
||||
{children}
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"; // Error boundaries must be Client Components
|
||||
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full m-2 ml-0 border bg-[#2225] backdrop-blur-md rounded-md flex flex-col gap-2 justify-center items-center">
|
||||
<h2 className="font-bold text-2xl">Something went wrong!</h2>
|
||||
<Button
|
||||
onClick={
|
||||
// Attempt to recover by trying to re-render the segment
|
||||
() => reset()
|
||||
}
|
||||
>
|
||||
<RefreshCw />
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+32
-46
@@ -48,7 +48,7 @@ import {
|
||||
import { NavUser } from "@/components/user";
|
||||
import { db } from "@/lib/db";
|
||||
import { versions } from "@/lib/db/schema";
|
||||
import { VersionCreateDialogForm } from "./client";
|
||||
import { SidebarLink, VersionCreateDialogForm } from "./client";
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
@@ -133,28 +133,22 @@ export default async function AdminLayout({
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/tl/ver">
|
||||
<SidebarMenuButton>
|
||||
<GitGraph />
|
||||
Versions
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/tl/ver">
|
||||
<GitGraph />
|
||||
Versions
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/tl/badges">
|
||||
<SidebarMenuButton>
|
||||
<Badge />
|
||||
Badges
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/tl/badges">
|
||||
<Badge />
|
||||
Badges
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/tl/layout">
|
||||
<SidebarMenuButton>
|
||||
<Columns3Cog />
|
||||
Layout
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/tl/layout">
|
||||
<Columns3Cog />
|
||||
Layout
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
@@ -192,9 +186,9 @@ export default async function AdminLayout({
|
||||
<SidebarMenu>
|
||||
{vers.map((v) => (
|
||||
<SidebarMenuItem key={v.id}>
|
||||
<Link href={`/admin/ver/${v.id}`}>
|
||||
<SidebarMenuButton>{v.name}</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href={`/admin/ver/${v.id}`}>
|
||||
{v.name}
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
@@ -207,36 +201,28 @@ export default async function AdminLayout({
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/char">
|
||||
<SidebarMenuButton>
|
||||
<SquareUserRound />
|
||||
Characters
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/char">
|
||||
<SquareUserRound />
|
||||
Characters
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/cdn">
|
||||
<SidebarMenuButton>
|
||||
<Package />
|
||||
CDN
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/cdn">
|
||||
<Package />
|
||||
CDN
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem className="mt-auto">
|
||||
<Link href="/admin/log">
|
||||
<SidebarMenuButton>
|
||||
<ScrollText />
|
||||
Audit Log
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/log">
|
||||
<ScrollText />
|
||||
Audit Log
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<Link href="/admin/settings">
|
||||
<SidebarMenuButton>
|
||||
<Settings />
|
||||
Settings
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarLink href="/admin/settings">
|
||||
<Settings />
|
||||
Settings
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
|
||||
Reference in New Issue
Block a user