@k34a/forms
v0.0.40
Published
Create and share forms, and allow users to fill them.
Readme
@k34a/forms
Dynamic Form Builder & Handler for Next.js and Supabase
Easily create, render, and manage dynamic forms directly from your admin panel.
Supports form validation, user-friendly UI (powered by Mantine), and built-in submission handling.
Features
- Dynamic form rendering using JSON schema
- Automatic form validation with Zod
- Form schema management from your Supabase database
- Built-in notification hooks for success, error, and validation feedback
- Easy API integration for form submission
- Custom callback support (e.g., Telegram notifications)
Installation
npm install @k34a/formsRequired peer dependencies
Make sure you already have these installed in your project:
npm install @mantine/core @mantine/dates @mantine/dropzone @mantine/notifications @supabase/supabase-js @tabler/icons-react react zodRequired versions:
| Package | Version |
|----------|----------|
| @mantine/core | ≥ 8.0.0 |
| @mantine/dates | ≥ 8.0.0 |
| @mantine/dropzone | ≥ 8.0.0 |
| @mantine/notifications | ≥ 8.0.0 |
| @supabase/supabase-js | ≥ 2.52.0 |
| @tabler/icons-react | ≥ 3.0.0 |
| react | ≥ 19.1.0 |
| zod | ≥ 4.0.0 |
⚙️ API Setup
You might need to create an API route in your application to handle form submissions.
Example using Next.js Route Handlers (app/api/fill-me/[formType]/route.ts):
import { adminPanelLink, ORG_ID } from "@/config/config";
import { supabaseAdmin } from "@/lib/db/supabase";
import { sendTelegramMessage } from "@/lib/telegram";
import { FormFillingService } from "@k34a/forms";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
function isPlainObject(input: unknown): input is Record<string, any> {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
async function getSourceDetails() {
const hdrs = await headers();
const userAgent = hdrs.get("user-agent") ?? null;
const xff = hdrs.get("x-forwarded-for");
const realIp = hdrs.get("x-real-ip");
const sourceIp = xff?.split(",")[0].trim() ?? realIp ?? null;
return { sourceIp, userAgent };
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ formType: string }> },
) {
const { formType } = await params;
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
if (!isPlainObject(body)) {
return NextResponse.json(
{ error: "Invalid request format. Expected a key-value object." },
{ status: 400 }
);
}
try {
const service = new FormFillingService(
adminPanelLink,
ORG_ID,
supabaseAdmin,
async (msg) => {
try {
await sendTelegramMessage(msg);
} catch (error) {
console.log(error);
}
},
);
const userDetails = await getSourceDetails();
const result = await service.fillForm(
formType,
body,
userDetails.sourceIp ?? "",
userDetails.userAgent ?? "",
);
return NextResponse.json(result);
} catch (err: any) {
return NextResponse.json({ error: err.message || "Internal Server Error" }, { status: 500 });
}
}Explanation of Methods & Callbacks
| Function / Callback | Description |
|---------------------|-------------|
| isPlainObject() | Ensures the incoming JSON body is a plain object (key-value pairs) and not an array or invalid input. |
| getSourceDetails() | Extracts the client’s IP address and User-Agent from headers for logging or analytics. |
| FormFillingService | Core service from @k34a/forms that handles form validation, storage, and notifications. |
| fillForm(formType, body, ip, userAgent) | Saves and validates the submitted form data. It links the form type to the schema defined in your admin panel. |
| Telegram Callback (async (msg) => { ... }) | Optional async callback triggered after a successful form submission. You can send notifications to Telegram, Slack, etc. |
Creating a Dynamic Form Component
The simplest way to render a form dynamically is by using the FormBuilder component.
"use client";
import { FormBuilder, FormSchema } from "@k34a/forms";
import z from "zod";
import { notifications } from "@mantine/notifications";
import { ORG_ID } from "@/config/config";
interface FormProps {
schema: z.infer<typeof FormSchema>;
formType: string;
}
export const FillMe = (props: FormProps) => {
return (
<FormBuilder
mode="fill"
schema={props.schema}
formType={props.formType}
orgId={ORG_ID}
submissionAPIEndPoint={`/api/fill-me/${props.formType}`}
onSuccess={() =>
notifications.show({
title: "All Set!",
message:
"Your details were submitted successfully. Thank you for completing the form!",
color: "green",
})
}
onValidationError={() =>
notifications.show({
title: "Please Review Your Form",
message:
"Some information seems to be missing or incorrect. Check the highlighted fields and try again.",
color: "orange",
})
}
onError={() =>
notifications.show({
title: "Submission Failed",
message:
"Something went wrong while sending your details. Please try again later.",
color: "red",
})
}
/>
);
};💡 Tip: Make sure to set your organization ID (
ORG_ID) from your admin panel at k34a.vercel.app.
Fetching the Form Schema from Supabase
You can dynamically fetch a form schema from your Supabase database before rendering the form:
import { FormFillingService } from "@k34a/forms";
import { supabaseAdmin } from "@/lib/db/supabase";
import { notFound } from "next/navigation";
import { adminPanelLink, ORG_ID } from "@/config/config";
import Partners from "@/components/partners/partners";
export default async function PartnersPage() {
let schema;
const formType = "csr_partnership_inquiry";
try {
schema = await new FormFillingService(
adminPanelLink,
ORG_ID,
supabaseAdmin,
).getFormSchema(formType);
} catch (err) {
console.error(err);
notFound();
}
return (
<main>
<Partners schema={schema} formType={formType} />
</main>
);
}Summary
| Step | Description |
|------|--------------|
| Install the package | npm install @k34a/forms |
| Create API handler | Accepts and validates form submissions |
| Use FormBuilder | To render and handle forms in your UI |
| Fetch form schemas | From your Supabase database using FormFillingService |
| Customize callbacks | (Success, ValidationError, Error) for better UX |
License
MIT © 2025 — Built with ❤️ by K34A
