refactor: migrate administrative form management to a centralized modular routing structure
This commit is contained in:
@@ -145,12 +145,12 @@ export async function deleteSubmission(id: string) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function deleteAllSubmissions() {
|
||||
export async function deleteAllSubmissions(formId: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) throw new Error("Not authenticated");
|
||||
|
||||
await db.delete(submissions);
|
||||
revalidatePath("/form/admin/result");
|
||||
await db.delete(submissions).where(eq(submissions.formId, formId));
|
||||
revalidatePath(`/admin/form/${formId}/result`);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -609,14 +609,14 @@ function AnswerPreview({ question }: { question: Question }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminSidebar() {
|
||||
export function AdminSidebar({ formId }: { formId: string }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Sidebar variant="sidebar" collapsible="icon">
|
||||
<SidebarHeader className="border-b border-border py-4 px-4 flex items-center justify-between">
|
||||
<span className="font-bold tracking-wider text-primary text-sm uppercase">
|
||||
Form admin
|
||||
Form Admin
|
||||
</span>
|
||||
</SidebarHeader>
|
||||
|
||||
@@ -626,26 +626,26 @@ export function AdminSidebar() {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={pathname === "/form/admin"}>
|
||||
<Link href="/form/admin">
|
||||
<SidebarMenuButton asChild isActive={pathname === `/admin/form/${formId}`}>
|
||||
<Link href={`/admin/form/${formId}`}>
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
<span>Dashboard</span>
|
||||
<span>Editor</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/result")}>
|
||||
<Link href="/form/admin/result">
|
||||
<SidebarMenuButton asChild isActive={pathname.startsWith(`/admin/form/${formId}/result`)}>
|
||||
<Link href={`/admin/form/${formId}/result`}>
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
<span>Response</span>
|
||||
<span>Responses</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/extra")}>
|
||||
<Link href="/form/admin/extra">
|
||||
<SidebarMenuButton asChild isActive={pathname.startsWith(`/admin/form/${formId}/extra`)}>
|
||||
<Link href={`/admin/form/${formId}/extra`}>
|
||||
<Webhook className="w-4 h-4" />
|
||||
<span>Extra</span>
|
||||
</Link>
|
||||
@@ -660,9 +660,9 @@ export function AdminSidebar() {
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/form">
|
||||
<Link href="/admin/form">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Go public</span>
|
||||
<span>All Forms</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -1,20 +1,21 @@
|
||||
import { db } from "@/db";
|
||||
import { forms } from "@/db/schema";
|
||||
import { notFound } from "next/navigation";
|
||||
import FormExtraClient from "./client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminExtraPage() {
|
||||
let form = await db.query.forms.findFirst();
|
||||
export default async function AdminExtraPage(
|
||||
props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const form = await db.query.forms.findFirst({
|
||||
where: (f, { eq }) => eq(f.id, params.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
const [newForm] = await db
|
||||
.insert(forms)
|
||||
.values({
|
||||
title: "Erika Custom Form",
|
||||
description: "Welcome! Please fill out the form below.",
|
||||
})
|
||||
.returning();
|
||||
form = newForm;
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -12,11 +12,13 @@ export const metadata: Metadata = {
|
||||
},
|
||||
}
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default async function AdminFormLayout(
|
||||
props: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "")
|
||||
.split(",")
|
||||
@@ -31,9 +33,9 @@ export default async function AdminLayout({
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex w-full bg-background text-foreground dark">
|
||||
<AdminSidebar />
|
||||
<AdminSidebar formId={params.id} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{children}
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
@@ -1,20 +1,22 @@
|
||||
import { db } from "@/db";
|
||||
import { forms } from "@/db/schema";
|
||||
import FormEditorClient from "./client";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminDashboardPage() {
|
||||
let form = await db.query.forms.findFirst();
|
||||
export default async function AdminDashboardPage(
|
||||
props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const form = await db.query.forms.findFirst({
|
||||
where: (f, { eq }) => eq(f.id, params.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
const [newForm] = await db
|
||||
.insert(forms)
|
||||
.values({
|
||||
title: "Erika Custom Form",
|
||||
description: "Welcome! Please fill out the form below.",
|
||||
})
|
||||
.returning();
|
||||
form = newForm;
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
@@ -26,7 +28,7 @@ export default async function AdminDashboardPage() {
|
||||
<div className="space-y-6 max-w-4xl mx-auto pb-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Admin Dashboard</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Edit Form</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Customize form settings, add/edit questions, or upload images.
|
||||
</p>
|
||||
+12
-10
@@ -9,14 +9,15 @@ import { DeleteSubmissionButton } from "@/components/delete-buttons";
|
||||
interface SubmissionDetailPageProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
submissionId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function SubmissionDetailPage({ params }: SubmissionDetailPageProps) {
|
||||
const { id } = await params;
|
||||
const { id: formId, submissionId } = await params;
|
||||
|
||||
const submission = await db.query.submissions.findFirst({
|
||||
where: (s, { eq }) => eq(s.id, id),
|
||||
where: (s, { eq }) => eq(s.id, submissionId),
|
||||
});
|
||||
|
||||
if (!submission) {
|
||||
@@ -37,18 +38,19 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
answersMap[ans.questionId] = ans.value;
|
||||
}
|
||||
const submissionsList = await db.query.submissions.findMany({
|
||||
where: (s, { eq }) => eq(s.formId, formId),
|
||||
orderBy: (s, { desc }) => [desc(s.submittedAt)],
|
||||
});
|
||||
|
||||
const currentIndex = submissionsList.findIndex((s) => s.id === id);
|
||||
const currentIndex = submissionsList.findIndex((s) => s.id === submissionId);
|
||||
const prevId = currentIndex > 0 ? submissionsList[currentIndex - 1].id : null;
|
||||
const nextId = currentIndex >= 0 && currentIndex < submissionsList.length - 1 ? submissionsList[currentIndex + 1].id : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full pb-10">
|
||||
<div className="space-y-6 w-full max-w-4xl mx-auto p-4 md:p-6 pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/form/admin/result">
|
||||
<Link href={`/admin/form/${formId}/result`}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -57,16 +59,16 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{prevId && (
|
||||
<Link href={`/form/admin/result/${prevId}`}>
|
||||
<Link href={`/admin/form/${formId}/result/${prevId}`}>
|
||||
<Button variant="outline" size="sm">Previous</Button>
|
||||
</Link>
|
||||
)}
|
||||
{nextId && (
|
||||
<Link href={`/form/admin/result/${nextId}`}>
|
||||
<Link href={`/admin/form/${formId}/result/${nextId}`}>
|
||||
<Button variant="outline" size="sm">Next</Button>
|
||||
</Link>
|
||||
)}
|
||||
<DeleteSubmissionButton id={submission.id} redirectUrl="/form/admin/result" />
|
||||
<DeleteSubmissionButton id={submission.id} redirectUrl={`/admin/form/${formId}/result`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +80,7 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
|
||||
<div className="flex justify-between items-center mt-8 pt-6 border-t">
|
||||
{prevId ? (
|
||||
<Link href={`/form/admin/result/${prevId}`}>
|
||||
<Link href={`/admin/form/${formId}/result/${prevId}`}>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Previous
|
||||
</Button>
|
||||
@@ -87,7 +89,7 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
<div />
|
||||
)}
|
||||
{nextId && (
|
||||
<Link href={`/form/admin/result/${nextId}`}>
|
||||
<Link href={`/admin/form/${formId}/result/${nextId}`}>
|
||||
<Button variant="outline">
|
||||
Next <ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
@@ -3,18 +3,32 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { Users } from "lucide-react";
|
||||
import { Users, ArrowLeft } from "lucide-react";
|
||||
import { DeleteSubmissionButton, DeleteAllSubmissionsButton } from "@/components/delete-buttons";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ResultsListPage() {
|
||||
export default async function ResultsListPage(
|
||||
props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const submissionsList = await db.query.submissions.findMany({
|
||||
where: (s, { eq }) => eq(s.formId, params.id),
|
||||
orderBy: (s, { desc }) => [desc(s.submittedAt)],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="space-y-6 w-full max-w-4xl mx-auto p-4 md:p-6">
|
||||
<div>
|
||||
<Button variant="ghost" asChild className="mb-4">
|
||||
<Link href={`/admin/form/${params.id}`}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Form Editor
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Form Submissions</h1>
|
||||
@@ -22,7 +36,7 @@ export default async function ResultsListPage() {
|
||||
Review all submissions and individual user answers.
|
||||
</p>
|
||||
</div>
|
||||
{submissionsList.length > 0 && <DeleteAllSubmissionsButton />}
|
||||
{submissionsList.length > 0 && <DeleteAllSubmissionsButton formId={params.id} />}
|
||||
</div>
|
||||
|
||||
{submissionsList.length === 0 ? (
|
||||
@@ -60,13 +74,13 @@ export default async function ResultsListPage() {
|
||||
return (
|
||||
<TableRow key={sub.id} className="cursor-pointer group">
|
||||
<TableCell className="p-0">
|
||||
<Link href={`/form/admin/result/${sub.id}`} className="flex items-center gap-2 p-4 w-full h-full font-medium">
|
||||
<Link href={`/admin/form/${params.id}/result/${sub.id}`} className="flex items-center gap-2 p-4 w-full h-full font-medium">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{userLabel}</span>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-muted-foreground">
|
||||
<Link href={`/form/admin/result/${sub.id}`} className="flex items-center p-4 w-full h-full">
|
||||
<Link href={`/admin/form/${params.id}/result/${sub.id}`} className="flex items-center p-4 w-full h-full">
|
||||
{new Date(sub.submittedAt).toLocaleString()}
|
||||
</Link>
|
||||
</TableCell>
|
||||
@@ -0,0 +1,95 @@
|
||||
import { db } from "@/db";
|
||||
import { forms } from "@/db/schema";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminFormsPage() {
|
||||
const formsList = await db.query.forms.findMany({
|
||||
orderBy: (f, { desc }) => [desc(f.createdAt)],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto p-4 md:p-6 pb-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Forms</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage your custom forms and view submissions.
|
||||
</p>
|
||||
</div>
|
||||
<form action={async () => {
|
||||
"use server";
|
||||
const [newForm] = await db.insert(forms).values({
|
||||
title: "New Form",
|
||||
description: "",
|
||||
}).returning();
|
||||
redirect(`/admin/form/${newForm.id}`);
|
||||
}}>
|
||||
<Button type="submit">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Form
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{formsList.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="h-24 text-center text-muted-foreground">
|
||||
No forms found. Create one to get started.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
formsList.map((form) => (
|
||||
<TableRow key={form.id}>
|
||||
<TableCell className="font-medium">{form.title}</TableCell>
|
||||
<TableCell>
|
||||
{form.isOpen ? (
|
||||
<Badge variant="default">Open</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Closed</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{form.createdAt ? new Date(form.createdAt).toLocaleDateString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right space-x-2">
|
||||
<Button variant="outline" asChild size="sm">
|
||||
<Link href={`/admin/form/${form.id}`}>Edit</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild size="sm">
|
||||
<Link href={`/admin/form/${form.id}/result`}>Results</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { db } from "@/db";
|
||||
import { FormFillerClient } from "../client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function FormsSubmitPage(
|
||||
props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
) {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const discordId = (session?.user as { discordId?: string })?.discordId;
|
||||
|
||||
const form = await db.query.forms.findFirst({
|
||||
where: (f, { eq }) => eq(f.id, params.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, form.id),
|
||||
orderBy: (q, { asc }) => [asc(q.displayOrder)],
|
||||
});
|
||||
|
||||
let initialAnswers: Record<string, string> = {};
|
||||
if (discordId) {
|
||||
const existingSubmission = await db.query.submissions.findFirst({
|
||||
where: (s, { eq, and }) =>
|
||||
and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)),
|
||||
});
|
||||
|
||||
if (existingSubmission) {
|
||||
const answersList = await db.query.answers.findMany({
|
||||
where: (a, { eq }) => eq(a.submissionId, existingSubmission.id),
|
||||
});
|
||||
for (const a of answersList) {
|
||||
initialAnswers[a.questionId] = a.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<FormFillerClient
|
||||
form={form}
|
||||
questions={questionsList}
|
||||
initialAnswers={initialAnswers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-2
@@ -40,15 +40,19 @@ export function FormLayoutClient({
|
||||
userName: string;
|
||||
userImage: string;
|
||||
}) {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{userImage && (
|
||||
{userImage && !imgError && (
|
||||
<Image
|
||||
src={userImage}
|
||||
alt={userName}
|
||||
width={28}
|
||||
height={28}
|
||||
className="rounded-full "
|
||||
className="rounded-full object-cover"
|
||||
onError={() => setImgError(true)}
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">{userName}</span>
|
||||
|
||||
+33
-47
@@ -1,57 +1,43 @@
|
||||
import { db } from "@/db";
|
||||
import { forms } from "@/db/schema";
|
||||
import { FormFillerClient } from "./client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function FormsSubmitPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
const discordId = (session?.user as { discordId?: string })?.discordId;
|
||||
|
||||
// Automatically retrieve the first form, or create a default one
|
||||
let form = await db.query.forms.findFirst();
|
||||
if (!form) {
|
||||
const [newForm] = await db
|
||||
.insert(forms)
|
||||
.values({
|
||||
title: "Erika Custom Form",
|
||||
description: "Welcome! Please fill out the form below.",
|
||||
})
|
||||
.returning();
|
||||
form = newForm;
|
||||
}
|
||||
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, form.id),
|
||||
orderBy: (q, { asc }) => [asc(q.displayOrder)],
|
||||
export default async function FormsListingPage() {
|
||||
const formsList = await db.query.forms.findMany({
|
||||
where: (f, { eq }) => eq(f.isOpen, true),
|
||||
orderBy: (f, { desc }) => [desc(f.createdAt)],
|
||||
});
|
||||
|
||||
let initialAnswers: Record<string, string> = {};
|
||||
if (discordId) {
|
||||
const existingSubmission = await db.query.submissions.findFirst({
|
||||
where: (s, { eq, and }) =>
|
||||
and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)),
|
||||
});
|
||||
|
||||
if (existingSubmission) {
|
||||
const answersList = await db.query.answers.findMany({
|
||||
where: (a, { eq }) => eq(a.submissionId, existingSubmission.id),
|
||||
});
|
||||
for (const a of answersList) {
|
||||
initialAnswers[a.questionId] = a.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<FormFillerClient
|
||||
form={form}
|
||||
questions={questionsList}
|
||||
initialAnswers={initialAnswers}
|
||||
/>
|
||||
<div className="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Available Forms</h1>
|
||||
{formsList.length === 0 ? (
|
||||
<p className="text-muted-foreground">There are currently no open forms available.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{formsList.map((form) => (
|
||||
<Link key={form.id} href={`/form/${form.id}`}>
|
||||
<Card className="hover:bg-muted/50 transition-colors h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>{form.title}</CardTitle>
|
||||
{form.description && (
|
||||
<CardDescription className="line-clamp-3">
|
||||
{form.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export function MapIframe() {
|
||||
return (
|
||||
<iframe
|
||||
src="https://file.gunshiz.top/erika/minecraft/ErikaSMP"
|
||||
className="w-full h-full border-0"
|
||||
title="Minecraft Server View"
|
||||
allowFullScreen
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
const siteUrl = process.env.BASE_URL;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Erika's SMP",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "th_TH",
|
||||
url: siteUrl,
|
||||
siteName: "Erika's SMP",
|
||||
title: "หน้าแรกของ Erika's SMP website",
|
||||
description:
|
||||
"Minecraft server ของช่อง Erika",
|
||||
images: [
|
||||
{
|
||||
url: "/avatar.webp",
|
||||
width: 512,
|
||||
height: 512,
|
||||
alt: "Erika! avatar",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function MinecraftLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { CopyButton } from "@/components/copybutton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { MapIframe } from "./client";
|
||||
import { Kbd } from "@/components/ui/kbd"
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function MinecraftJoinPage() {
|
||||
return (
|
||||
<div className="container max-w-4xl py-10 mx-auto">
|
||||
<div className="fixed inset-0 -z-10">
|
||||
<Image
|
||||
src="/mc-bg.png"
|
||||
alt="bg"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Erika's SMP</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Server Address</p>
|
||||
<p className="font-mono text-lg">erique.sudloh.com</p>
|
||||
</div>
|
||||
<CopyButton text="erique.sudloh.com" iconOnly={false} variant="outline">
|
||||
คัดลอง Ip
|
||||
</CopyButton>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Port</p> <Kbd> ใช้แค่ Bedrock </Kbd>
|
||||
<p className="font-mono text-lg">30652</p>
|
||||
</div>
|
||||
<CopyButton text="30652" iconOnly={false} variant="outline">
|
||||
คัดลอง Port
|
||||
</CopyButton>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>หลังเข้าเซิร์ฟต้องทำยังไง?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-hidden rounded-lg border aspect-video bg-muted">
|
||||
<MapIframe />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user