nextworks 0.0.1 → 0.1.0-alpha.0
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 +209 -30
- package/dist/.gitkeep +0 -0
- package/dist/cli_manifests/auth_manifest.json +86 -0
- package/dist/cli_manifests/blocks_manifest.json +185 -0
- package/dist/cli_manifests/data_manifest.json +51 -0
- package/dist/cli_manifests/forms_manifest.json +61 -0
- package/dist/commands/admin-posts.d.ts +2 -0
- package/dist/commands/admin-posts.d.ts.map +1 -0
- package/dist/commands/admin-posts.js +15 -0
- package/dist/commands/admin-posts.js.map +1 -0
- package/dist/commands/admin-users.d.ts +2 -0
- package/dist/commands/admin-users.d.ts.map +1 -0
- package/dist/commands/admin-users.js +15 -0
- package/dist/commands/admin-users.js.map +1 -0
- package/dist/commands/auth-core.d.ts +2 -0
- package/dist/commands/auth-core.d.ts.map +1 -0
- package/dist/commands/auth-core.js +83 -0
- package/dist/commands/auth-core.js.map +1 -0
- package/dist/commands/auth-forms.d.ts +2 -0
- package/dist/commands/auth-forms.d.ts.map +1 -0
- package/dist/commands/auth-forms.js +15 -0
- package/dist/commands/auth-forms.js.map +1 -0
- package/dist/commands/blocks-options.d.ts +7 -0
- package/dist/commands/blocks-options.d.ts.map +1 -0
- package/dist/commands/blocks-options.js +19 -0
- package/dist/commands/blocks-options.js.map +1 -0
- package/dist/commands/blocks.d.ts +7 -0
- package/dist/commands/blocks.d.ts.map +1 -0
- package/dist/commands/blocks.js +140 -0
- package/dist/commands/blocks.js.map +1 -0
- package/dist/commands/data.d.ts +3 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/data.js +88 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/forms.d.ts +6 -0
- package/dist/commands/forms.d.ts.map +1 -0
- package/dist/commands/forms.js +107 -0
- package/dist/commands/forms.js.map +1 -0
- package/dist/commands/remove-auth-core.d.ts +2 -0
- package/dist/commands/remove-auth-core.d.ts.map +1 -0
- package/dist/commands/remove-auth-core.js +69 -0
- package/dist/commands/remove-auth-core.js.map +1 -0
- package/dist/commands/remove-blocks.d.ts +2 -0
- package/dist/commands/remove-blocks.d.ts.map +1 -0
- package/dist/commands/remove-blocks.js +36 -0
- package/dist/commands/remove-blocks.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/kits/auth-core/README.md +82 -0
- package/dist/kits/auth-core/app/(protected)/dashboard/page.tsx +8 -0
- package/dist/kits/auth-core/app/(protected)/layout.tsx +18 -0
- package/dist/kits/auth-core/app/(protected)/settings/profile/page.tsx +15 -0
- package/dist/kits/auth-core/app/(protected)/settings/profile/profile-form.tsx +114 -0
- package/dist/kits/auth-core/app/api/auth/[...nextauth]/route.ts +1 -0
- package/dist/kits/auth-core/app/api/auth/forgot-password/route.ts +114 -0
- package/dist/kits/auth-core/app/api/auth/providers/route.ts +6 -0
- package/dist/kits/auth-core/app/api/auth/reset-password/route.ts +63 -0
- package/dist/kits/auth-core/app/api/auth/send-verify-email/route.ts +6 -0
- package/dist/kits/auth-core/app/api/signup/route.ts +41 -0
- package/dist/kits/auth-core/app/auth/forgot-password/page.tsx +21 -0
- package/dist/kits/auth-core/app/auth/login/page.tsx +5 -0
- package/dist/kits/auth-core/app/auth/reset-password/page.tsx +187 -0
- package/dist/kits/auth-core/app/auth/signup/page.tsx +5 -0
- package/dist/kits/auth-core/app/auth/verify-email/page.tsx +11 -0
- package/dist/kits/auth-core/components/admin/admin-header.tsx +57 -0
- package/dist/kits/auth-core/components/auth/dashboard.tsx +237 -0
- package/dist/kits/auth-core/components/auth/forgot-password-form.tsx +90 -0
- package/dist/kits/auth-core/components/auth/login-form.tsx +467 -0
- package/dist/kits/auth-core/components/auth/logout-button.tsx +50 -0
- package/dist/kits/auth-core/components/auth/minimal-logout-button.tsx +40 -0
- package/dist/kits/auth-core/components/auth/signup-form.tsx +468 -0
- package/dist/kits/auth-core/components/require-auth.tsx +59 -0
- package/dist/kits/auth-core/components/session-provider.tsx +11 -0
- package/dist/kits/auth-core/components/ui/README.txt +1 -0
- package/dist/kits/auth-core/components/ui/button.tsx +55 -0
- package/dist/kits/auth-core/components/ui/input.tsx +25 -0
- package/dist/kits/auth-core/components/ui/label.tsx +23 -0
- package/dist/kits/auth-core/lib/api/errors.ts +14 -0
- package/dist/kits/auth-core/lib/auth-helpers.ts +29 -0
- package/dist/kits/auth-core/lib/auth.ts +142 -0
- package/dist/kits/auth-core/lib/email/dev-transport.ts +42 -0
- package/dist/kits/auth-core/lib/email/index.ts +28 -0
- package/dist/kits/auth-core/lib/email/provider-smtp.ts +36 -0
- package/dist/kits/auth-core/lib/forms/map-errors.ts +11 -0
- package/dist/kits/auth-core/lib/hash.ts +6 -0
- package/dist/kits/auth-core/lib/prisma.ts +15 -0
- package/dist/kits/auth-core/lib/server/result.ts +45 -0
- package/dist/kits/auth-core/lib/utils.ts +6 -0
- package/dist/kits/auth-core/lib/validation/forms.ts +88 -0
- package/dist/kits/auth-core/package-deps.json +19 -0
- package/dist/kits/auth-core/prisma/auth-models.prisma +81 -0
- package/dist/kits/auth-core/prisma/schema.prisma +81 -0
- package/dist/kits/auth-core/scripts/populate-tokenhash.mjs +26 -0
- package/dist/kits/auth-core/scripts/promote-admin.mjs +33 -0
- package/dist/kits/auth-core/scripts/seed-demo.mjs +40 -0
- package/dist/kits/auth-core/types/next-auth.d.ts +25 -0
- package/dist/kits/blocks/README.md +53 -0
- package/dist/kits/blocks/app/globals.css +175 -0
- package/dist/kits/blocks/app/templates/digitalagency/PresetThemeVars.tsx +80 -0
- package/dist/kits/blocks/app/templates/digitalagency/README.md +36 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/About.tsx +99 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/CTA.tsx +74 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Contact.tsx +227 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Footer.tsx +89 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Hero.tsx +90 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Navbar.tsx +168 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/NetworkPattern.tsx +297 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Portfolio.tsx +157 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Pricing.tsx +114 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Process.tsx +59 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Services.tsx +55 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Team.tsx +28 -0
- package/dist/kits/blocks/app/templates/digitalagency/components/Testimonials.tsx +65 -0
- package/dist/kits/blocks/app/templates/digitalagency/page.tsx +38 -0
- package/dist/kits/blocks/app/templates/gallery/PresetThemeVars.tsx +85 -0
- package/dist/kits/blocks/app/templates/gallery/page.tsx +303 -0
- package/dist/kits/blocks/app/templates/productlaunch/PresetThemeVars.tsx +74 -0
- package/dist/kits/blocks/app/templates/productlaunch/README.md +55 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/About.tsx +178 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/CTA.tsx +93 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Contact.tsx +231 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/FAQ.tsx +93 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Features.tsx +84 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Footer.tsx +132 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Hero.tsx +89 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Navbar.tsx +162 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Pricing.tsx +106 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/ProcessTimeline.tsx +110 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/ServicesGrid.tsx +68 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Team.tsx +104 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/Testimonials.tsx +89 -0
- package/dist/kits/blocks/app/templates/productlaunch/components/TrustBadges.tsx +76 -0
- package/dist/kits/blocks/app/templates/productlaunch/page.tsx +45 -0
- package/dist/kits/blocks/app/templates/saasdashboard/PresetThemeVars.tsx +80 -0
- package/dist/kits/blocks/app/templates/saasdashboard/README.md +38 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Contact.tsx +176 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Dashboard.tsx +293 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/FAQ.tsx +55 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Features.tsx +91 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Footer.tsx +77 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero.tsx +105 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Hero_mask.tsx +127 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Navbar.tsx +159 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Pricing.tsx +90 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/SmoothScroll.tsx +97 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/Testimonials.tsx +72 -0
- package/dist/kits/blocks/app/templates/saasdashboard/components/TrustBadges.tsx +53 -0
- package/dist/kits/blocks/app/templates/saasdashboard/page.tsx +39 -0
- package/dist/kits/blocks/components/app-providers.tsx +1 -0
- package/dist/kits/blocks/components/enhanced-theme-provider.tsx +195 -0
- package/dist/kits/blocks/components/sections/About.tsx +291 -0
- package/dist/kits/blocks/components/sections/CTA.tsx +258 -0
- package/dist/kits/blocks/components/sections/Contact.tsx +267 -0
- package/dist/kits/blocks/components/sections/FAQ.tsx +226 -0
- package/dist/kits/blocks/components/sections/Features.tsx +269 -0
- package/dist/kits/blocks/components/sections/Footer.tsx +302 -0
- package/dist/kits/blocks/components/sections/HeroMotion.tsx +307 -0
- package/dist/kits/blocks/components/sections/HeroOverlay.tsx +358 -0
- package/dist/kits/blocks/components/sections/HeroSplit.tsx +352 -0
- package/dist/kits/blocks/components/sections/Navbar.tsx +353 -0
- package/dist/kits/blocks/components/sections/Newsletter.tsx +156 -0
- package/dist/kits/blocks/components/sections/PortfolioSimple.tsx +550 -0
- package/dist/kits/blocks/components/sections/Pricing.tsx +264 -0
- package/dist/kits/blocks/components/sections/ProcessTimeline.tsx +325 -0
- package/dist/kits/blocks/components/sections/ServicesGrid.tsx +210 -0
- package/dist/kits/blocks/components/sections/Team.tsx +309 -0
- package/dist/kits/blocks/components/sections/Testimonials.tsx +158 -0
- package/dist/kits/blocks/components/sections/TrustBadges.tsx +162 -0
- package/dist/kits/blocks/components/theme-provider.tsx +34 -0
- package/dist/kits/blocks/components/ui/alert-dialog.tsx +134 -0
- package/dist/kits/blocks/components/ui/brand-node.tsx +121 -0
- package/dist/kits/blocks/components/ui/button.tsx +122 -0
- package/dist/kits/blocks/components/ui/button_bck.tsx +93 -0
- package/dist/kits/blocks/components/ui/card.tsx +95 -0
- package/dist/kits/blocks/components/ui/checkbox.tsx +30 -0
- package/dist/kits/blocks/components/ui/cta-button.tsx +125 -0
- package/dist/kits/blocks/components/ui/dropdown-menu.tsx +201 -0
- package/dist/kits/blocks/components/ui/feature-card.tsx +91 -0
- package/dist/kits/blocks/components/ui/input.tsx +27 -0
- package/dist/kits/blocks/components/ui/label.tsx +29 -0
- package/dist/kits/blocks/components/ui/pricing-card.tsx +120 -0
- package/dist/kits/blocks/components/ui/select.tsx +25 -0
- package/dist/kits/blocks/components/ui/skeleton.tsx +13 -0
- package/dist/kits/blocks/components/ui/switch.tsx +78 -0
- package/dist/kits/blocks/components/ui/table.tsx +98 -0
- package/dist/kits/blocks/components/ui/testimonial-card.tsx +108 -0
- package/dist/kits/blocks/components/ui/textarea.tsx +26 -0
- package/dist/kits/blocks/components/ui/theme-selector.tsx +247 -0
- package/dist/kits/blocks/components/ui/theme-toggle.tsx +74 -0
- package/dist/kits/blocks/components/ui/toaster.tsx +7 -0
- package/dist/kits/blocks/lib/themes.ts +399 -0
- package/dist/kits/blocks/lib/themes_old.ts +37 -0
- package/dist/kits/blocks/lib/utils.ts +9 -0
- package/dist/kits/blocks/next.config.ts +11 -0
- package/dist/kits/blocks/notes/THEME_GUIDE.md +29 -0
- package/dist/kits/blocks/notes/THEMING_CONVERSION_SUMMARY.md +14 -0
- package/dist/kits/blocks/package-deps.json +22 -0
- package/dist/kits/blocks/public/placeholders/gallery/hero-pexels-broken-9945014.avif +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626431.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626432.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626434.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/gallery/pexels-googledeepmind-25626436.jpg +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_1.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_2.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_3.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/feature_4.png +0 -0
- package/dist/kits/blocks/public/placeholders/product_launch/hero.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/analytics.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/chat.png +0 -0
- package/dist/kits/blocks/public/placeholders/saas_dashboard/projectBoard.png +0 -0
- package/dist/kits/data/.gitkeep +0 -0
- package/dist/kits/data/README.md +80 -0
- package/dist/kits/data/app/(protected)/admin/posts/page.tsx +5 -0
- package/dist/kits/data/app/(protected)/admin/users/page.tsx +5 -0
- package/dist/kits/data/app/api/posts/[id]/route.ts +83 -0
- package/dist/kits/data/app/api/posts/route.ts +138 -0
- package/dist/kits/data/app/api/seed-demo/route.ts +45 -0
- package/dist/kits/data/app/api/users/[id]/route.ts +127 -0
- package/dist/kits/data/app/api/users/check-email/route.ts +18 -0
- package/dist/kits/data/app/api/users/check-unique/route.ts +27 -0
- package/dist/kits/data/app/api/users/route.ts +79 -0
- package/dist/kits/data/app/examples/demo/README.md +4 -0
- package/dist/kits/data/app/examples/demo/create-post-form.tsx +106 -0
- package/dist/kits/data/app/examples/demo/page.tsx +118 -0
- package/dist/kits/data/app/examples/demo/seed-demo-button.tsx +37 -0
- package/dist/kits/data/components/admin/posts-manager.tsx +719 -0
- package/dist/kits/data/components/admin/users-manager.tsx +432 -0
- package/dist/kits/data/lib/prisma.ts +15 -0
- package/dist/kits/data/lib/server/result.ts +90 -0
- package/dist/kits/data/package-deps.json +11 -0
- package/dist/kits/data/scripts/seed-demo.mjs +41 -0
- package/dist/kits/forms/.gitkeep +0 -0
- package/dist/kits/forms/README.md +49 -0
- package/dist/kits/forms/app/.gitkeep +0 -0
- package/dist/kits/forms/app/api/wizard/route.ts +71 -0
- package/dist/kits/forms/app/examples/forms/basic/page.tsx +124 -0
- package/dist/kits/forms/app/examples/forms/server-action/form-client.tsx +28 -0
- package/dist/kits/forms/app/examples/forms/server-action/page.tsx +71 -0
- package/dist/kits/forms/app/examples/forms/wizard/page.tsx +15 -0
- package/dist/kits/forms/app/examples/forms/wizard/wizard-client.tsx +2 -0
- package/dist/kits/forms/components/.gitkeep +0 -0
- package/dist/kits/forms/components/examples/wizard-client.tsx +231 -0
- package/dist/kits/forms/components/hooks/useCheckUnique.ts +79 -0
- package/dist/kits/forms/components/ui/button.tsx +122 -0
- package/dist/kits/forms/components/ui/checkbox.tsx +30 -0
- package/dist/kits/forms/components/ui/form/context.ts +33 -0
- package/dist/kits/forms/components/ui/form/form-control.tsx +28 -0
- package/dist/kits/forms/components/ui/form/form-description.tsx +22 -0
- package/dist/kits/forms/components/ui/form/form-field.tsx +36 -0
- package/dist/kits/forms/components/ui/form/form-item.tsx +21 -0
- package/dist/kits/forms/components/ui/form/form-label.tsx +24 -0
- package/dist/kits/forms/components/ui/form/form-message.tsx +29 -0
- package/dist/kits/forms/components/ui/form/form.tsx +26 -0
- package/dist/kits/forms/components/ui/input.tsx +27 -0
- package/dist/kits/forms/components/ui/label.tsx +29 -0
- package/dist/kits/forms/components/ui/select.tsx +25 -0
- package/dist/kits/forms/components/ui/switch.tsx +78 -0
- package/dist/kits/forms/components/ui/textarea.tsx +26 -0
- package/dist/kits/forms/lib/.gitkeep +0 -0
- package/dist/kits/forms/lib/forms/map-errors.ts +29 -0
- package/dist/kits/forms/lib/prisma.ts +16 -0
- package/dist/kits/forms/lib/utils.ts +9 -0
- package/dist/kits/forms/lib/validation/forms.ts +88 -0
- package/dist/kits/forms/lib/validation/wizard.ts +32 -0
- package/dist/kits/forms/package-deps.json +17 -0
- package/dist/utils/file-operations.d.ts +18 -0
- package/dist/utils/file-operations.d.ts.map +1 -0
- package/dist/utils/file-operations.js +327 -0
- package/dist/utils/file-operations.js.map +1 -0
- package/dist/utils/installation-tracker.d.ts +26 -0
- package/dist/utils/installation-tracker.d.ts.map +1 -0
- package/dist/utils/installation-tracker.js +98 -0
- package/dist/utils/installation-tracker.js.map +1 -0
- package/package.json +51 -21
- package/index.js +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { prisma } from "@/lib/prisma";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { postSchema } from "@/lib/validation/forms";
|
|
4
|
+
import { jsonOk, jsonFail, jsonFromZod } from "@/lib/server/result";
|
|
5
|
+
import { getServerSession } from "next-auth";
|
|
6
|
+
import { authOptions } from "@/lib/auth";
|
|
7
|
+
|
|
8
|
+
// Ensure Prisma runs in Node.js (not Edge)
|
|
9
|
+
export const runtime = "nodejs";
|
|
10
|
+
|
|
11
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
12
|
+
|
|
13
|
+
export async function PUT(req: Request, { params }: RouteContext) {
|
|
14
|
+
try {
|
|
15
|
+
const { id } = await params;
|
|
16
|
+
|
|
17
|
+
// Require authenticated session
|
|
18
|
+
const session = await getServerSession(authOptions);
|
|
19
|
+
if (!session?.user) {
|
|
20
|
+
return jsonFail("Unauthorized", { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Only admins or the post author may update
|
|
24
|
+
const post = await prisma.post.findUnique({ where: { id } });
|
|
25
|
+
if (!post) return jsonFail("Not found", { status: 404 });
|
|
26
|
+
|
|
27
|
+
const isAdmin = (session.user as { role?: string }).role === "admin";
|
|
28
|
+
const isAuthor = (session.user as { id?: string }).id === post.authorId;
|
|
29
|
+
if (!isAdmin && !isAuthor) {
|
|
30
|
+
return jsonFail("Forbidden", { status: 403 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate only the fields we allow to update. Make them optional so partial updates (e.g. { published: true }) work.
|
|
34
|
+
const updateSchema = postSchema
|
|
35
|
+
.pick({ title: true, content: true, published: true })
|
|
36
|
+
.partial();
|
|
37
|
+
const body = await req.json();
|
|
38
|
+
const data = updateSchema.parse(body);
|
|
39
|
+
|
|
40
|
+
// Build Prisma update object only with provided fields to avoid overwriting with undefined
|
|
41
|
+
const updateData: any = {};
|
|
42
|
+
if (data.title !== undefined) updateData.title = data.title;
|
|
43
|
+
if (data.content !== undefined) updateData.content = data.content ?? null;
|
|
44
|
+
if (data.published !== undefined) updateData.published = data.published;
|
|
45
|
+
|
|
46
|
+
const updated = await prisma.post.update({
|
|
47
|
+
where: { id },
|
|
48
|
+
data: updateData,
|
|
49
|
+
});
|
|
50
|
+
return jsonOk(updated, { status: 200, message: "Post updated" });
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
if (error instanceof z.ZodError) {
|
|
53
|
+
return jsonFromZod(error, { status: 400, message: "Validation failed" });
|
|
54
|
+
}
|
|
55
|
+
return jsonFail("Failed to update post", { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function DELETE(_req: Request, { params }: RouteContext) {
|
|
60
|
+
try {
|
|
61
|
+
const { id } = await params; // await params
|
|
62
|
+
|
|
63
|
+
const session = await getServerSession(authOptions);
|
|
64
|
+
if (!session?.user) {
|
|
65
|
+
return jsonFail("Unauthorized", { status: 401 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const post = await prisma.post.findUnique({ where: { id } });
|
|
69
|
+
if (!post) return jsonFail("Not found", { status: 404 });
|
|
70
|
+
|
|
71
|
+
const isAdmin = (session.user as { role?: string }).role === "admin";
|
|
72
|
+
const isAuthor = (session.user as { id?: string }).id === post.authorId;
|
|
73
|
+
if (!isAdmin && !isAuthor) {
|
|
74
|
+
return jsonFail("Forbidden", { status: 403 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await prisma.post.delete({ where: { id } }); // If Int: id: Number(id)
|
|
78
|
+
return jsonOk({ ok: true }, { status: 200, message: "Post deleted" });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error("DELETE /api/posts/[id] error:", e);
|
|
81
|
+
return jsonFail("Failed to delete post", { status: 500 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ZodError, z } from "zod";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { postSchema } from "@/lib/validation/forms";
|
|
5
|
+
import { getServerSession } from "next-auth";
|
|
6
|
+
import { authOptions } from "@/lib/auth";
|
|
7
|
+
import { jsonOk, jsonFail, jsonFromZod } from "@/lib/server/result";
|
|
8
|
+
|
|
9
|
+
// Ensure Prisma runs in Node.js (not Edge)
|
|
10
|
+
export const runtime = "nodejs";
|
|
11
|
+
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(req.url);
|
|
15
|
+
const page = Math.max(1, Number(url.searchParams.get("page") ?? "1"));
|
|
16
|
+
const perPage = Math.min(
|
|
17
|
+
200,
|
|
18
|
+
Math.max(1, Number(url.searchParams.get("perPage") ?? "8")),
|
|
19
|
+
);
|
|
20
|
+
const q = url.searchParams.get("q") ?? undefined;
|
|
21
|
+
const sort = url.searchParams.get("sort") ?? "createdAt_desc";
|
|
22
|
+
const sortField = url.searchParams.get("sortField") ?? "createdAt";
|
|
23
|
+
const sortDir =
|
|
24
|
+
(url.searchParams.get("sortDir") as "asc" | "desc") ?? "desc";
|
|
25
|
+
|
|
26
|
+
const skip = (page - 1) * perPage;
|
|
27
|
+
|
|
28
|
+
const where: any = {};
|
|
29
|
+
if (q) {
|
|
30
|
+
where.title = { contains: q, mode: "insensitive" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Filter by published state if requested: published=published|draft
|
|
34
|
+
const publishedParam = url.searchParams.get("published");
|
|
35
|
+
if (publishedParam === "published") {
|
|
36
|
+
where.published = true;
|
|
37
|
+
} else if (publishedParam === "draft") {
|
|
38
|
+
where.published = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const orderBy: any = {};
|
|
42
|
+
// Allow server to order by title or createdAt for now. Defaults to createdAt
|
|
43
|
+
if (sortField === "title") {
|
|
44
|
+
orderBy.title = sortDir === "asc" ? "asc" : "desc";
|
|
45
|
+
} else {
|
|
46
|
+
orderBy.createdAt = sortDir === "asc" ? "asc" : "desc";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [total, items] = await Promise.all([
|
|
50
|
+
prisma.post.count({ where }),
|
|
51
|
+
prisma.post.findMany({
|
|
52
|
+
where,
|
|
53
|
+
orderBy,
|
|
54
|
+
skip,
|
|
55
|
+
take: perPage,
|
|
56
|
+
include: { author: { select: { name: true, email: true } } },
|
|
57
|
+
}),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
return jsonOk({ items, total, page, perPage });
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
console.error("GET /api/posts error:", error);
|
|
63
|
+
return jsonFail("Failed to fetch posts", { status: 500 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a new post.
|
|
69
|
+
* DX: authorId is optional. If omitted, we default to the currently signed-in user.
|
|
70
|
+
* - If authorId is provided in the payload, it is used as-is.
|
|
71
|
+
* - Otherwise we read the NextAuth session and use session.user.id.
|
|
72
|
+
* - If neither is available (no session and no authorId), we return 400 to preserve data integrity.
|
|
73
|
+
*/
|
|
74
|
+
export async function POST(req: Request) {
|
|
75
|
+
try {
|
|
76
|
+
// Allow authenticated users to create posts. Admins may create on behalf of others.
|
|
77
|
+
const session = await getServerSession(authOptions);
|
|
78
|
+
if (!session?.user) {
|
|
79
|
+
return jsonFail("Not authenticated", { status: 401 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1) Parse and validate the request body. Server-side we allow authorId to be blank/omitted
|
|
83
|
+
// because we can infer it from the authenticated session.
|
|
84
|
+
const body = await req.json();
|
|
85
|
+
const serverPostSchema = postSchema.extend({
|
|
86
|
+
// Make authorId optional for this API (UI may still require it via postSchema)
|
|
87
|
+
authorId: postSchema.shape.authorId.optional().or(z.literal("")),
|
|
88
|
+
});
|
|
89
|
+
const data = serverPostSchema.parse(body);
|
|
90
|
+
|
|
91
|
+
// 2) Resolve the authorId: prefer explicit authorId (only if admin or same as session user), else default to current session user
|
|
92
|
+
const providedAuthorId =
|
|
93
|
+
data.authorId && data.authorId.length > 0 ? data.authorId : undefined;
|
|
94
|
+
|
|
95
|
+
const sessionUserId = (session.user as { id?: string } | undefined)?.id;
|
|
96
|
+
const sessionIsAdmin =
|
|
97
|
+
(session.user as { role?: string } | undefined)?.role === "admin";
|
|
98
|
+
|
|
99
|
+
if (providedAuthorId) {
|
|
100
|
+
// If a non-admin tries to set someone else as the author, reject for safety.
|
|
101
|
+
if (!sessionIsAdmin && providedAuthorId !== sessionUserId) {
|
|
102
|
+
return jsonFail("Forbidden: cannot set authorId to another user", {
|
|
103
|
+
status: 403,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resolvedAuthorId = providedAuthorId ?? sessionUserId;
|
|
109
|
+
|
|
110
|
+
if (!resolvedAuthorId) {
|
|
111
|
+
return jsonFail(
|
|
112
|
+
"authorId is required. Provide an authorId or sign in so we can default to your user id.",
|
|
113
|
+
{
|
|
114
|
+
status: 400,
|
|
115
|
+
errors: {
|
|
116
|
+
authorId: "Author ID is required or sign in to use your user ID",
|
|
117
|
+
},
|
|
118
|
+
code: "MISSING_AUTHOR",
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const created = await prisma.post.create({
|
|
124
|
+
data: {
|
|
125
|
+
title: data.title,
|
|
126
|
+
content: data.content || null,
|
|
127
|
+
authorId: resolvedAuthorId,
|
|
128
|
+
published: data.published ?? false,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return jsonOk(created, { status: 201, message: "Post created" });
|
|
132
|
+
} catch (error: unknown) {
|
|
133
|
+
if (error instanceof ZodError) {
|
|
134
|
+
return jsonFromZod(error, { status: 400, message: "Validation failed" });
|
|
135
|
+
}
|
|
136
|
+
return jsonFail("Failed to create post", { status: 500 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
4
|
+
import { getServerSession } from "next-auth";
|
|
5
|
+
import { authOptions } from "@/lib/auth";
|
|
6
|
+
|
|
7
|
+
export const runtime = "nodejs";
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await getServerSession(authOptions);
|
|
12
|
+
if (!session?.user?.id) {
|
|
13
|
+
return jsonFail("Not authenticated", { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const userId = session.user.id;
|
|
17
|
+
|
|
18
|
+
// Create 3 sample posts for the current user
|
|
19
|
+
const posts = await prisma.post.createMany({
|
|
20
|
+
data: [
|
|
21
|
+
{
|
|
22
|
+
title: "Welcome to Nextworks",
|
|
23
|
+
content: "This is a seeded post.",
|
|
24
|
+
authorId: userId,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "Seeded Demo Post",
|
|
28
|
+
content: "Another seeded post for demo purposes.",
|
|
29
|
+
authorId: userId,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "Try editing me",
|
|
33
|
+
content: "Edit/delete this post to test the CRUD flows.",
|
|
34
|
+
authorId: userId,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
skipDuplicates: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return jsonOk({ created: posts }, { message: "Demo data seeded" });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(e);
|
|
43
|
+
return jsonFail("Failed to seed demo data");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import type { Prisma } from "@prisma/client";
|
|
4
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
5
|
+
import { getServerSession } from "next-auth";
|
|
6
|
+
import { authOptions } from "@/lib/auth";
|
|
7
|
+
|
|
8
|
+
// Ensure Prisma runs in Node.js (not Edge)
|
|
9
|
+
export const runtime = "nodejs";
|
|
10
|
+
|
|
11
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
12
|
+
|
|
13
|
+
// Type guards/helpers (kept minimal here)
|
|
14
|
+
const hasErrorCode = (e: unknown, code: string): boolean =>
|
|
15
|
+
typeof e === "object" &&
|
|
16
|
+
e !== null &&
|
|
17
|
+
"code" in e &&
|
|
18
|
+
typeof (e as { code?: unknown }).code === "string" &&
|
|
19
|
+
(e as { code: string }).code === code;
|
|
20
|
+
|
|
21
|
+
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
|
22
|
+
try {
|
|
23
|
+
const { id } = await params; // async params in Next.js 15
|
|
24
|
+
|
|
25
|
+
const user = await prisma.user.findUnique({
|
|
26
|
+
where: { id },
|
|
27
|
+
select: { id: true, name: true, email: true, role: true },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!user) {
|
|
31
|
+
return jsonFail("Not found", { status: 404 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return jsonOk(user);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("GET /api/users/[id] error:", e);
|
|
37
|
+
return jsonFail("Failed to fetch user", { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function PUT(req: NextRequest, { params }: RouteContext) {
|
|
42
|
+
try {
|
|
43
|
+
// Only admins may update other users. Allow self-update as well.
|
|
44
|
+
const session = await getServerSession(authOptions);
|
|
45
|
+
if (!session?.user) return jsonFail("Unauthorized", { status: 401 });
|
|
46
|
+
|
|
47
|
+
const { id } = await params;
|
|
48
|
+
const isAdmin = (session.user as { role?: string }).role === "admin";
|
|
49
|
+
const isSelf = (session.user as { id?: string }).id === id;
|
|
50
|
+
if (!isAdmin && !isSelf) return jsonFail("Forbidden", { status: 403 });
|
|
51
|
+
|
|
52
|
+
const body: unknown = await req.json();
|
|
53
|
+
if (typeof body !== "object" || body === null) {
|
|
54
|
+
return jsonFail("Body must be a JSON object", { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate with Zod (imported lazily to keep this route light)
|
|
58
|
+
const { userUpdateSchema } = await import("@/lib/validation/forms");
|
|
59
|
+
try {
|
|
60
|
+
const parsed = userUpdateSchema.parse(body);
|
|
61
|
+
|
|
62
|
+
// Build Prisma-compatible update object
|
|
63
|
+
const data: Prisma.UserUpdateInput = {};
|
|
64
|
+
if (parsed.name !== undefined)
|
|
65
|
+
data.name = parsed.name === "" ? null : parsed.name;
|
|
66
|
+
if (parsed.email !== undefined) data.email = parsed.email;
|
|
67
|
+
if (parsed.image !== undefined)
|
|
68
|
+
data.image = parsed.image === "" ? null : parsed.image;
|
|
69
|
+
if (parsed.password !== undefined) {
|
|
70
|
+
// Hash password before saving
|
|
71
|
+
const { hashPassword } = await import("@/lib/hash");
|
|
72
|
+
data.password = await hashPassword(parsed.password as string);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.emailVerified !== undefined)
|
|
75
|
+
data.emailVerified = parsed.emailVerified as any;
|
|
76
|
+
|
|
77
|
+
const updated = await prisma.user.update({
|
|
78
|
+
where: { id },
|
|
79
|
+
data,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return jsonOk(updated, { status: 200, message: "User updated" });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Zod errors
|
|
85
|
+
if (err && typeof err === "object" && "issues" in (err as any)) {
|
|
86
|
+
const { jsonFromZod } = await import("@/lib/server/result");
|
|
87
|
+
return jsonFromZod(err as any, {
|
|
88
|
+
status: 400,
|
|
89
|
+
message: "Validation failed",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error("PUT /api/users/[id] error:", e);
|
|
96
|
+
if (hasErrorCode(e, "P2025")) {
|
|
97
|
+
return jsonFail("Not found", { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
return jsonFail("Failed to update user", { status: 500 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function DELETE(_req: NextRequest, { params }: RouteContext) {
|
|
104
|
+
try {
|
|
105
|
+
const { requireAdminApi } = await import("@/lib/auth-helpers");
|
|
106
|
+
const session = await requireAdminApi();
|
|
107
|
+
if (!session) return jsonFail("Forbidden", { status: 403 });
|
|
108
|
+
|
|
109
|
+
const { id } = await params;
|
|
110
|
+
|
|
111
|
+
// Clean up dependent records first to avoid FK violations
|
|
112
|
+
await prisma.$transaction([
|
|
113
|
+
prisma.account.deleteMany({ where: { userId: id } }),
|
|
114
|
+
prisma.session.deleteMany({ where: { userId: id } }),
|
|
115
|
+
prisma.post.deleteMany({ where: { authorId: id } }),
|
|
116
|
+
prisma.user.delete({ where: { id } }),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return jsonOk({ ok: true }, { status: 200, message: "User deleted" });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error("DELETE /api/users/[id] error:", e);
|
|
122
|
+
if (hasErrorCode(e, "P2025")) {
|
|
123
|
+
return jsonFail("Not found", { status: 404 });
|
|
124
|
+
}
|
|
125
|
+
return jsonFail("Failed to delete user", { status: 500 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export async function POST(req: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const body = await req.json().catch(() => null);
|
|
10
|
+
const email = body?.email;
|
|
11
|
+
if (!email) return jsonFail("Missing email", { status: 400 });
|
|
12
|
+
|
|
13
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
14
|
+
return jsonOk({ exists: !!user });
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return jsonFail("Failed to check email", { status: 500 });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
4
|
+
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export async function POST(req: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const body = await req.json().catch(() => null);
|
|
10
|
+
const field = body?.field;
|
|
11
|
+
const value = body?.value;
|
|
12
|
+
if (!field || !value) return jsonFail("Missing field or value", { status: 400 });
|
|
13
|
+
|
|
14
|
+
// Only allow specific fields for security
|
|
15
|
+
if (!["email", "username"].includes(field)) {
|
|
16
|
+
return jsonFail("Unsupported field", { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let where: any = {};
|
|
20
|
+
where[field] = value;
|
|
21
|
+
|
|
22
|
+
const user = await prisma.user.findUnique({ where });
|
|
23
|
+
return jsonOk({ unique: !user });
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return jsonFail("Failed to check uniqueness", { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { userSchema } from "@/lib/validation/forms";
|
|
4
|
+
import { getServerSession } from "next-auth";
|
|
5
|
+
import { authOptions } from "@/lib/auth";
|
|
6
|
+
import {
|
|
7
|
+
jsonOk,
|
|
8
|
+
jsonFail,
|
|
9
|
+
jsonFromZod,
|
|
10
|
+
jsonFromPrisma,
|
|
11
|
+
} from "@/lib/server/result";
|
|
12
|
+
|
|
13
|
+
export const runtime = "nodejs";
|
|
14
|
+
|
|
15
|
+
export async function GET(req: Request) {
|
|
16
|
+
// Restrict listing users to admins only to avoid leaking emails/passwords.
|
|
17
|
+
const { requireAdminApi } = await import("@/lib/auth-helpers");
|
|
18
|
+
const session = await requireAdminApi();
|
|
19
|
+
if (!session) {
|
|
20
|
+
return jsonFail("Forbidden", { status: 403 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Simple server-side paging: ?page=1&perPage=20
|
|
24
|
+
const url = new URL(req.url);
|
|
25
|
+
const page = Math.max(1, Number(url.searchParams.get("page") ?? "1"));
|
|
26
|
+
const perPage = Math.min(
|
|
27
|
+
200,
|
|
28
|
+
Math.max(1, Number(url.searchParams.get("perPage") ?? "20")),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const skip = (page - 1) * perPage;
|
|
32
|
+
|
|
33
|
+
// Return only safe public fields for users (no password, tokens, etc.)
|
|
34
|
+
const [total, items] = await Promise.all([
|
|
35
|
+
prisma.user.count(),
|
|
36
|
+
prisma.user.findMany({
|
|
37
|
+
orderBy: { createdAt: "desc" },
|
|
38
|
+
skip,
|
|
39
|
+
take: perPage,
|
|
40
|
+
select: {
|
|
41
|
+
id: true,
|
|
42
|
+
email: true,
|
|
43
|
+
name: true,
|
|
44
|
+
image: true,
|
|
45
|
+
role: true,
|
|
46
|
+
createdAt: true,
|
|
47
|
+
emailVerified: true,
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
return jsonOk({ items, total, page, perPage });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function POST(req: Request) {
|
|
56
|
+
try {
|
|
57
|
+
// Only admins may create users via admin API
|
|
58
|
+
const { requireAdminApi } = await import("@/lib/auth-helpers");
|
|
59
|
+
const session = await requireAdminApi();
|
|
60
|
+
if (!session) {
|
|
61
|
+
return jsonFail("Forbidden", { status: 403 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const body = await req.json();
|
|
65
|
+
const data = userSchema.parse(body);
|
|
66
|
+
const created = await prisma.user.create({
|
|
67
|
+
data: { email: data.email, name: data.name || null },
|
|
68
|
+
});
|
|
69
|
+
return jsonOk(created, { status: 201, message: "User created" });
|
|
70
|
+
} catch (error: unknown) {
|
|
71
|
+
if (error instanceof ZodError) {
|
|
72
|
+
return jsonFromZod(error, { status: 400, message: "Validation failed" });
|
|
73
|
+
}
|
|
74
|
+
if ((error as unknown as { code?: string })?.code === "P2002") {
|
|
75
|
+
return jsonFromPrisma(error);
|
|
76
|
+
}
|
|
77
|
+
return jsonFail("Failed to create user", { status: 500 });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
+
import { postSchema } from "@/lib/validation/forms";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { useRouter } from "next/navigation";
|
|
9
|
+
import { Form } from "@/components/ui/form/form";
|
|
10
|
+
import { FormField } from "@/components/ui/form/form-field";
|
|
11
|
+
import { FormItem } from "@/components/ui/form/form-item";
|
|
12
|
+
import { FormLabel } from "@/components/ui/form/form-label";
|
|
13
|
+
import { FormControl } from "@/components/ui/form/form-control";
|
|
14
|
+
import { FormMessage } from "@/components/ui/form/form-message";
|
|
15
|
+
import { Input } from "@/components/ui/input";
|
|
16
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
|
|
19
|
+
import { toast } from "sonner";
|
|
20
|
+
|
|
21
|
+
import { useSession } from "next-auth/react";
|
|
22
|
+
|
|
23
|
+
export default function CreatePostForm() {
|
|
24
|
+
// Relax the schema for this demo form: authorId is optional here
|
|
25
|
+
const postFormSchema = postSchema.extend({
|
|
26
|
+
authorId: postSchema.shape.authorId.optional().or(z.literal("")),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const methods = useForm({
|
|
30
|
+
resolver: zodResolver(postFormSchema),
|
|
31
|
+
defaultValues: { title: "", content: "" },
|
|
32
|
+
});
|
|
33
|
+
const { control, handleSubmit, reset } = methods;
|
|
34
|
+
|
|
35
|
+
const session = useSession();
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
|
|
38
|
+
const onSubmit = async (values: any) => {
|
|
39
|
+
if (session.status !== "authenticated") {
|
|
40
|
+
toast.error(
|
|
41
|
+
"You must be signed in to create a post. Use the Sign up / Log in links above.",
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch("/api/posts", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(values),
|
|
50
|
+
});
|
|
51
|
+
const payload = await res.json().catch(() => null);
|
|
52
|
+
if (!res.ok || !payload?.success) {
|
|
53
|
+
const msg = payload ? mapApiErrorsToForm(methods, payload) : undefined;
|
|
54
|
+
toast.error(msg || payload?.message || "Create failed");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
reset();
|
|
58
|
+
toast.success("Post created");
|
|
59
|
+
// Refresh the server-rendered list below
|
|
60
|
+
router.refresh();
|
|
61
|
+
} catch (e) {
|
|
62
|
+
toast.error("Create failed");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="bg-card rounded-md p-6">
|
|
68
|
+
<h3 className="mb-3 text-lg font-semibold">
|
|
69
|
+
Create a post (requires sign in)
|
|
70
|
+
</h3>
|
|
71
|
+
<Form methods={methods}>
|
|
72
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
|
|
73
|
+
<FormField
|
|
74
|
+
control={control}
|
|
75
|
+
name="title"
|
|
76
|
+
render={({ field }) => (
|
|
77
|
+
<FormItem>
|
|
78
|
+
<FormLabel>Title</FormLabel>
|
|
79
|
+
<FormControl>
|
|
80
|
+
<Input {...field} />
|
|
81
|
+
</FormControl>
|
|
82
|
+
<FormMessage />
|
|
83
|
+
</FormItem>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<FormField
|
|
88
|
+
control={control}
|
|
89
|
+
name="content"
|
|
90
|
+
render={({ field }) => (
|
|
91
|
+
<FormItem>
|
|
92
|
+
<FormLabel>Content</FormLabel>
|
|
93
|
+
<FormControl>
|
|
94
|
+
<Textarea {...field} rows={4} />
|
|
95
|
+
</FormControl>
|
|
96
|
+
<FormMessage />
|
|
97
|
+
</FormItem>
|
|
98
|
+
)}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<Button type="submit">Create Post</Button>
|
|
102
|
+
</form>
|
|
103
|
+
</Form>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|