@k34a/blog 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # @k34a/forms
2
+
3
+ **Dynamic Form Builder & Handler for Next.js and Supabase**
4
+ Easily create, render, and manage dynamic forms directly from your admin panel.
5
+ Supports form validation, user-friendly UI (powered by Mantine), and built-in submission handling.
6
+
7
+ ## Features
8
+
9
+ - **Dynamic form rendering** using JSON schema
10
+ - **Automatic form validation** with [Zod](https://github.com/colinhacks/zod)
11
+ - **Form schema management** from your Supabase database
12
+ - **Built-in notification hooks** for success, error, and validation feedback
13
+ - **Easy API integration** for form submission
14
+ - **Custom callback support** (e.g., Telegram notifications)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @k34a/forms
20
+ ```
21
+
22
+ ### Required peer dependencies
23
+
24
+ Make sure you already have these installed in your project:
25
+
26
+ ```bash
27
+ npm install @mantine/core @mantine/dates @mantine/dropzone @mantine/notifications @supabase/supabase-js @tabler/icons-react react zod
28
+ ```
29
+
30
+ **Required versions:**
31
+
32
+ | Package | Version |
33
+ |----------|----------|
34
+ | `@mantine/core` | ≥ 8.0.0 |
35
+ | `@mantine/dates` | ≥ 8.0.0 |
36
+ | `@mantine/dropzone` | ≥ 8.0.0 |
37
+ | `@mantine/notifications` | ≥ 8.0.0 |
38
+ | `@supabase/supabase-js` | ≥ 2.52.0 |
39
+ | `@tabler/icons-react` | ≥ 3.0.0 |
40
+ | `react` | ≥ 19.1.0 |
41
+ | `zod` | ≥ 4.0.0 |
42
+
43
+
44
+ ## ⚙️ API Setup
45
+
46
+ You might need to create an **API route** in your application to handle form submissions.
47
+
48
+ Example using Next.js **Route Handlers (`app/api/fill-me/[formType]/route.ts`)**:
49
+
50
+ ```ts
51
+ import { adminPanelLink, ORG_ID } from "@/config/config";
52
+ import { supabaseAdmin } from "@/lib/db/supabase";
53
+ import { sendTelegramMessage } from "@/lib/telegram";
54
+ import { FormFillingService } from "@k34a/forms";
55
+ import { headers } from "next/headers";
56
+ import { NextRequest, NextResponse } from "next/server";
57
+
58
+ function isPlainObject(input: unknown): input is Record<string, any> {
59
+ return typeof input === "object" && input !== null && !Array.isArray(input);
60
+ }
61
+
62
+ async function getSourceDetails() {
63
+ const hdrs = await headers();
64
+ const userAgent = hdrs.get("user-agent") ?? null;
65
+ const xff = hdrs.get("x-forwarded-for");
66
+ const realIp = hdrs.get("x-real-ip");
67
+ const sourceIp = xff?.split(",")[0].trim() ?? realIp ?? null;
68
+ return { sourceIp, userAgent };
69
+ }
70
+
71
+ export async function POST(
72
+ request: NextRequest,
73
+ { params }: { params: Promise<{ formType: string }> },
74
+ ) {
75
+ const { formType } = await params;
76
+
77
+ let body: unknown;
78
+ try {
79
+ body = await request.json();
80
+ } catch {
81
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
82
+ }
83
+
84
+ if (!isPlainObject(body)) {
85
+ return NextResponse.json(
86
+ { error: "Invalid request format. Expected a key-value object." },
87
+ { status: 400 }
88
+ );
89
+ }
90
+
91
+ try {
92
+ const service = new FormFillingService(
93
+ adminPanelLink,
94
+ ORG_ID,
95
+ supabaseAdmin,
96
+ async (msg) => {
97
+ try {
98
+ await sendTelegramMessage(msg);
99
+ } catch (error) {
100
+ console.log(error);
101
+ }
102
+ },
103
+ );
104
+
105
+ const userDetails = await getSourceDetails();
106
+ const result = await service.fillForm(
107
+ formType,
108
+ body,
109
+ userDetails.sourceIp ?? "",
110
+ userDetails.userAgent ?? "",
111
+ );
112
+
113
+ return NextResponse.json(result);
114
+ } catch (err: any) {
115
+ return NextResponse.json({ error: err.message || "Internal Server Error" }, { status: 500 });
116
+ }
117
+ }
118
+ ```
119
+
120
+
121
+ ### Explanation of Methods & Callbacks
122
+
123
+ | Function / Callback | Description |
124
+ |---------------------|-------------|
125
+ | **`isPlainObject()`** | Ensures the incoming JSON body is a plain object (key-value pairs) and not an array or invalid input. |
126
+ | **`getSourceDetails()`** | Extracts the client’s IP address and `User-Agent` from headers for logging or analytics. |
127
+ | **`FormFillingService`** | Core service from `@k34a/forms` that handles form validation, storage, and notifications. |
128
+ | **`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. |
129
+ | **Telegram Callback (`async (msg) => { ... }`)** | Optional async callback triggered after a successful form submission. You can send notifications to Telegram, Slack, etc. |
130
+
131
+
132
+ ## Creating a Dynamic Form Component
133
+
134
+ The simplest way to render a form dynamically is by using the `FormBuilder` component.
135
+
136
+ ```tsx
137
+ "use client";
138
+
139
+ import { FormBuilder, FormSchema } from "@k34a/forms";
140
+ import z from "zod";
141
+ import { notifications } from "@mantine/notifications";
142
+ import { ORG_ID } from "@/config/config";
143
+
144
+ interface FormProps {
145
+ schema: z.infer<typeof FormSchema>;
146
+ formType: string;
147
+ }
148
+
149
+ export const FillMe = (props: FormProps) => {
150
+ return (
151
+ <FormBuilder
152
+ mode="fill"
153
+ schema={props.schema}
154
+ formType={props.formType}
155
+ orgId={ORG_ID}
156
+ submissionAPIEndPoint={`/api/fill-me/${props.formType}`}
157
+ onSuccess={() =>
158
+ notifications.show({
159
+ title: "All Set!",
160
+ message:
161
+ "Your details were submitted successfully. Thank you for completing the form!",
162
+ color: "green",
163
+ })
164
+ }
165
+ onValidationError={() =>
166
+ notifications.show({
167
+ title: "Please Review Your Form",
168
+ message:
169
+ "Some information seems to be missing or incorrect. Check the highlighted fields and try again.",
170
+ color: "orange",
171
+ })
172
+ }
173
+ onError={() =>
174
+ notifications.show({
175
+ title: "Submission Failed",
176
+ message:
177
+ "Something went wrong while sending your details. Please try again later.",
178
+ color: "red",
179
+ })
180
+ }
181
+ />
182
+ );
183
+ };
184
+ ```
185
+
186
+ > 💡 **Tip:** Make sure to set your organization ID (`ORG_ID`) from your admin panel at [k34a.vercel.app](https://k34a.vercel.app).
187
+
188
+
189
+ ## Fetching the Form Schema from Supabase
190
+
191
+ You can dynamically fetch a form schema from your Supabase database before rendering the form:
192
+
193
+ ```tsx
194
+ import { FormFillingService } from "@k34a/forms";
195
+ import { supabaseAdmin } from "@/lib/db/supabase";
196
+ import { notFound } from "next/navigation";
197
+ import { adminPanelLink, ORG_ID } from "@/config/config";
198
+ import Partners from "@/components/partners/partners";
199
+
200
+ export default async function PartnersPage() {
201
+ let schema;
202
+ const formType = "csr_partnership_inquiry";
203
+
204
+ try {
205
+ schema = await new FormFillingService(
206
+ adminPanelLink,
207
+ ORG_ID,
208
+ supabaseAdmin,
209
+ ).getFormSchema(formType);
210
+ } catch (err) {
211
+ console.error(err);
212
+ notFound();
213
+ }
214
+
215
+ return (
216
+ <main>
217
+ <Partners schema={schema} formType={formType} />
218
+ </main>
219
+ );
220
+ }
221
+ ```
222
+
223
+
224
+ ## Summary
225
+
226
+ | Step | Description |
227
+ |------|--------------|
228
+ | Install the package | `npm install @k34a/forms` |
229
+ | Create API handler | Accepts and validates form submissions |
230
+ | Use `FormBuilder` | To render and handle forms in your UI |
231
+ | Fetch form schemas | From your Supabase database using `FormFillingService` |
232
+ | Customize callbacks | (Success, ValidationError, Error) for better UX |
233
+
234
+ ## License
235
+
236
+ MIT © 2025 — Built with ❤️ by [K34A](https://k34a.vercel.app)
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { ArticleService } from './services/articles';
2
+ export { ArticleService };
package/dist/main.js ADDED
@@ -0,0 +1,4 @@
1
+ import { ArticleService as c } from "./services/articles.js";
2
+ export {
3
+ c as ArticleService
4
+ };
@@ -0,0 +1,52 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { articleQuerySchema } from './search-params';
3
+ import { default as z } from 'zod';
4
+ export interface ArticleDetails {
5
+ id: string;
6
+ title: string;
7
+ description: string;
8
+ slug: string;
9
+ created_at: Date;
10
+ updated_at: Date;
11
+ status: string;
12
+ banner_image: string | null;
13
+ metadata?: Record<string, any> | null;
14
+ tags: Array<string>;
15
+ }
16
+ export interface ArticleDetailsForListing {
17
+ id: string;
18
+ title: string;
19
+ description: string;
20
+ slug: string;
21
+ created_at: Date | string;
22
+ banner_image: string | null;
23
+ }
24
+ export interface PaginationArticleFilters {
25
+ limit: number;
26
+ offset: number;
27
+ }
28
+ export declare class ArticleService {
29
+ private db;
30
+ constructor(db: SupabaseClient);
31
+ /**
32
+ * Fetch a single article by slug.
33
+ * Includes its tags.
34
+ */
35
+ getBySlug(slug: string): Promise<ArticleDetails | null>;
36
+ /**
37
+ * Get total number of articles.
38
+ */
39
+ getCount(): Promise<number>;
40
+ /**
41
+ * Get all available tag names.
42
+ */
43
+ getTagNames(): Promise<string[]>;
44
+ /**
45
+ * Paginated list of articles with filters and sorting.
46
+ */
47
+ list(params: z.infer<typeof articleQuerySchema>): Promise<{
48
+ items: ArticleDetailsForListing[];
49
+ total: number;
50
+ }>;
51
+ getDescription(id: string): Promise<string | null>;
52
+ }
@@ -0,0 +1,95 @@
1
+ import { articleSortByVsQuery as _ } from "./search-params.js";
2
+ import "zod";
3
+ const p = 12;
4
+ class q {
5
+ db;
6
+ constructor(r) {
7
+ this.db = r;
8
+ }
9
+ /**
10
+ * Fetch a single article by slug.
11
+ * Includes its tags.
12
+ */
13
+ async getBySlug(r) {
14
+ const { data: t, error: a } = await this.db.from("articles").select("*").eq("slug", r).eq("status", "Published").single();
15
+ if (a)
16
+ return console.error("Error fetching article details:", a.message), null;
17
+ const { data: c, error: s } = await this.db.from("tag_articles").select("tag_id").eq("article_id", t.id);
18
+ if (s)
19
+ return console.error(
20
+ "Error fetching article tag links:",
21
+ s.message
22
+ ), { ...t, tags: [] };
23
+ const o = c?.map((i) => i.tag_id) || [];
24
+ let e = [];
25
+ if (o.length > 0) {
26
+ const { data: i, error: n } = await this.db.from("tags").select("name").in("id", o);
27
+ n ? console.error(
28
+ "Error fetching article tag names:",
29
+ n.message
30
+ ) : e = i.map((l) => l.name);
31
+ }
32
+ return { ...t, tags: e };
33
+ }
34
+ /**
35
+ * Get total number of articles.
36
+ */
37
+ async getCount() {
38
+ const { data: r, error: t } = await this.db.rpc("get_table_row_count", {
39
+ arg_schema_name: "public",
40
+ arg_table_name: "articles"
41
+ }).single();
42
+ return t ? (console.error("Error fetching articles count:", t), 0) : r || 0;
43
+ }
44
+ /**
45
+ * Get all available tag names.
46
+ */
47
+ async getTagNames() {
48
+ const { data: r, error: t } = await this.db.from("tags").select("name");
49
+ return t ? (console.error("Error fetching tags:", t.message), []) : r?.map((a) => a.name) ?? [];
50
+ }
51
+ /**
52
+ * Paginated list of articles with filters and sorting.
53
+ */
54
+ async list(r) {
55
+ const { page: t, search: a, sortBy: c, tags: s } = r, o = _[c] ?? _.latest;
56
+ let e = this.db.from("articles").select("*", { count: "exact" }).eq("status", "Published");
57
+ if (a && (e = e.ilike("title", `%${a}%`)), s && s.length > 0) {
58
+ const { data: m, error: f } = await this.db.from("tags").select("id").in("name", s);
59
+ if (f || !m?.length)
60
+ return console.error("Error fetching tag IDs:", f?.message), { items: [], total: 0 };
61
+ const w = m.map((g) => g.id), { data: u, error: h } = await this.db.from("tag_articles").select("article_id").in("tag_id", w);
62
+ if (h || !u?.length)
63
+ return console.error(
64
+ "Error fetching articles by tags:",
65
+ h?.message
66
+ ), { items: [], total: 0 };
67
+ const b = Array.from(
68
+ new Set(u.map((g) => g.article_id))
69
+ );
70
+ if (b.length === 0)
71
+ return { items: [], total: 0 };
72
+ e = e.in("id", b);
73
+ }
74
+ e = e.order(o.column, { ascending: o.ascending });
75
+ const i = t * p, n = i + p - 1;
76
+ e = e.range(i, n);
77
+ const { data: l, error: d, count: E } = await e;
78
+ return d ? (console.error("Error fetching articles:", d), { items: [], total: 0 }) : {
79
+ items: l || [],
80
+ total: E || 0
81
+ };
82
+ }
83
+ async getDescription(r) {
84
+ const { data: t, error: a } = await this.db.storage.from("content").download(`articles/${r}/description.html`);
85
+ return a ? (console.error(
86
+ `Error fetching article description for article ID "${r}":`,
87
+ a.message
88
+ ), null) : t ? await t.text() : (console.warn(
89
+ `No article description file found for article ID: ${r}`
90
+ ), null);
91
+ }
92
+ }
93
+ export {
94
+ q as ArticleService
95
+ };
@@ -0,0 +1,16 @@
1
+ import { default as z } from 'zod';
2
+ export declare const articleQuerySchema: z.ZodObject<{
3
+ search: z.ZodDefault<z.ZodString>;
4
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
5
+ sortBy: z.ZodDefault<z.ZodEnum<{
6
+ latest: "latest";
7
+ oldest: "oldest";
8
+ "A-Z": "A-Z";
9
+ "Z-A": "Z-A";
10
+ }>>;
11
+ tags: z.ZodDefault<z.ZodPipe<z.ZodTransform<any[], unknown>, z.ZodArray<z.ZodString>>>;
12
+ }, z.core.$strip>;
13
+ export declare const articleSortByVsQuery: Record<z.infer<typeof articleQuerySchema>["sortBy"], {
14
+ column: string;
15
+ ascending: boolean;
16
+ }>;
@@ -0,0 +1,16 @@
1
+ import e from "zod";
2
+ const n = e.object({
3
+ search: e.string().default(""),
4
+ page: e.coerce.number().int().nonnegative().default(0),
5
+ sortBy: e.enum(["latest", "oldest", "A-Z", "Z-A"]).default("latest"),
6
+ tags: e.preprocess((t) => typeof t == "string" ? t.split(",").map((r) => r.trim()).filter(Boolean) : Array.isArray(t) ? t : [], e.array(e.string())).default([])
7
+ }), s = {
8
+ latest: { column: "created_at", ascending: !1 },
9
+ oldest: { column: "created_at", ascending: !0 },
10
+ "A-Z": { column: "title", ascending: !0 },
11
+ "Z-A": { column: "title", ascending: !1 }
12
+ };
13
+ export {
14
+ n as articleQuerySchema,
15
+ s as articleSortByVsQuery
16
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@k34a/blog",
3
+ "description": "Create and share articles with your audience.",
4
+ "private": false,
5
+ "version": "0.0.1",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/lib/main.d.ts",
10
+ "default": "./dist/main.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "build": "tsc --p ./tsconfig.lib.json --jsx react-jsx && vite build",
19
+ "lint": "eslint .",
20
+ "preview": "vite preview",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "peerDependencies": {
24
+ "@mantine/core": ">=8.0.0",
25
+ "@mantine/dates": ">=8.0.0",
26
+ "@mantine/dropzone": ">=8.0.0",
27
+ "@mantine/notifications": ">=8.0.0",
28
+ "@supabase/supabase-js": ">=2.52.0",
29
+ "@tabler/icons-react": ">=3.0.0",
30
+ "react": ">=19.1.0",
31
+ "zod": ">=4.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/js": "^9.36.0",
35
+ "@mantine/core": "^8.1.3",
36
+ "@mantine/dates": "^8.3.5",
37
+ "@mantine/dropzone": "^8.3.5",
38
+ "@mantine/notifications": "^8.3.5",
39
+ "@supabase/supabase-js": "^2.76.1",
40
+ "@tabler/icons-react": "^3.35.0",
41
+ "@types/node": "^24.6.0",
42
+ "@types/react": "^19.1.16",
43
+ "@types/react-dom": "^19.1.9",
44
+ "@vitejs/plugin-react": "^5.0.4",
45
+ "eslint": "^9.36.0",
46
+ "eslint-plugin-react-hooks": "^5.2.0",
47
+ "eslint-plugin-react-refresh": "^0.4.22",
48
+ "glob": "^11.0.3",
49
+ "globals": "^16.4.0",
50
+ "react": "^19.1.1",
51
+ "react-dom": "^19.1.1",
52
+ "rollup-plugin-banner2": "^1.3.1",
53
+ "typescript": "~5.9.3",
54
+ "typescript-eslint": "^8.45.0",
55
+ "vite": "^7.1.7",
56
+ "vite-plugin-dts": "^4.5.4",
57
+ "vite-plugin-lib-inject-css": "^2.2.2",
58
+ "zod": "^4.1.12"
59
+ },
60
+ "sideEffects": [
61
+ "**/*.css"
62
+ ]
63
+ }