refactor: consolidate form client components and standardize layout modularity

This commit is contained in:
2026-05-28 17:56:03 +07:00 Unverified
parent 49e7690b7b
commit 9879fd8ee8
22 changed files with 844 additions and 288 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ export async function createForm(formData: FormData) {
redirect(`/form/admin/${form.id}/edit`);
}
export async function updateForm(id: string, data: { title: string; description?: string }) {
export async function updateForm(id: string, data: { title: string; description?: string; isOpen?: boolean }) {
const session = await getServerSession(authOptions);
const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const discordId = (session?.user as { discordId?: string } | undefined)?.discordId;
+6
View File
@@ -17,6 +17,12 @@ export async function submitForm(
const discordId = (session.user as { discordId?: string }).discordId;
if (!discordId) throw new Error("No Discord ID found");
const form = await db.query.forms.findFirst({
where: (f, { eq }) => eq(f.id, formId),
});
if (!form) throw new Error("Form not found");
if (!form.isOpen) throw new Error("This form is disabled for now");
const existingSubmission = await db.query.submissions.findFirst({
where: (s, { eq, and }) =>
and(eq(s.formId, formId), eq(s.userDiscordId, discordId)),
@@ -37,6 +37,7 @@ interface FormEditorClientProps {
id: string;
title: string;
description: string | null;
isOpen: boolean;
};
initialQuestions: Question[];
}
@@ -45,6 +46,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
const router = useRouter();
const [formTitle, setFormTitle] = useState(form.title);
const [formDescription, setFormDescription] = useState(form.description || "");
const [formIsOpen, setFormIsOpen] = useState(form.isOpen);
const [questions, setQuestions] = useState<Question[]>(initialQuestions);
const [uploadingId, setUploadingId] = useState<string | null>(null);
const [isFormSaving, startFormSaveTransition] = useTransition();
@@ -60,6 +62,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
await updateForm(form.id, {
title: formTitle,
description: formDescription,
isOpen: formIsOpen,
});
toast.success("Form metadata saved!");
router.refresh();
@@ -210,6 +213,16 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
className="min-h-20"
/>
</div>
<div className="flex items-center space-x-2 pt-2">
<Switch
id="form-is-open"
checked={formIsOpen}
onCheckedChange={setFormIsOpen}
/>
<Label htmlFor="form-is-open" className="text-sm font-normal text-muted-foreground">
Accepting Submissions
</Label>
</div>
<div className="flex justify-end pt-2">
<Button onClick={saveFormMetadata} disabled={isFormSaving} className="gap-2">
{isFormSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
+3 -2
View File
@@ -1,5 +1,6 @@
import { SidebarProvider } from "@/components/ui/sidebar";
import { AdminSidebar } from "./admin-sidebar";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AdminSidebar } from "@/components/admin-sidebar";
import { Toaster } from "@/components/ui/sonner";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
+1 -1
View File
@@ -1,6 +1,6 @@
import { db } from "@/db";
import { forms } from "@/db/schema";
import FormEditorClient from "./form-editor-client";
import FormEditorClient from "./client";
export const dynamic = "force-dynamic";
+2 -2
View File
@@ -3,8 +3,8 @@ import { notFound } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import SubmissionDetailClient from "./submission-detail-client";
import { DeleteSubmissionButton } from "../delete-buttons-client";
import SubmissionDetailClient from "./client";
import { DeleteSubmissionButton } from "../client";
interface SubmissionDetailPageProps {
params: Promise<{
+1 -1
View File
@@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Eye, Users } from "lucide-react";
import { DeleteSubmissionButton, DeleteAllSubmissionsButton } from "./delete-buttons-client";
import { DeleteSubmissionButton, DeleteAllSubmissionsButton } from "./client";
export const dynamic = "force-dynamic";
+288
View File
@@ -0,0 +1,288 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { submitForm } from "@/app/actions/submissions";
import { Loader2, LogOut } from "lucide-react";
import { signOut } from "next-auth/react";
import Image from "next/image";
export function FormLayoutClient({
userName,
userImage,
}: {
userName: string;
userImage: string;
}) {
return (
<div className="flex items-center gap-3">
{userImage && (
<Image
src={userImage}
alt={userName}
width={28}
height={28}
className="rounded-full"
/>
)}
<span className="text-sm text-muted-foreground">{userName}</span>
<Button
variant="ghost"
size="sm"
onClick={() => signOut({ callbackUrl: "/auth/signin" })}
>
<LogOut className="w-4 h-4" />
</Button>
</div>
);
}
interface Question {
id: string;
formId: string;
type: "text" | "textarea" | "radio" | "checkbox";
label: string;
imageUrl: string | null;
displayOrder: number;
required: boolean;
options: string[] | null;
}
interface FormFillerClientProps {
form: {
id: string;
title: string;
description: string | null;
isOpen: boolean;
};
questions: Question[];
initialAnswers?: Record<string, string>;
}
export function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) {
const router = useRouter();
// Pre-process checkbox answers since they are stored as comma-separated strings
const initialCheckboxes: Record<string, string[]> = {};
for (const q of questions) {
if (q.type === "checkbox" && initialAnswers[q.id]) {
initialCheckboxes[q.id] = initialAnswers[q.id].split(", ");
}
}
const [answers, setAnswers] = useState<Record<string, string>>(initialAnswers);
const [checkboxAnswers, setCheckboxAnswers] = useState<Record<string, string[]>>(initialCheckboxes);
const [submitting, setSubmitting] = useState(false);
const isUpdating = Object.keys(initialAnswers).length > 0;
const handleTextChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleRadioChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleCheckboxChange = (questionId: string, option: string, checked: boolean) => {
setCheckboxAnswers((prev) => {
const current = prev[questionId] || [];
const updated = checked
? [...current, option]
: current.filter((item) => item !== option);
return { ...prev, [questionId]: updated };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.isOpen) return;
// Validate required questions
for (const q of questions) {
if (q.required) {
if (q.type === "checkbox") {
const selected = checkboxAnswers[q.id] || [];
if (selected.length === 0) {
toast.error(`"${q.label}" is required.`);
return;
}
} else {
const ans = answers[q.id] || "";
if (!ans.trim()) {
toast.error(`"${q.label}" is required.`);
return;
}
}
}
}
setSubmitting(true);
try {
const formattedAnswers = questions.map((q) => {
let value = "";
if (q.type === "checkbox") {
value = (checkboxAnswers[q.id] || []).join(", ");
} else {
value = answers[q.id] || "";
}
return {
questionId: q.id,
value,
};
});
const res = await submitForm(form.id, formattedAnswers);
if (res.ok) {
toast.success(isUpdating ? "Form updated successfully!" : "Form submitted successfully!");
if (!isUpdating) {
setAnswers({});
setCheckboxAnswers({});
}
}
} catch (err: any) {
console.error(err);
toast.error(err.message || "Failed to submit form.");
} finally {
setSubmitting(false);
}
};
return (
<div className="relative">
{!form.isOpen && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/50 backdrop-blur-sm rounded-xl">
<div className="bg-background border shadow-lg p-6 rounded-lg">
<h2 className="text-xl font-bold text-center">This form disable for now</h2>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<Card className="border-t-8 border-t-primary shadow-md">
<CardHeader>
<CardTitle className="text-3xl font-bold">{form.title}</CardTitle>
{form.description && (
<CardDescription className="text-base mt-2 whitespace-pre-wrap">
{form.description}
</CardDescription>
)}
</CardHeader>
</Card>
<div className="space-y-4">
{questions.map((q) => (
<Card key={q.id} className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<Label className="text-base font-semibold leading-normal">
{q.label}
{q.required && <span className="text-destructive ml-1">*</span>}
</Label>
</div>
{q.imageUrl && (
<div className="mt-3 relative max-w-full h-auto max-h-80 overflow-hidden rounded-md border">
<img
src={q.imageUrl}
alt={q.label}
className="object-contain max-h-80 w-auto"
/>
</div>
)}
</CardHeader>
<CardContent>
{q.type === "text" && (
<Input
type="text"
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full"
disabled={!form.isOpen}
/>
)}
{q.type === "textarea" && (
<Textarea
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full min-h-24 resize-y"
disabled={!form.isOpen}
/>
)}
{q.type === "radio" && q.options && (
<RadioGroup
required={q.required}
value={answers[q.id] || ""}
onValueChange={(val) => handleRadioChange(q.id, val)}
className="space-y-2"
disabled={!form.isOpen}
>
{q.options.map((opt, idx) => (
<div key={idx} className="flex items-center space-x-2">
<RadioGroupItem value={opt} id={`${q.id}-${idx}`} disabled={!form.isOpen} />
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
))}
</RadioGroup>
)}
{q.type === "checkbox" && q.options && (
<div className="space-y-2">
{q.options.map((opt, idx) => {
const isChecked = (checkboxAnswers[q.id] || []).includes(opt);
return (
<div key={idx} className="flex items-center space-x-2">
<Checkbox
id={`${q.id}-${idx}`}
checked={isChecked}
onCheckedChange={(checked) =>
handleCheckboxChange(q.id, opt, !!checked)
}
disabled={!form.isOpen}
/>
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
))}
</div>
<div className="flex justify-end items-center pt-4">
<Button type="submit" disabled={submitting || !form.isOpen} className="min-w-28">
{submitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting
</>
) : isUpdating ? (
"Update Submission"
) : (
"Submit"
)}
</Button>
</div>
</form>
</div>
);
}
-241
View File
@@ -1,241 +0,0 @@
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { submitForm } from "@/app/actions/submissions";
import { Loader2 } from "lucide-react";
interface Question {
id: string;
formId: string;
type: "text" | "textarea" | "radio" | "checkbox";
label: string;
imageUrl: string | null;
displayOrder: number;
required: boolean;
options: string[] | null;
}
interface FormFillerClientProps {
form: {
id: string;
title: string;
description: string | null;
};
questions: Question[];
initialAnswers?: Record<string, string>;
}
export default function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) {
const router = useRouter();
// Pre-process checkbox answers since they are stored as comma-separated strings
const initialCheckboxes: Record<string, string[]> = {};
for (const q of questions) {
if (q.type === "checkbox" && initialAnswers[q.id]) {
initialCheckboxes[q.id] = initialAnswers[q.id].split(", ");
}
}
const [answers, setAnswers] = useState<Record<string, string>>(initialAnswers);
const [checkboxAnswers, setCheckboxAnswers] = useState<Record<string, string[]>>(initialCheckboxes);
const [submitting, setSubmitting] = useState(false);
const isUpdating = Object.keys(initialAnswers).length > 0;
const handleTextChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleRadioChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleCheckboxChange = (questionId: string, option: string, checked: boolean) => {
setCheckboxAnswers((prev) => {
const current = prev[questionId] || [];
const updated = checked
? [...current, option]
: current.filter((item) => item !== option);
return { ...prev, [questionId]: updated };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required questions
for (const q of questions) {
if (q.required) {
if (q.type === "checkbox") {
const selected = checkboxAnswers[q.id] || [];
if (selected.length === 0) {
toast.error(`"${q.label}" is required.`);
return;
}
} else {
const ans = answers[q.id] || "";
if (!ans.trim()) {
toast.error(`"${q.label}" is required.`);
return;
}
}
}
}
setSubmitting(true);
try {
const formattedAnswers = questions.map((q) => {
let value = "";
if (q.type === "checkbox") {
value = (checkboxAnswers[q.id] || []).join(", ");
} else {
value = answers[q.id] || "";
}
return {
questionId: q.id,
value,
};
});
const res = await submitForm(form.id, formattedAnswers);
if (res.ok) {
toast.success(isUpdating ? "Form updated successfully!" : "Form submitted successfully!");
// We don't reset answers if updating, so they can see their current state
if (!isUpdating) {
setAnswers({});
setCheckboxAnswers({});
}
}
} catch (err: any) {
console.error(err);
toast.error(err.message || "Failed to submit form.");
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<Card className="border-t-8 border-t-primary shadow-md">
<CardHeader>
<CardTitle className="text-3xl font-bold">{form.title}</CardTitle>
{form.description && (
<CardDescription className="text-base mt-2 whitespace-pre-wrap">
{form.description}
</CardDescription>
)}
</CardHeader>
</Card>
<div className="space-y-4">
{questions.map((q) => (
<Card key={q.id} className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<Label className="text-base font-semibold leading-normal">
{q.label}
{q.required && <span className="text-destructive ml-1">*</span>}
</Label>
</div>
{q.imageUrl && (
<div className="mt-3 relative max-w-full h-auto max-h-80 overflow-hidden rounded-md border">
<img
src={q.imageUrl}
alt={q.label}
className="object-contain max-h-80 w-auto"
/>
</div>
)}
</CardHeader>
<CardContent>
{q.type === "text" && (
<Input
type="text"
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full"
/>
)}
{q.type === "textarea" && (
<Textarea
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full min-h-24 resize-y"
/>
)}
{q.type === "radio" && q.options && (
<RadioGroup
required={q.required}
value={answers[q.id] || ""}
onValueChange={(val) => handleRadioChange(q.id, val)}
className="space-y-2"
>
{q.options.map((opt, idx) => (
<div key={idx} className="flex items-center space-x-2">
<RadioGroupItem value={opt} id={`${q.id}-${idx}`} />
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
))}
</RadioGroup>
)}
{q.type === "checkbox" && q.options && (
<div className="space-y-2">
{q.options.map((opt, idx) => {
const isChecked = (checkboxAnswers[q.id] || []).includes(opt);
return (
<div key={idx} className="flex items-center space-x-2">
<Checkbox
id={`${q.id}-${idx}`}
checked={isChecked}
onCheckedChange={(checked) =>
handleCheckboxChange(q.id, opt, !!checked)
}
/>
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
))}
</div>
<div className="flex justify-end items-center pt-4">
<Button type="submit" disabled={submitting} className="min-w-28">
{submitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting
</>
) : isUpdating ? (
"Update Submission"
) : (
"Submit"
)}
</Button>
</div>
</form>
);
}
-36
View File
@@ -1,36 +0,0 @@
"use client";
import { signOut } from "next-auth/react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { LogOut } from "lucide-react";
export default function FormLayoutClient({
userName,
userImage,
}: {
userName: string;
userImage: string;
}) {
return (
<div className="flex items-center gap-3">
{userImage && (
<Image
src={userImage}
alt={userName}
width={28}
height={28}
className="rounded-full"
/>
)}
<span className="text-sm text-muted-foreground">{userName}</span>
<Button
variant="ghost"
size="sm"
onClick={() => signOut({ callbackUrl: "/auth/signin" })}
>
<LogOut className="w-4 h-4" />
</Button>
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { Toaster } from "@/components/ui/sonner";
import FormLayoutClient from "./layout-client";
import LoginDialog from "./login-dialog";
import { FormLayoutClient } from "./client";
import LoginDialog from "@/components/login-dialog";
export default async function FormLayout({
children,
+1 -1
View File
@@ -1,6 +1,6 @@
import { db } from "@/db";
import { forms } from "@/db/schema";
import FormFillerClient from "./form-filler-client";
import { FormFillerClient } from "./client";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+1
View File
@@ -71,6 +71,7 @@ export const forms = pgTable("form", {
title: text("title").notNull(),
description: text("description"),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
isOpen: boolean("is_open").notNull().default(true),
});
export type QuestionType = "text" | "textarea" | "radio" | "checkbox";
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "form" ADD COLUMN "is_open" boolean DEFAULT true NOT NULL;
+515
View File
@@ -0,0 +1,515 @@
{
"id": "1d99ab40-4e60-4255-9948-e4a4b54375eb",
"prevId": "d9863f2d-5c3b-4d3a-8759-4170d72381a3",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_providerAccountId_pk": {
"name": "account_provider_providerAccountId_pk",
"columns": [
"provider",
"providerAccountId"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.answer": {
"name": "answer",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"submission_id": {
"name": "submission_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"question_id": {
"name": "question_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {
"answer_submission_id_submission_id_fk": {
"name": "answer_submission_id_submission_id_fk",
"tableFrom": "answer",
"tableTo": "submission",
"columnsFrom": [
"submission_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"answer_question_id_question_id_fk": {
"name": "answer_question_id_question_id_fk",
"tableFrom": "answer",
"tableTo": "question",
"columnsFrom": [
"question_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.form": {
"name": "form",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"is_open": {
"name": "is_open",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.question": {
"name": "question",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"form_id": {
"name": "form_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"label": {
"name": "label",
"type": "text",
"primaryKey": false,
"notNull": true
},
"image_url": {
"name": "image_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_order": {
"name": "display_order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"required": {
"name": "required",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"options": {
"name": "options",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
}
},
"indexes": {},
"foreignKeys": {
"question_form_id_form_id_fk": {
"name": "question_form_id_form_id_fk",
"tableFrom": "question",
"tableTo": "form",
"columnsFrom": [
"form_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.submission": {
"name": "submission",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"form_id": {
"name": "form_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_discord_id": {
"name": "user_discord_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_name": {
"name": "user_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"submitted_at": {
"name": "submitted_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"edit_history": {
"name": "edit_history",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'[]'::jsonb"
}
},
"indexes": {},
"foreignKeys": {
"submission_form_id_form_id_fk": {
"name": "submission_form_id_form_id_fk",
"tableFrom": "submission",
"tableTo": "form",
"columnsFrom": [
"form_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false
},
"emailVerified": {
"name": "emailVerified",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"discord_id": {
"name": "discord_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verificationToken": {
"name": "verificationToken",
"schema": "",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"name": "verificationToken_identifier_token_pk",
"columns": [
"identifier",
"token"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -15,6 +15,13 @@
"when": 1779964210864,
"tag": "0001_wooden_ravenous",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1779965278875,
"tag": "0002_first_dragon_man",
"breakpoints": true
}
]
}
+2 -1
View File
@@ -10,7 +10,8 @@
"up": "docker compose up -d --build",
"logs": "docker compose logs -f",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
"db:migrate": "drizzle-kit migrate",
"db": "drizzle-kit"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.11.2",