npm package: @k34a/forms
Description: Create and share forms, and allow users to fill them.
Latest version: 0.0.45

# @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](https://github.com/colinhacks/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

```bash
npm install @k34a/forms
```

### Required peer dependencies

Make sure you already have these installed in your project:

```bash
npm install @mantine/core @mantine/dates @mantine/dropzone @mantine/notifications @supabase/supabase-js @tabler/icons-react react zod
```

**Required 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`)**:

```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.

```tsx
"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](https://k34a.vercel.app).


## Fetching the Form Schema from Supabase

You can dynamically fetch a form schema from your Supabase database before rendering the form:

```tsx
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](https://k34a.vercel.app)