@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 +236 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +4 -0
- package/dist/services/articles.d.ts +52 -0
- package/dist/services/articles.js +95 -0
- package/dist/services/search-params.d.ts +16 -0
- package/dist/services/search-params.js +16 -0
- package/package.json +63 -0
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
package/dist/main.js
ADDED
|
@@ -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
|
+
}
|