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,63 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
4
|
+
import { hash } from "bcryptjs";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
10
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
let body: unknown;
|
|
13
|
+
try {
|
|
14
|
+
body = await req.json();
|
|
15
|
+
} catch {
|
|
16
|
+
return NextResponse.json({ message: "Invalid request" }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const parsed = resetPasswordSchema.parse(body);
|
|
21
|
+
const { token, password } = parsed;
|
|
22
|
+
|
|
23
|
+
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
24
|
+
const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
|
|
25
|
+
if (!pr) {
|
|
26
|
+
return NextResponse.json({ message: "Invalid or expired token" }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
if ((pr as any).used) {
|
|
29
|
+
return NextResponse.json({ message: "Token already used" }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
if (pr.expires < new Date()) {
|
|
32
|
+
return NextResponse.json({ message: "Token expired" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hashed = await hash(password, 10);
|
|
36
|
+
await prisma.user.update({ where: { id: pr.userId }, data: { password: hashed } });
|
|
37
|
+
await prisma.passwordReset.update({ where: { id: pr.id }, data: { used: true } });
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ message: "Password updated" });
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
if (err instanceof z.ZodError) {
|
|
42
|
+
return NextResponse.json({ message: "Validation failed", errors: (err as z.ZodError).issues }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
return NextResponse.json({ message: "Failed" }, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function GET(req: Request) {
|
|
49
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
50
|
+
return NextResponse.json({ message: "Not found" }, { status: 404 });
|
|
51
|
+
}
|
|
52
|
+
const url = new URL(req.url);
|
|
53
|
+
const token = url.searchParams.get("token");
|
|
54
|
+
if (!token) return NextResponse.json({ valid: false }, { status: 400 });
|
|
55
|
+
|
|
56
|
+
const tokenHash = createHash("sha256").update(token).digest("hex");
|
|
57
|
+
const pr = await prisma.passwordReset.findFirst({ where: { tokenHash } });
|
|
58
|
+
if (!pr) return NextResponse.json({ valid: false });
|
|
59
|
+
if ((pr as any).used) return NextResponse.json({ valid: false });
|
|
60
|
+
if (pr.expires < new Date()) return NextResponse.json({ valid: false });
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({ valid: true });
|
|
63
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { hash } from "bcryptjs";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import { signupSchema } from "@/lib/validation/forms";
|
|
5
|
+
import { ZodError } from "zod";
|
|
6
|
+
import { jsonOk, jsonFail, jsonFromZod } from "@/lib/server/result";
|
|
7
|
+
|
|
8
|
+
export const runtime = "nodejs";
|
|
9
|
+
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await req.json();
|
|
13
|
+
const data = signupSchema.parse(body);
|
|
14
|
+
|
|
15
|
+
const existing = await prisma.user.findUnique({
|
|
16
|
+
where: { email: data.email },
|
|
17
|
+
});
|
|
18
|
+
if (existing) {
|
|
19
|
+
return jsonFail("Email already in use", {
|
|
20
|
+
status: 409,
|
|
21
|
+
errors: { email: "Email already in use" },
|
|
22
|
+
code: "EMAIL_EXISTS",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hashed = await hash(data.password, 10);
|
|
27
|
+
await prisma.user.create({
|
|
28
|
+
data: { name: data.name, email: data.email, password: hashed },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return jsonOk(undefined, {
|
|
32
|
+
status: 201,
|
|
33
|
+
message: "User created successfully",
|
|
34
|
+
});
|
|
35
|
+
} catch (error: unknown) {
|
|
36
|
+
if (error instanceof ZodError) {
|
|
37
|
+
return jsonFromZod(error, { status: 400, message: "Validation failed" });
|
|
38
|
+
}
|
|
39
|
+
return jsonFail("Error during signup", { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ForgotPasswordForm from "@/components/auth/forgot-password-form";
|
|
3
|
+
|
|
4
|
+
export default function ForgotPasswordPage() {
|
|
5
|
+
// Guard rendered server-side: the feature is opt-in via NEXTWORKS_ENABLE_PASSWORD_RESET=1
|
|
6
|
+
if (process.env.NEXTWORKS_ENABLE_PASSWORD_RESET !== "1") {
|
|
7
|
+
return (
|
|
8
|
+
<div className="mx-auto w-full max-w-md pt-6">
|
|
9
|
+
<h2 className="text-foreground text-center text-2xl font-bold">
|
|
10
|
+
Not found
|
|
11
|
+
</h2>
|
|
12
|
+
<p className="text-muted-foreground mt-1 text-center text-sm">
|
|
13
|
+
Password reset is disabled.
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Render the client component (the form)
|
|
20
|
+
return <ForgotPasswordForm />;
|
|
21
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useSearchParams, useRouter } from "next/navigation";
|
|
5
|
+
import { useForm } from "react-hook-form";
|
|
6
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
7
|
+
import { resetPasswordSchema } from "@/lib/validation/forms";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Form } from "@/components/ui/form/form";
|
|
11
|
+
import { FormField } from "@/components/ui/form/form-field";
|
|
12
|
+
import { FormItem } from "@/components/ui/form/form-item";
|
|
13
|
+
import { FormLabel } from "@/components/ui/form/form-label";
|
|
14
|
+
import { FormControl } from "@/components/ui/form/form-control";
|
|
15
|
+
import { FormMessage } from "@/components/ui/form/form-message";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
import { signOut } from "next-auth/react";
|
|
18
|
+
|
|
19
|
+
export default function ResetPasswordPage() {
|
|
20
|
+
// Do not gate rendering on process.env here — rendering must be consistent
|
|
21
|
+
// between server and client to avoid hydration mismatches. The API routes
|
|
22
|
+
// already enforce the feature guard (returning 404) so we let the client
|
|
23
|
+
// always render and surface the API's response.
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const token = searchParams.get("token") || "";
|
|
27
|
+
const [valid, setValid] = useState<boolean | null>(null);
|
|
28
|
+
|
|
29
|
+
const methods = useForm({
|
|
30
|
+
resolver: zodResolver(resetPasswordSchema) as any,
|
|
31
|
+
defaultValues: { token, password: "", confirmPassword: "" },
|
|
32
|
+
});
|
|
33
|
+
const {
|
|
34
|
+
handleSubmit,
|
|
35
|
+
control,
|
|
36
|
+
formState: { isSubmitting },
|
|
37
|
+
} = methods;
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
(async () => {
|
|
41
|
+
if (!token) return setValid(false);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(
|
|
44
|
+
`/api/auth/reset-password?token=${encodeURIComponent(token)}`,
|
|
45
|
+
);
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
const json = await res.json();
|
|
48
|
+
setValid(!!json.valid);
|
|
49
|
+
} else {
|
|
50
|
+
setValid(false);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
setValid(false);
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
}, [token]);
|
|
57
|
+
|
|
58
|
+
const onSubmit = async (data: any) => {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch("/api/auth/reset-password", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify(data),
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
});
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
toast.success("Password updated. You can now sign in.");
|
|
67
|
+
try {
|
|
68
|
+
// Ensure any existing session is cleared so the login page doesn't
|
|
69
|
+
// immediately redirect away. This also avoids confusion during testing.
|
|
70
|
+
await signOut({ redirect: false });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// ignore signOut failures but log for debugging
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.error("signOut failed:", e);
|
|
75
|
+
}
|
|
76
|
+
// Poll /api/auth/session until it returns null, or timeout after 2s.
|
|
77
|
+
// This avoids the race where signOut is accepted server-side but the
|
|
78
|
+
// client still believes it's authenticated due to caching or timing.
|
|
79
|
+
const waitForSignOut = async (timeout = 2000, interval = 200) => {
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
while (Date.now() - start < timeout) {
|
|
82
|
+
try {
|
|
83
|
+
const r = await fetch("/api/auth/session");
|
|
84
|
+
if (r.ok) {
|
|
85
|
+
const json = await r.json();
|
|
86
|
+
if (!json) {
|
|
87
|
+
// session cleared
|
|
88
|
+
window.location.href = "/auth/login?signup=1";
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// ignore transient fetch errors
|
|
94
|
+
}
|
|
95
|
+
// eslint-disable-next-line no-await-in-loop
|
|
96
|
+
await new Promise((res) => setTimeout(res, interval));
|
|
97
|
+
}
|
|
98
|
+
// Timeout: navigate anyway to ensure user sees login
|
|
99
|
+
window.location.href = "/auth/login?signup=1";
|
|
100
|
+
};
|
|
101
|
+
waitForSignOut();
|
|
102
|
+
} else {
|
|
103
|
+
const json = await res.json();
|
|
104
|
+
toast.error(json?.message || "Failed to reset password");
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
toast.error("Failed to reset password");
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (valid === null)
|
|
112
|
+
return (
|
|
113
|
+
<div className="mx-auto w-full max-w-md pt-6">Checking token...</div>
|
|
114
|
+
);
|
|
115
|
+
if (valid === false)
|
|
116
|
+
return (
|
|
117
|
+
<div className="mx-auto w-full max-w-md pt-6">
|
|
118
|
+
Invalid or expired token.
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="mx-auto w-full max-w-md pt-6">
|
|
124
|
+
<h2 className="text-foreground text-center text-2xl font-bold">
|
|
125
|
+
Reset password
|
|
126
|
+
</h2>
|
|
127
|
+
<p className="text-muted-foreground mt-1 text-center text-sm">
|
|
128
|
+
Set a new password for your account.
|
|
129
|
+
</p>
|
|
130
|
+
|
|
131
|
+
<Form methods={methods}>
|
|
132
|
+
<form
|
|
133
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
134
|
+
className="border-border bg-card space-y-4 rounded-lg border p-6 shadow-sm"
|
|
135
|
+
>
|
|
136
|
+
<FormField
|
|
137
|
+
control={control}
|
|
138
|
+
name="password"
|
|
139
|
+
render={({ field: f }) => (
|
|
140
|
+
<FormItem>
|
|
141
|
+
<FormLabel>New password</FormLabel>
|
|
142
|
+
<FormControl>
|
|
143
|
+
<Input
|
|
144
|
+
id="password"
|
|
145
|
+
type="password"
|
|
146
|
+
placeholder="At least 6 characters"
|
|
147
|
+
{...f}
|
|
148
|
+
/>
|
|
149
|
+
</FormControl>
|
|
150
|
+
<FormMessage />
|
|
151
|
+
</FormItem>
|
|
152
|
+
)}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<FormField
|
|
156
|
+
control={control}
|
|
157
|
+
name="confirmPassword"
|
|
158
|
+
render={({ field: f }) => (
|
|
159
|
+
<FormItem>
|
|
160
|
+
<FormLabel>Confirm password</FormLabel>
|
|
161
|
+
<FormControl>
|
|
162
|
+
<Input
|
|
163
|
+
id="confirmPassword"
|
|
164
|
+
type="password"
|
|
165
|
+
placeholder="Repeat password"
|
|
166
|
+
{...f}
|
|
167
|
+
/>
|
|
168
|
+
</FormControl>
|
|
169
|
+
<FormMessage />
|
|
170
|
+
</FormItem>
|
|
171
|
+
)}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
<FormField
|
|
175
|
+
control={control}
|
|
176
|
+
name="token"
|
|
177
|
+
render={({ field: f }) => <input type="hidden" {...f} />}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<Button type="submit" disabled={isSubmitting} className="w-full">
|
|
181
|
+
Set password
|
|
182
|
+
</Button>
|
|
183
|
+
</form>
|
|
184
|
+
</Form>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function VerifyEmailPage() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="mx-auto max-w-sm py-10">
|
|
4
|
+
<h1 className="text-xl font-semibold">Verify your email</h1>
|
|
5
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
6
|
+
Placeholder page. In a production app you might show a message that a
|
|
7
|
+
verification email has been sent.
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import MinimalLogoutButton from "@/components/auth/minimal-logout-button";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal admin header with basic links.
|
|
9
|
+
* - Uses only Next.js primitives and basic utility classes
|
|
10
|
+
* - Independent from any UI kit
|
|
11
|
+
*/
|
|
12
|
+
export default function AdminHeader() {
|
|
13
|
+
const pathname = usePathname();
|
|
14
|
+
const isActive = (href: string) => {
|
|
15
|
+
if (!pathname) return false;
|
|
16
|
+
return pathname === href || pathname.startsWith(href + "/");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const linkClass = (href: string) =>
|
|
20
|
+
[
|
|
21
|
+
"no-underline border-b-2 pb-0.5 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 rounded-sm",
|
|
22
|
+
isActive(href)
|
|
23
|
+
? "text-foreground border-foreground"
|
|
24
|
+
: "text-foreground/70 border-transparent",
|
|
25
|
+
].join(" ");
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<header className="bg-background/50 supports-[backdrop-filter]:bg-background/60 border-b backdrop-blur">
|
|
29
|
+
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3">
|
|
30
|
+
<nav className="flex items-center gap-4 text-sm">
|
|
31
|
+
<Link
|
|
32
|
+
className={linkClass("/dashboard")}
|
|
33
|
+
href="/dashboard"
|
|
34
|
+
aria-current={isActive("/dashboard") ? "page" : undefined}
|
|
35
|
+
>
|
|
36
|
+
Dashboard
|
|
37
|
+
</Link>
|
|
38
|
+
<Link
|
|
39
|
+
className={linkClass("/admin/users")}
|
|
40
|
+
href="/admin/users"
|
|
41
|
+
aria-current={isActive("/admin/users") ? "page" : undefined}
|
|
42
|
+
>
|
|
43
|
+
Users
|
|
44
|
+
</Link>
|
|
45
|
+
<Link
|
|
46
|
+
className={linkClass("/admin/posts")}
|
|
47
|
+
href="/admin/posts"
|
|
48
|
+
aria-current={isActive("/admin/posts") ? "page" : undefined}
|
|
49
|
+
>
|
|
50
|
+
Posts
|
|
51
|
+
</Link>
|
|
52
|
+
</nav>
|
|
53
|
+
<MinimalLogoutButton />
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { Session } from "next-auth";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import LogoutButton from "@/components/auth/logout-button";
|
|
8
|
+
import {
|
|
9
|
+
Settings,
|
|
10
|
+
Key,
|
|
11
|
+
FolderGit2,
|
|
12
|
+
BookOpen,
|
|
13
|
+
MessageCircle,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
|
|
16
|
+
export interface DashboardProps {
|
|
17
|
+
id?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
session?: Session | null;
|
|
20
|
+
|
|
21
|
+
headingText?: { text?: string; className?: string };
|
|
22
|
+
subheadingText?: { text?: string; className?: string };
|
|
23
|
+
|
|
24
|
+
container?: { className?: string };
|
|
25
|
+
card?: { className?: string };
|
|
26
|
+
heading?: { className?: string };
|
|
27
|
+
subheading?: { className?: string };
|
|
28
|
+
actions?: { className?: string };
|
|
29
|
+
|
|
30
|
+
// MVP blocks
|
|
31
|
+
showProfileCard?: boolean;
|
|
32
|
+
quickLinks?: Array<{ label: string; href: string; icon?: React.ReactNode }>;
|
|
33
|
+
quickLinksTitle?: { text?: string; className?: string };
|
|
34
|
+
|
|
35
|
+
// Slots for blocks
|
|
36
|
+
profileCard?: { className?: string };
|
|
37
|
+
profileAvatar?: { className?: string };
|
|
38
|
+
profileContent?: { className?: string };
|
|
39
|
+
quickLinksCard?: { className?: string };
|
|
40
|
+
quickLinksGrid?: { className?: string };
|
|
41
|
+
quickLinkItem?: { className?: string };
|
|
42
|
+
quickLinkIcon?: { className?: string };
|
|
43
|
+
|
|
44
|
+
showLogout?: boolean;
|
|
45
|
+
ariaLabel?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default function Dashboard({
|
|
49
|
+
id,
|
|
50
|
+
className,
|
|
51
|
+
session,
|
|
52
|
+
headingText = {
|
|
53
|
+
text: "Welcome",
|
|
54
|
+
className: "text-2xl font-semibold text-foreground",
|
|
55
|
+
},
|
|
56
|
+
subheadingText,
|
|
57
|
+
container = { className: "mx-auto max-w-5xl p-6" },
|
|
58
|
+
card = {
|
|
59
|
+
className:
|
|
60
|
+
"rounded-lg border border-border bg-card p-6 shadow-sm text-card-foreground",
|
|
61
|
+
},
|
|
62
|
+
heading = { className: "" },
|
|
63
|
+
subheading = { className: "mt-2 text-sm text-muted-foreground" },
|
|
64
|
+
actions = { className: "flex items-center gap-3" },
|
|
65
|
+
|
|
66
|
+
// MVP blocks defaults
|
|
67
|
+
showProfileCard = true,
|
|
68
|
+
quickLinks = [
|
|
69
|
+
{
|
|
70
|
+
label: "Settings",
|
|
71
|
+
href: "/settings/profile/",
|
|
72
|
+
icon: <Settings className="h-4 w-4" />,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "API Keys",
|
|
76
|
+
href: "/settings/api-keys",
|
|
77
|
+
icon: <Key className="h-4 w-4" />,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "Projects",
|
|
81
|
+
href: "/projects",
|
|
82
|
+
icon: <FolderGit2 className="h-4 w-4" />,
|
|
83
|
+
},
|
|
84
|
+
{ label: "Docs", href: "/docs", icon: <BookOpen className="h-4 w-4" /> },
|
|
85
|
+
{
|
|
86
|
+
label: "Support",
|
|
87
|
+
href: "/support",
|
|
88
|
+
icon: <MessageCircle className="h-4 w-4" />,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
quickLinksTitle = {
|
|
92
|
+
text: "Quick links",
|
|
93
|
+
className: "text-sm font-medium text-muted-foreground",
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
profileCard = {
|
|
97
|
+
className: "rounded-lg border border-border bg-card p-6 shadow-sm",
|
|
98
|
+
},
|
|
99
|
+
profileAvatar = {
|
|
100
|
+
className:
|
|
101
|
+
"h-12 w-12 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-base font-semibold",
|
|
102
|
+
},
|
|
103
|
+
profileContent = { className: "mt-3" },
|
|
104
|
+
quickLinksCard = {
|
|
105
|
+
className: "rounded-lg border border-border bg-card p-6 shadow-sm",
|
|
106
|
+
},
|
|
107
|
+
quickLinksGrid = { className: "mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2" },
|
|
108
|
+
quickLinkItem = {
|
|
109
|
+
className:
|
|
110
|
+
"group flex items-center gap-3 rounded-md border border-border bg-secondary/30 p-3 text-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-secondary/50",
|
|
111
|
+
},
|
|
112
|
+
quickLinkIcon = {
|
|
113
|
+
className:
|
|
114
|
+
"text-muted-foreground group-hover:text-foreground transition-colors",
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
showLogout = true,
|
|
118
|
+
ariaLabel = "Dashboard",
|
|
119
|
+
}: DashboardProps) {
|
|
120
|
+
const displayName =
|
|
121
|
+
(session?.user?.name && session.user.name.trim().length > 0
|
|
122
|
+
? session.user.name
|
|
123
|
+
: session?.user?.email) ?? "";
|
|
124
|
+
|
|
125
|
+
const computedSubheading =
|
|
126
|
+
subheadingText?.text ?? (displayName ? `Signed in as ${displayName}` : "");
|
|
127
|
+
|
|
128
|
+
const nameOrEmail = displayName || session?.user?.email || "";
|
|
129
|
+
const initials = nameOrEmail
|
|
130
|
+
.split(" ")
|
|
131
|
+
.map((s) => s[0])
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.slice(0, 2)
|
|
134
|
+
.join("")
|
|
135
|
+
.toUpperCase();
|
|
136
|
+
|
|
137
|
+
const createdAtRaw =
|
|
138
|
+
(session as any)?.user?.createdAt ?? (session as any)?.createdAt;
|
|
139
|
+
const createdAt = createdAtRaw ? new Date(createdAtRaw) : null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<main
|
|
143
|
+
id={id}
|
|
144
|
+
aria-label={ariaLabel}
|
|
145
|
+
className={cn(container.className, className)}
|
|
146
|
+
>
|
|
147
|
+
{/* Header card */}
|
|
148
|
+
<div className={cn(card.className)}>
|
|
149
|
+
<div className="flex items-center justify-between">
|
|
150
|
+
{headingText?.text && (
|
|
151
|
+
<h1 className={cn(headingText.className, heading.className)}>
|
|
152
|
+
{headingText.text}
|
|
153
|
+
</h1>
|
|
154
|
+
)}
|
|
155
|
+
{showLogout && (
|
|
156
|
+
<div className={cn(actions.className)}>
|
|
157
|
+
<LogoutButton />
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{computedSubheading && (
|
|
163
|
+
<p className={cn(subheading.className, subheadingText?.className)}>
|
|
164
|
+
{computedSubheading}
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Content grid */}
|
|
170
|
+
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
171
|
+
{showProfileCard && (
|
|
172
|
+
<div className={cn(profileCard.className)}>
|
|
173
|
+
<div className="flex items-center gap-4">
|
|
174
|
+
{session?.user?.image ? (
|
|
175
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
176
|
+
<img
|
|
177
|
+
src={session.user.image}
|
|
178
|
+
alt="Avatar"
|
|
179
|
+
className={cn(
|
|
180
|
+
"h-12 w-12 rounded-full object-cover",
|
|
181
|
+
profileAvatar.className,
|
|
182
|
+
)}
|
|
183
|
+
/>
|
|
184
|
+
) : (
|
|
185
|
+
<div className={cn(profileAvatar.className)}>
|
|
186
|
+
{initials || "U"}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
<div className="min-w-0">
|
|
190
|
+
<div className="text-foreground truncate text-base font-medium">
|
|
191
|
+
{session?.user?.name || session?.user?.email || "User"}
|
|
192
|
+
</div>
|
|
193
|
+
{session?.user?.email && (
|
|
194
|
+
<div className="text-muted-foreground truncate text-sm">
|
|
195
|
+
{session.user.email}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div className={cn(profileContent.className)}>
|
|
201
|
+
{createdAt && !Number.isNaN(createdAt.getTime()) && (
|
|
202
|
+
<p className="text-muted-foreground text-sm">
|
|
203
|
+
Member since {createdAt.toLocaleDateString()}
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Quick links */}
|
|
211
|
+
<div className={cn(quickLinksCard.className)}>
|
|
212
|
+
{quickLinksTitle?.text && (
|
|
213
|
+
<div className={cn(quickLinksTitle.className)}>
|
|
214
|
+
{quickLinksTitle.text}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
<div className={cn(quickLinksGrid.className)}>
|
|
218
|
+
{quickLinks?.map((item, i) => (
|
|
219
|
+
<Link
|
|
220
|
+
key={`${item.label}-${i}`}
|
|
221
|
+
href={item.href || "#"}
|
|
222
|
+
className={cn(quickLinkItem.className)}
|
|
223
|
+
>
|
|
224
|
+
{item.icon && (
|
|
225
|
+
<span className={cn(quickLinkIcon.className)}>
|
|
226
|
+
{item.icon}
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
<span className="text-foreground truncate">{item.label}</span>
|
|
230
|
+
</Link>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</main>
|
|
236
|
+
);
|
|
237
|
+
}
|