nextworks 0.1.0-alpha.11 → 0.1.0-alpha.14
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 +20 -9
- package/dist/kits/auth-core/.nextworks/docs/AUTH_CORE_README.md +3 -3
- package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +264 -244
- package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +120 -114
- package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +116 -114
- package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +66 -63
- package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +1 -1
- package/dist/kits/auth-core/app/api/users/[id]/route.ts +134 -127
- package/dist/kits/auth-core/app/auth/reset-password/page.tsx +186 -187
- package/dist/kits/auth-core/components/auth/dashboard.tsx +25 -2
- package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -90
- package/dist/kits/auth-core/components/auth/login-form.tsx +492 -467
- package/dist/kits/auth-core/components/auth/signup-form.tsx +28 -29
- package/dist/kits/auth-core/lib/auth.ts +46 -15
- package/dist/kits/auth-core/lib/forms/map-errors.ts +37 -11
- package/dist/kits/auth-core/lib/server/result.ts +45 -45
- package/dist/kits/auth-core/lib/validation/forms.ts +1 -2
- package/dist/kits/auth-core/package-deps.json +4 -2
- package/dist/kits/auth-core/types/next-auth.d.ts +1 -1
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +2 -8
- package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +18 -1
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +0 -2
- package/dist/kits/blocks/components/sections/FAQ.tsx +0 -1
- package/dist/kits/blocks/components/sections/Newsletter.tsx +2 -2
- package/dist/kits/blocks/components/ui/switch.tsx +78 -78
- package/dist/kits/blocks/components/ui/theme-selector.tsx +1 -1
- package/dist/kits/blocks/lib/themes.ts +1 -0
- package/dist/kits/blocks/package-deps.json +4 -4
- package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +128 -112
- package/dist/kits/data/.nextworks/docs/DATA_README.md +2 -1
- package/dist/kits/data/app/api/posts/[id]/route.ts +83 -83
- package/dist/kits/data/app/api/posts/route.ts +136 -138
- package/dist/kits/data/app/api/seed-demo/route.ts +1 -2
- package/dist/kits/data/app/api/users/[id]/route.ts +29 -17
- package/dist/kits/data/app/api/users/check-email/route.ts +1 -1
- package/dist/kits/data/app/api/users/check-unique/route.ts +30 -27
- package/dist/kits/data/app/api/users/route.ts +0 -2
- package/dist/kits/data/app/examples/demo/create-post-form.tsx +108 -106
- package/dist/kits/data/app/examples/demo/page.tsx +2 -1
- package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +1 -1
- package/dist/kits/data/components/admin/posts-manager.tsx +727 -719
- package/dist/kits/data/components/admin/users-manager.tsx +435 -432
- package/dist/kits/data/lib/server/result.ts +5 -2
- package/dist/kits/data/package-deps.json +1 -1
- package/dist/kits/data/scripts/seed-demo.mjs +1 -2
- package/dist/kits/forms/app/api/wizard/route.ts +76 -71
- package/dist/kits/forms/app/examples/forms/server-action/page.tsx +78 -71
- package/dist/kits/forms/components/hooks/useCheckUnique.ts +85 -79
- package/dist/kits/forms/components/ui/form/form-control.tsx +28 -28
- package/dist/kits/forms/components/ui/form/form-description.tsx +23 -22
- package/dist/kits/forms/components/ui/form/form-item.tsx +21 -21
- package/dist/kits/forms/components/ui/form/form-label.tsx +24 -24
- package/dist/kits/forms/components/ui/form/form-message.tsx +28 -29
- package/dist/kits/forms/components/ui/switch.tsx +78 -78
- package/dist/kits/forms/lib/forms/map-errors.ts +1 -1
- package/dist/kits/forms/lib/validation/forms.ts +1 -2
- package/package.json +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import type { ZodError } from "zod";
|
|
3
|
-
import type { Prisma } from "@prisma/client";
|
|
4
3
|
import { zodErrorToFieldErrors, type FieldErrors } from "@/lib/api/errors";
|
|
5
4
|
|
|
6
5
|
export type ApiResult<T = unknown> = {
|
|
@@ -65,7 +64,11 @@ function isPrismaKnownRequestError(
|
|
|
65
64
|
function extractUniqueField(
|
|
66
65
|
err: PrismaKnownRequestErrorLike,
|
|
67
66
|
): string | undefined {
|
|
68
|
-
const
|
|
67
|
+
const meta = err.meta;
|
|
68
|
+
const target =
|
|
69
|
+
meta && typeof meta === "object" && "target" in meta
|
|
70
|
+
? (meta as { target?: unknown }).target
|
|
71
|
+
: undefined;
|
|
69
72
|
if (Array.isArray(target) && target.length) return String(target[0]);
|
|
70
73
|
if (typeof target === "string") return String(target.split("_")[0]);
|
|
71
74
|
return undefined;
|
|
@@ -4,10 +4,9 @@ const prisma = new PrismaClient();
|
|
|
4
4
|
|
|
5
5
|
async function main() {
|
|
6
6
|
const adminEmail = process.env.SEED_ADMIN_EMAIL || "admin@example.com";
|
|
7
|
-
const adminPassword = process.env.SEED_ADMIN_PASSWORD || "password123";
|
|
8
7
|
|
|
9
8
|
// Upsert admin user
|
|
10
|
-
|
|
9
|
+
await prisma.user.upsert({
|
|
11
10
|
where: { email: adminEmail },
|
|
12
11
|
update: {},
|
|
13
12
|
create: {
|
|
@@ -1,71 +1,76 @@
|
|
|
1
|
-
import { NextRequest } from "next/server";
|
|
2
|
-
import {
|
|
3
|
-
jsonOk,
|
|
4
|
-
jsonFromZod,
|
|
5
|
-
jsonFromPrisma,
|
|
6
|
-
jsonFail,
|
|
7
|
-
} from "@/lib/server/result";
|
|
8
|
-
import { wizardServerSchema } from "@/lib/validation/wizard";
|
|
9
|
-
import { prisma } from "@/lib/prisma";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
jsonOk,
|
|
4
|
+
jsonFromZod,
|
|
5
|
+
jsonFromPrisma,
|
|
6
|
+
jsonFail,
|
|
7
|
+
} from "@/lib/server/result";
|
|
8
|
+
import { wizardServerSchema } from "@/lib/validation/wizard";
|
|
9
|
+
import { prisma } from "@/lib/prisma";
|
|
10
|
+
import { getServerSession } from "next-auth";
|
|
11
|
+
import { authOptions } from "@/lib/auth";
|
|
12
|
+
|
|
13
|
+
export const runtime = "nodejs";
|
|
14
|
+
|
|
15
|
+
export async function POST(req: NextRequest) {
|
|
16
|
+
try {
|
|
17
|
+
const body = await req.json();
|
|
18
|
+
const parsed = wizardServerSchema.parse(body);
|
|
19
|
+
// coerce to typed server input
|
|
20
|
+
void (parsed satisfies import("@/lib/validation/wizard").WizardServerValues);
|
|
21
|
+
// require session to attach author
|
|
22
|
+
const session = await getServerSession(authOptions);
|
|
23
|
+
if (!session?.user?.id)
|
|
24
|
+
return jsonFail("Not authenticated", { status: 401 });
|
|
25
|
+
const userId = session.user.id;
|
|
26
|
+
// map visibility to published flag
|
|
27
|
+
const published = parsed.visibility === "public";
|
|
28
|
+
// create Post record (persist full set of fields)
|
|
29
|
+
const data = {
|
|
30
|
+
title: parsed.title,
|
|
31
|
+
slug: parsed.slug || undefined,
|
|
32
|
+
content: parsed.content || undefined,
|
|
33
|
+
excerpt: parsed.excerpt || undefined,
|
|
34
|
+
tags: parsed.tags || undefined,
|
|
35
|
+
published,
|
|
36
|
+
author: { connect: { id: userId } },
|
|
37
|
+
} satisfies Parameters<typeof prisma.post.create>[0]["data"];
|
|
38
|
+
const created = await prisma.post.create({ data });
|
|
39
|
+
// return a cleaned, typed result object (maps published -> visibility)
|
|
40
|
+
const result: import("@/lib/validation/wizard").WizardServerValues & {
|
|
41
|
+
id: string;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
} = {
|
|
44
|
+
title: created.title,
|
|
45
|
+
slug: created.slug ?? "",
|
|
46
|
+
content: created.content ?? "",
|
|
47
|
+
excerpt: created.excerpt ?? "",
|
|
48
|
+
visibility: created.published ? "public" : "private",
|
|
49
|
+
tags: created.tags ?? "",
|
|
50
|
+
id: created.id,
|
|
51
|
+
createdAt: created.createdAt,
|
|
52
|
+
};
|
|
53
|
+
return jsonOk(result, { status: 201, message: "Created" });
|
|
54
|
+
} catch (e: unknown) {
|
|
55
|
+
// zod errors
|
|
56
|
+
// prisma errors
|
|
57
|
+
try {
|
|
58
|
+
// detect zod error (ZodError has an `issues` array)
|
|
59
|
+
if (
|
|
60
|
+
typeof e === "object" &&
|
|
61
|
+
e !== null &&
|
|
62
|
+
"issues" in e &&
|
|
63
|
+
Array.isArray((e as { issues?: unknown }).issues)
|
|
64
|
+
) {
|
|
65
|
+
return jsonFromZod(e as never);
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return jsonFromPrisma(e);
|
|
71
|
+
} catch {
|
|
72
|
+
console.error(e);
|
|
73
|
+
return jsonOk(null, { message: "Unknown error" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,71 +1,78 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import ServerActionForm from "./form-client";
|
|
3
|
-
import { prisma } from "@/lib/prisma";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { redirect } from "next/navigation";
|
|
6
|
-
|
|
7
|
-
const schema = z.object({
|
|
8
|
-
title: z.string().min(1, "Title is required"),
|
|
9
|
-
content: z.string().optional(),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export default async function Page({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ServerActionForm from "./form-client";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { redirect } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
const schema = z.object({
|
|
8
|
+
title: z.string().min(1, "Title is required"),
|
|
9
|
+
content: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default async function Page({
|
|
13
|
+
searchParams,
|
|
14
|
+
}: {
|
|
15
|
+
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
16
|
+
}) {
|
|
17
|
+
async function createPost(formData: FormData) {
|
|
18
|
+
"use server";
|
|
19
|
+
const title = formData.get("title")?.toString() ?? "";
|
|
20
|
+
const content = formData.get("content")?.toString() ?? "";
|
|
21
|
+
|
|
22
|
+
const parsed = schema.safeParse({ title, content });
|
|
23
|
+
if (!parsed.success) {
|
|
24
|
+
// For simplicity in this example, throw an error which will surface in the server logs / dev overlay.
|
|
25
|
+
// In a real app, you'd return structured errors or use progressive enhancement with client-side validation.
|
|
26
|
+
throw new Error("Validation failed: " + parsed.error.message);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// NOTE: The Post model requires authorId. For this example we upsert a demo author so the FK is satisfied.
|
|
30
|
+
// In real apps you should resolve the current user via NextAuth session.
|
|
31
|
+
const fallbackAuthorId = "__demo_author__";
|
|
32
|
+
|
|
33
|
+
await prisma.user.upsert({
|
|
34
|
+
where: { id: fallbackAuthorId },
|
|
35
|
+
update: {},
|
|
36
|
+
create: {
|
|
37
|
+
id: fallbackAuthorId,
|
|
38
|
+
email: "demo@example.com",
|
|
39
|
+
name: "Demo User",
|
|
40
|
+
// No password for demo account
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await prisma.post.create({
|
|
45
|
+
data: {
|
|
46
|
+
title: parsed.data.title,
|
|
47
|
+
content: parsed.data.content ?? null,
|
|
48
|
+
authorId: fallbackAuthorId,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
redirect("/examples/forms/server-action?created=1");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const params = (await searchParams) ?? {};
|
|
56
|
+
const createdRaw = params.created;
|
|
57
|
+
const created = Array.isArray(createdRaw)
|
|
58
|
+
? createdRaw[0] === "1"
|
|
59
|
+
: createdRaw === "1";
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="mx-auto max-w-xl p-6">
|
|
63
|
+
<h1 className="mb-4 text-2xl font-bold">Server Action Example</h1>
|
|
64
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
65
|
+
This demo shows a simple server action that validates with zod and
|
|
66
|
+
creates a Post via prisma (uses a demo fallback authorId).
|
|
67
|
+
</p>
|
|
68
|
+
{/* Pass the server action down to the client form */}
|
|
69
|
+
<ServerActionForm action={createPost} />
|
|
70
|
+
|
|
71
|
+
{created && (
|
|
72
|
+
<p className="mt-4 text-sm text-green-600">
|
|
73
|
+
Post created successfully.
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -1,79 +1,85 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, useRef } from "react";
|
|
4
|
-
|
|
5
|
-
type UseCheckUniqueResult = {
|
|
6
|
-
loading: boolean;
|
|
7
|
-
unique: boolean | null; // null = unknown / not checked
|
|
8
|
-
error: string | null;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export default function useCheckUnique(
|
|
12
|
-
field: string,
|
|
13
|
-
value: string | undefined | null,
|
|
14
|
-
delay = 500,
|
|
15
|
-
): UseCheckUniqueResult {
|
|
16
|
-
const [loading, setLoading] = useState(false);
|
|
17
|
-
const [unique, setUnique] = useState<boolean | null>(null);
|
|
18
|
-
const [error, setError] = useState<string | null>(null);
|
|
19
|
-
const timer = useRef<number | undefined>(undefined);
|
|
20
|
-
const controllerRef = useRef<AbortController | null>(null);
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
// Clear previous timer / request
|
|
24
|
-
if (timer.current) window.clearTimeout(timer.current);
|
|
25
|
-
if (controllerRef.current) controllerRef.current.abort();
|
|
26
|
-
|
|
27
|
-
// Reset state for empty values
|
|
28
|
-
if (!value) {
|
|
29
|
-
setLoading(false);
|
|
30
|
-
setUnique(null);
|
|
31
|
-
setError(null);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setLoading(true);
|
|
36
|
-
setError(null);
|
|
37
|
-
|
|
38
|
-
timer.current = window.setTimeout(() => {
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
controllerRef.current = controller;
|
|
41
|
-
|
|
42
|
-
(async () => {
|
|
43
|
-
try {
|
|
44
|
-
const res = await fetch("/api/users/check-unique", {
|
|
45
|
-
method: "POST",
|
|
46
|
-
headers: { "Content-Type": "application/json" },
|
|
47
|
-
body: JSON.stringify({ field, value }),
|
|
48
|
-
signal: controller.signal,
|
|
49
|
-
});
|
|
50
|
-
const payload = await res.json().catch(() => null);
|
|
51
|
-
if (!res.ok || !payload) {
|
|
52
|
-
setError("Failed to validate");
|
|
53
|
-
setUnique(null);
|
|
54
|
-
} else if (payload?.success) {
|
|
55
|
-
// Expecting { success: true, data: { unique: boolean } }
|
|
56
|
-
setUnique(Boolean(payload.data?.unique));
|
|
57
|
-
} else {
|
|
58
|
-
setError(payload?.message || "Validation failed");
|
|
59
|
-
setUnique(null);
|
|
60
|
-
}
|
|
61
|
-
} catch (e: unknown) {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
type UseCheckUniqueResult = {
|
|
6
|
+
loading: boolean;
|
|
7
|
+
unique: boolean | null; // null = unknown / not checked
|
|
8
|
+
error: string | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function useCheckUnique(
|
|
12
|
+
field: string,
|
|
13
|
+
value: string | undefined | null,
|
|
14
|
+
delay = 500,
|
|
15
|
+
): UseCheckUniqueResult {
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [unique, setUnique] = useState<boolean | null>(null);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
const timer = useRef<number | undefined>(undefined);
|
|
20
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Clear previous timer / request
|
|
24
|
+
if (timer.current) window.clearTimeout(timer.current);
|
|
25
|
+
if (controllerRef.current) controllerRef.current.abort();
|
|
26
|
+
|
|
27
|
+
// Reset state for empty values
|
|
28
|
+
if (!value) {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
setUnique(null);
|
|
31
|
+
setError(null);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setLoading(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
|
|
38
|
+
timer.current = window.setTimeout(() => {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
controllerRef.current = controller;
|
|
41
|
+
|
|
42
|
+
(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch("/api/users/check-unique", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ field, value }),
|
|
48
|
+
signal: controller.signal,
|
|
49
|
+
});
|
|
50
|
+
const payload = await res.json().catch(() => null);
|
|
51
|
+
if (!res.ok || !payload) {
|
|
52
|
+
setError("Failed to validate");
|
|
53
|
+
setUnique(null);
|
|
54
|
+
} else if (payload?.success) {
|
|
55
|
+
// Expecting { success: true, data: { unique: boolean } }
|
|
56
|
+
setUnique(Boolean(payload.data?.unique));
|
|
57
|
+
} else {
|
|
58
|
+
setError(payload?.message || "Validation failed");
|
|
59
|
+
setUnique(null);
|
|
60
|
+
}
|
|
61
|
+
} catch (e: unknown) {
|
|
62
|
+
if (
|
|
63
|
+
e &&
|
|
64
|
+
typeof e === "object" &&
|
|
65
|
+
"name" in e &&
|
|
66
|
+
(e as { name?: unknown }).name === "AbortError"
|
|
67
|
+
)
|
|
68
|
+
return;
|
|
69
|
+
setError("Validation failed");
|
|
70
|
+
setUnique(null);
|
|
71
|
+
} finally {
|
|
72
|
+
setLoading(false);
|
|
73
|
+
controllerRef.current = null;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
}, delay);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
if (timer.current) window.clearTimeout(timer.current);
|
|
80
|
+
if (controllerRef.current) controllerRef.current.abort();
|
|
81
|
+
};
|
|
82
|
+
}, [field, value, delay]);
|
|
83
|
+
|
|
84
|
+
return { loading, unique, error };
|
|
85
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { useFormField } from "./context";
|
|
5
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const FormControl = React.forwardRef<
|
|
11
|
-
React.ElementRef<typeof Slot>,
|
|
12
|
-
FormControlProps
|
|
13
|
-
>(({ ...props }, ref) => {
|
|
14
|
-
const { formItemId, formDescriptionId, formMessageId, error } =
|
|
15
|
-
useFormField();
|
|
16
|
-
return (
|
|
17
|
-
<Slot
|
|
18
|
-
ref={ref}
|
|
19
|
-
id={formItemId}
|
|
20
|
-
aria-describedby={
|
|
21
|
-
error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId
|
|
22
|
-
}
|
|
23
|
-
aria-invalid={!!error}
|
|
24
|
-
{...props}
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
FormControl.displayName = "FormControl";
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useFormField } from "./context";
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
+
|
|
7
|
+
export type FormControlProps = React.ComponentPropsWithoutRef<typeof Slot>;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export const FormControl = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof Slot>,
|
|
12
|
+
FormControlProps
|
|
13
|
+
>(({ ...props }, ref) => {
|
|
14
|
+
const { formItemId, formDescriptionId, formMessageId, error } =
|
|
15
|
+
useFormField();
|
|
16
|
+
return (
|
|
17
|
+
<Slot
|
|
18
|
+
ref={ref}
|
|
19
|
+
id={formItemId}
|
|
20
|
+
aria-describedby={
|
|
21
|
+
error ? `${formDescriptionId} ${formMessageId}` : formDescriptionId
|
|
22
|
+
}
|
|
23
|
+
aria-invalid={!!error}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
FormControl.displayName = "FormControl";
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { useFormField } from "./context";
|
|
5
|
-
import { cn } from "@/lib/utils";
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useFormField } from "./context";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
export type FormDescriptionProps =
|
|
8
|
+
React.HTMLAttributes<HTMLParagraphElement>;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export const FormDescription = ({
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: FormDescriptionProps) => {
|
|
15
|
+
const { formDescriptionId } = useFormField();
|
|
16
|
+
return (
|
|
17
|
+
<p
|
|
18
|
+
id={formDescriptionId}
|
|
19
|
+
className={cn("text-xs text-muted-foreground", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|