feat: add development environment configuration and Docker setup
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { ArrowRight, PlusIcon, Trash2 } from "lucide-react";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { TypedFormData } from "@/app/(ui)/rubgram/type";
|
||||
import { CdnChooser } from "@/components/chooser";
|
||||
import { DatePicker } from "@/components/date";
|
||||
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 { Kbd, KbdGroup } from "@/components/ui/kbd";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { actionLog } from "@/lib/api";
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { tierlistTypes, tierlistVersions, versions } from "@/lib/db/schema";
|
||||
|
||||
export default async function TlVersionEditPage({
|
||||
params,
|
||||
}: PageProps<"/admin/tl/ver/[type]/[ver]/edit">) {
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
const { type: typeId, ver: verId } = await params;
|
||||
const [[type], [ver], versionList] = await Promise.all([
|
||||
db.select().from(tierlistTypes).where(eq(tierlistTypes.id, typeId)),
|
||||
db.select().from(tierlistVersions).where(eq(tierlistVersions.id, verId)),
|
||||
db.select().from(versions).orderBy(desc(versions.id)),
|
||||
]);
|
||||
if (!type) notFound();
|
||||
if (!ver) notFound();
|
||||
|
||||
async function submit(
|
||||
form: TypedFormData<{
|
||||
name: string;
|
||||
id: string;
|
||||
order: string;
|
||||
from: string;
|
||||
image: string;
|
||||
disclaimer: string;
|
||||
deprecates: string;
|
||||
}>,
|
||||
) {
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
for (const field of [
|
||||
"id",
|
||||
"name",
|
||||
"image",
|
||||
"disclaimer",
|
||||
"from",
|
||||
"deprecates",
|
||||
"order",
|
||||
] as const) {
|
||||
if (!form.get(field)) {
|
||||
return { error: `Field "${field}" is required.` };
|
||||
}
|
||||
}
|
||||
|
||||
let data: typeof tierlistVersions.$inferInsert;
|
||||
|
||||
try {
|
||||
data = {
|
||||
id: form.get("id")!,
|
||||
name: form.get("name")!,
|
||||
image: form.get("image")!,
|
||||
disclaimer: form.get("disclaimer")!,
|
||||
from: form.get("from")!,
|
||||
deprecates: form.get("deprecates")!,
|
||||
order: parseInt(form.get("order")!, 10),
|
||||
type: typeId,
|
||||
};
|
||||
await db
|
||||
.update(tierlistVersions)
|
||||
.set(data)
|
||||
.where(eq(tierlistVersions.id, verId));
|
||||
} 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 create version.",
|
||||
};
|
||||
}
|
||||
|
||||
await actionLog(`Updated tierlist version ${typeId}/${data.id}`, data);
|
||||
|
||||
revalidatePath("/admin/tl/ver");
|
||||
return { toast: "Version saved successfully.", close: true };
|
||||
}
|
||||
|
||||
async function deleteVersion() {
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
await db.delete(tierlistVersions).where(eq(tierlistVersions.id, verId));
|
||||
|
||||
await actionLog(`Deleted tierlist version ${verId}`, type);
|
||||
|
||||
revalidatePath("/admin/char");
|
||||
return { toast: "Version deleted.", close: true };
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalBase title={`Create ${type.name} Tierlist`}>
|
||||
<FormProvider id={`tl-${typeId}-create`} onSubmit={submit} values={ver}>
|
||||
<FormInput name="prefix">
|
||||
<input defaultValue={typeId} className="hidden" />
|
||||
</FormInput>
|
||||
<FormRow>
|
||||
<FormInput name="name" label="Name">
|
||||
<Input placeholder="6.3a" autoFocus />
|
||||
</FormInput>
|
||||
<FormInput name="id" label="ID" subLabel="change = break">
|
||||
<Input placeholder="63a" disabled />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormInput name="order" label="Order">
|
||||
<Input placeholder="1" />
|
||||
</FormInput>
|
||||
<FormInput name="deprecates" label="Deprecation Date">
|
||||
<DatePicker />
|
||||
</FormInput>
|
||||
<FormRow>
|
||||
<FormInput name="from" label="Version">
|
||||
<Select>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{versionList.map((version) => (
|
||||
<SelectItem key={version.id} value={version.id}>
|
||||
{version.name}{" "}
|
||||
<KbdGroup>
|
||||
<Kbd>{version.id}</Kbd>
|
||||
{version.from && (
|
||||
<Kbd>
|
||||
<ArrowRight />
|
||||
{version.from}
|
||||
</Kbd>
|
||||
)}
|
||||
</KbdGroup>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormInput>
|
||||
<Button asChild size="icon">
|
||||
<Link href="/admin/ver/create">
|
||||
<PlusIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<FormInput name="image" label="Image" subLabel="optional">
|
||||
<CdnChooser />
|
||||
</FormInput>
|
||||
<FormInput name="disclaimer" label="Disclaimer" subLabel="optional">
|
||||
<CdnChooser />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<DialogFooter>
|
||||
<Button asChild variant="destructive">
|
||||
<FormAction type="action" action={deleteVersion}>
|
||||
<Trash2 />
|
||||
Delete
|
||||
</FormAction>
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button asChild>
|
||||
<FormAction
|
||||
type="submit"
|
||||
loading={
|
||||
<>
|
||||
<Spinner />
|
||||
Creating...
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
Create
|
||||
</FormAction>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</FormProvider>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export default async function TlVersionCreatePage({
|
||||
}: PageProps<"/admin/tl/ver/[type]/create">) {
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
const { type: typeId } = await params;
|
||||
const [[{ maxOrder }], [{ disclaimer }], [type], versionList] =
|
||||
const [[{ maxOrder }], [{ disclaimer } = {}], [type], versionList] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select({ maxOrder: sql<number>`MAX(${tierlistVersions.order})` })
|
||||
@@ -72,7 +72,15 @@ export default async function TlVersionCreatePage({
|
||||
"use server";
|
||||
if (!(await adminCheck())) redirect("/login");
|
||||
|
||||
for (const field of ["name", "id", "order"] as const) {
|
||||
for (const field of [
|
||||
"id",
|
||||
"name",
|
||||
"image",
|
||||
"disclaimer",
|
||||
"from",
|
||||
"deprecates",
|
||||
"order",
|
||||
] as const) {
|
||||
if (!form.get(field)) {
|
||||
return { error: `Field "${field}" is required.` };
|
||||
}
|
||||
@@ -97,7 +105,9 @@ export default async function TlVersionCreatePage({
|
||||
const err = e as Error & { cause: { detail: string; message: string } };
|
||||
return {
|
||||
error:
|
||||
err?.cause?.detail || err?.cause?.message || "Failed to create type.",
|
||||
err?.cause?.detail ||
|
||||
err?.cause?.message ||
|
||||
"Failed to create version.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@ export function SidebarLink({
|
||||
className,
|
||||
children,
|
||||
href,
|
||||
disabled,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Link>) {
|
||||
}: React.ComponentProps<typeof Link> & { disabled?: boolean }) {
|
||||
const target = useMemo(
|
||||
() =>
|
||||
href
|
||||
@@ -60,12 +61,18 @@ export function SidebarLink({
|
||||
);
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Link href={href} {...props}>
|
||||
<SidebarMenuButton
|
||||
className={cn(target === pathname ? "bg-accent" : "", className)}
|
||||
>
|
||||
{children}
|
||||
</SidebarMenuButton>
|
||||
</Link>
|
||||
<SidebarMenuButton
|
||||
disabled={disabled}
|
||||
className={cn(target === pathname ? "bg-accent" : "", className)}
|
||||
asChild={!disabled}
|
||||
>
|
||||
{disabled ? (
|
||||
children
|
||||
) : (
|
||||
<Link href={href} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,19 +117,19 @@ export default async function AdminLayout({
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarLink href="/admin/tl/ver">
|
||||
<SidebarLink href="/admin/tl/ver" disabled>
|
||||
<GitGraph />
|
||||
Versions
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarLink href="/admin/tl/badges">
|
||||
<SidebarLink href="/admin/tl/badges" disabled>
|
||||
<Badge />
|
||||
Badges
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarLink href="/admin/tl/layout">
|
||||
<SidebarLink href="/admin/tl/layout" disabled>
|
||||
<Columns3Cog />
|
||||
Layout
|
||||
</SidebarLink>
|
||||
@@ -170,7 +170,7 @@ export default async function AdminLayout({
|
||||
<SidebarMenu>
|
||||
{vers.map((v) => (
|
||||
<SidebarMenuItem key={v.id}>
|
||||
<SidebarLink href={`/admin/ver/${v.id}`}>
|
||||
<SidebarLink href={`/admin/ver/${v.id}`} disabled>
|
||||
{v.name}
|
||||
</SidebarLink>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -329,7 +329,7 @@ export async function calcPrice(service: string[]) {
|
||||
) - (types.every((s) => service.includes(s.id)) ? allDiscount : 0);
|
||||
}
|
||||
|
||||
async function removeExpiredSubmissions() {
|
||||
export async function removeExpiredSubmissions() {
|
||||
// First, get count of items to be removed
|
||||
const expiredCond = and(
|
||||
not(endgameSubmissions.paid),
|
||||
|
||||
Reference in New Issue
Block a user