feat: add allowOther field to questions and update image upload to return data URIs
This commit is contained in:
@@ -37,6 +37,7 @@ export async function createQuestion(
|
|||||||
required: data.required,
|
required: data.required,
|
||||||
displayOrder: data.displayOrder,
|
displayOrder: data.displayOrder,
|
||||||
options: data.options ?? [],
|
options: data.options ?? [],
|
||||||
|
allowOther: false,
|
||||||
});
|
});
|
||||||
revalidatePath("/form");
|
revalidatePath("/form");
|
||||||
revalidatePath("/form/admin");
|
revalidatePath("/form/admin");
|
||||||
@@ -52,6 +53,7 @@ export async function updateQuestion(
|
|||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
options: string[];
|
options: string[];
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
|
allowOther: boolean;
|
||||||
}>
|
}>
|
||||||
) {
|
) {
|
||||||
await assertAdmin();
|
await assertAdmin();
|
||||||
@@ -77,6 +79,7 @@ export async function bulkUpdateQuestions(
|
|||||||
required: boolean;
|
required: boolean;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
options: string[];
|
options: string[];
|
||||||
|
allowOther: boolean;
|
||||||
}[]
|
}[]
|
||||||
) {
|
) {
|
||||||
await assertAdmin();
|
await assertAdmin();
|
||||||
@@ -92,6 +95,7 @@ export async function bulkUpdateQuestions(
|
|||||||
required: u.required,
|
required: u.required,
|
||||||
displayOrder: u.displayOrder,
|
displayOrder: u.displayOrder,
|
||||||
options: u.options ?? [],
|
options: u.options ?? [],
|
||||||
|
allowOther: u.allowOther,
|
||||||
})
|
})
|
||||||
.where(eq(questions.id, u.id))
|
.where(eq(questions.id, u.id))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,14 +36,12 @@ export async function POST(req: Request) {
|
|||||||
return NextResponse.json({ error: "Invalid image extension" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid image extension" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = `${crypto.randomUUID()}.${originalExt}`;
|
|
||||||
const uploadDir = path.join(process.cwd(), "public", "form");
|
|
||||||
|
|
||||||
await mkdir(uploadDir, { recursive: true });
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
await writeFile(path.join(uploadDir, filename), buffer);
|
const base64 = buffer.toString("base64");
|
||||||
|
const mimeType = file.type;
|
||||||
|
const dataUri = `data:${mimeType};base64,${base64}`;
|
||||||
|
|
||||||
return NextResponse.json({ url: `/form/${filename}` });
|
return NextResponse.json({ url: dataUri });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Upload error:", error);
|
console.error("Upload error:", error);
|
||||||
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 });
|
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 });
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface Question {
|
|||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
options: string[] | null;
|
options: string[] | null;
|
||||||
|
allowOther: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormEditorClientProps {
|
interface FormEditorClientProps {
|
||||||
@@ -102,6 +103,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
|||||||
const formattedUpdates = questions.map(q => ({
|
const formattedUpdates = questions.map(q => ({
|
||||||
...q,
|
...q,
|
||||||
options: q.options === null ? [] : q.options,
|
options: q.options === null ? [] : q.options,
|
||||||
|
allowOther: q.allowOther,
|
||||||
}));
|
}));
|
||||||
await bulkUpdateQuestions(form.id, formattedUpdates);
|
await bulkUpdateQuestions(form.id, formattedUpdates);
|
||||||
toast.success("Questions saved!");
|
toast.success("Questions saved!");
|
||||||
@@ -338,6 +340,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
|||||||
height={200}
|
height={200}
|
||||||
src={q.imageUrl}
|
src={q.imageUrl}
|
||||||
alt={q.label}
|
alt={q.label}
|
||||||
|
unoptimized
|
||||||
className="w-32 h-auto rounded-md border object-contain"
|
className="w-32 h-auto rounded-md border object-contain"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -410,15 +413,29 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-border">
|
<div className="flex justify-between items-center pt-2 border-t border-border">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center flex-wrap gap-6">
|
||||||
<Switch
|
<div className="flex items-center gap-2">
|
||||||
id={`required-${q.id}`}
|
<Switch
|
||||||
checked={q.required}
|
id={`required-${q.id}`}
|
||||||
onCheckedChange={(checked) => handleUpdateQuestion(q.id, { required: checked })}
|
checked={q.required}
|
||||||
/>
|
onCheckedChange={(checked) => handleUpdateQuestion(q.id, { required: checked })}
|
||||||
<Label htmlFor={`required-${q.id}`} className="text-sm font-normal text-muted-foreground">
|
/>
|
||||||
Required
|
<Label htmlFor={`required-${q.id}`} className="text-sm font-normal text-muted-foreground">
|
||||||
</Label>
|
Required
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{(q.type === "radio" || q.type === "checkbox") && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id={`allowOther-${q.id}`}
|
||||||
|
checked={q.allowOther}
|
||||||
|
onCheckedChange={(checked) => handleUpdateQuestion(q.id, { allowOther: checked })}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`allowOther-${q.id}`} className="text-sm font-normal text-muted-foreground">
|
||||||
|
Add 'Other' option
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ export default function SubmissionDetailClient({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{q.imageUrl && (
|
{q.imageUrl && (
|
||||||
<div className="relative max-w-full h-auto max-h-60 overflow-hidden rounded-md border inline-block shrink-0">
|
<div className="relative w-24 h-24 overflow-hidden rounded-md border shrink-0">
|
||||||
<Image
|
<Image
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
src={q.imageUrl}
|
src={q.imageUrl}
|
||||||
alt={q.label}
|
alt="Question image"
|
||||||
className="object-contain w-auto"
|
fill
|
||||||
|
unoptimized
|
||||||
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+96
-6
@@ -67,6 +67,7 @@ interface Question {
|
|||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
options: string[] | null;
|
options: string[] | null;
|
||||||
|
allowOther: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormFillerClientProps {
|
interface FormFillerClientProps {
|
||||||
@@ -105,7 +106,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
|
|
||||||
const formSchema = useMemo(() => buildSchema(questions), [questions]);
|
const formSchema = useMemo(() => buildSchema(questions), [questions]);
|
||||||
|
|
||||||
const defaultValues = useMemo(() => {
|
const { defaultValues, initialOtherValues } = useMemo(() => {
|
||||||
const serverAnswers = { ...initialAnswers };
|
const serverAnswers = { ...initialAnswers };
|
||||||
let draft: Record<string, any> = {};
|
let draft: Record<string, any> = {};
|
||||||
try {
|
try {
|
||||||
@@ -116,16 +117,43 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
const merged = { ...draft, ...serverAnswers };
|
const merged = { ...draft, ...serverAnswers };
|
||||||
|
|
||||||
const vals: Record<string, any> = {};
|
const vals: Record<string, any> = {};
|
||||||
|
const others: Record<string, string> = {};
|
||||||
|
|
||||||
questions.forEach(q => {
|
questions.forEach(q => {
|
||||||
|
const ans = merged[q.id];
|
||||||
if (q.type === "checkbox") {
|
if (q.type === "checkbox") {
|
||||||
vals[q.id] = merged[q.id] ? (Array.isArray(merged[q.id]) ? merged[q.id] : merged[q.id].split(", ").filter(Boolean)) : [];
|
let arr = ans ? (Array.isArray(ans) ? ans : ans.split(", ").filter(Boolean)) : [];
|
||||||
|
if (q.allowOther && q.options) {
|
||||||
|
const standardOptions = q.options;
|
||||||
|
const customAnswers = arr.filter((x: string) => !standardOptions.includes(x) && x !== "__OTHER__");
|
||||||
|
if (customAnswers.length > 0) {
|
||||||
|
others[q.id] = customAnswers.join(", ");
|
||||||
|
}
|
||||||
|
// If the draft had __OTHER__ or if there's a custom answer from server, ensure __OTHER__ is selected
|
||||||
|
if (arr.includes("__OTHER__") || customAnswers.length > 0) {
|
||||||
|
arr = arr.filter((x: string) => standardOptions.includes(x));
|
||||||
|
arr.push("__OTHER__");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vals[q.id] = arr;
|
||||||
|
} else if (q.type === "radio") {
|
||||||
|
let val = ans || "";
|
||||||
|
if (q.allowOther && q.options && val !== "" && !q.options.includes(val)) {
|
||||||
|
if (val !== "__OTHER__") {
|
||||||
|
others[q.id] = val;
|
||||||
|
}
|
||||||
|
val = "__OTHER__";
|
||||||
|
}
|
||||||
|
vals[q.id] = val;
|
||||||
} else {
|
} else {
|
||||||
vals[q.id] = merged[q.id] || "";
|
vals[q.id] = ans || "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return vals;
|
return { defaultValues: vals, initialOtherValues: others };
|
||||||
}, [form.id, initialAnswers, questions]);
|
}, [form.id, initialAnswers, questions]);
|
||||||
|
|
||||||
|
const [otherValues, setOtherValues] = useState<Record<string, string>>(initialOtherValues);
|
||||||
|
|
||||||
const methods = useForm<z.infer<typeof formSchema>>({
|
const methods = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -170,10 +198,22 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const formattedAnswers = questions.map((q) => {
|
const formattedAnswers = questions.map((q) => {
|
||||||
const val = values[q.id] as string | string[] | undefined;
|
let val = values[q.id] as string | string[] | undefined;
|
||||||
|
if (q.allowOther) {
|
||||||
|
const customText = otherValues[q.id] || "";
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val = val.map(v => v === "__OTHER__" ? customText : v);
|
||||||
|
} else if (val === "__OTHER__") {
|
||||||
|
val = customText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalString = Array.isArray(val) ? val.join(", ") : (val || "");
|
||||||
|
if (q.required && !finalString.trim()) {
|
||||||
|
throw new Error(`จำเป็นต้องตอบ "${q.label}" (สำหรับตัวเลือก 'อื่นๆ')`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
questionId: q.id,
|
questionId: q.id,
|
||||||
value: Array.isArray(val) ? val.join(", ") : (val || ""),
|
value: finalString,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,6 +298,7 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
src={q.imageUrl}
|
src={q.imageUrl}
|
||||||
alt={q.label}
|
alt={q.label}
|
||||||
fill
|
fill
|
||||||
|
unoptimized
|
||||||
className="object-contain rounded-md"
|
className="object-contain rounded-md"
|
||||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
||||||
/>
|
/>
|
||||||
@@ -314,6 +355,25 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{q.allowOther && (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="__OTHER__" id={`${q.id}-other`} />
|
||||||
|
<Label htmlFor={`${q.id}-other`} className="font-normal cursor-pointer">
|
||||||
|
อื่นๆ
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{field.value === "__OTHER__" && (
|
||||||
|
<Input
|
||||||
|
className="ml-6 w-full max-w-xs"
|
||||||
|
value={otherValues[q.id] || ""}
|
||||||
|
onChange={(e) => setOtherValues(prev => ({...prev, [q.id]: e.target.value}))}
|
||||||
|
placeholder="โปรดระบุ"
|
||||||
|
disabled={!form.isOpen || isSubmitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -343,6 +403,36 @@ export function FormFillerClient({ form, questions, initialAnswers = {} }: FormF
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{q.allowOther && (
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`${q.id}-other`}
|
||||||
|
checked={Array.isArray(field.value) && field.value.includes("__OTHER__")}
|
||||||
|
disabled={!form.isOpen || isSubmitting}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const current = Array.isArray(field.value) ? field.value : [];
|
||||||
|
const updated = checked
|
||||||
|
? [...current, "__OTHER__"]
|
||||||
|
: current.filter(x => x !== "__OTHER__");
|
||||||
|
field.onChange(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`${q.id}-other`} className="font-normal cursor-pointer">
|
||||||
|
อื่นๆ
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
{Array.isArray(field.value) && field.value.includes("__OTHER__") && (
|
||||||
|
<Input
|
||||||
|
className="ml-6 w-full max-w-xs"
|
||||||
|
value={otherValues[q.id] || ""}
|
||||||
|
onChange={(e) => setOtherValues(prev => ({...prev, [q.id]: e.target.value}))}
|
||||||
|
placeholder="โปรดระบุ"
|
||||||
|
disabled={!form.isOpen || isSubmitting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const questions = pgTable("question", {
|
|||||||
displayOrder: integer("display_order").notNull().default(0),
|
displayOrder: integer("display_order").notNull().default(0),
|
||||||
required: boolean("required").notNull().default(false),
|
required: boolean("required").notNull().default(false),
|
||||||
options: jsonb("options").$type<string[]>().default([]),
|
options: jsonb("options").$type<string[]>().default([]),
|
||||||
|
allowOther: boolean("allow_other").notNull().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const submissions = pgTable("submission", {
|
export const submissions = pgTable("submission", {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "question" ADD COLUMN "allow_other" boolean DEFAULT false NOT NULL;
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
{
|
||||||
|
"id": "7ce7e225-2a92-45cb-8a26-72fa7d80ba64",
|
||||||
|
"prevId": "9272e7bb-ff66-4aa5-8022-21ea3a0afb8f",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"account.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "account",
|
||||||
|
"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",
|
||||||
|
"schemaTo": "account",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"account.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "account",
|
||||||
|
"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",
|
||||||
|
"schemaTo": "account",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"account.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "account",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"account.verificationToken": {
|
||||||
|
"name": "verificationToken",
|
||||||
|
"schema": "account",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"discord_webhook_url": {
|
||||||
|
"name": "discord_webhook_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"discord_webhook_template": {
|
||||||
|
"name": "discord_webhook_template",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"discord_webhook_update_template": {
|
||||||
|
"name": "discord_webhook_update_template",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"allow_other": {
|
||||||
|
"name": "allow_other",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {
|
||||||
|
"account": "account"
|
||||||
|
},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,13 @@
|
|||||||
"when": 1779969537666,
|
"when": 1779969537666,
|
||||||
"tag": "0005_handy_sally_floyd",
|
"tag": "0005_handy_sally_floyd",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780078238907,
|
||||||
|
"tag": "0006_concerned_mimic",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user