nextworks 0.0.1 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +145 -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 +106 -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 +104 -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,468 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
type JSX,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
11
|
+
import { signIn, useSession, getProviders } from "next-auth/react";
|
|
12
|
+
|
|
13
|
+
import Link from "next/link";
|
|
14
|
+
import { useForm } from "react-hook-form";
|
|
15
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { Input } from "@/components/ui/input";
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
import { signupSchema, type SignupFormValues } from "@/lib/validation/forms";
|
|
20
|
+
import { Form } from "@/components/ui/form/form";
|
|
21
|
+
import { FormField } from "@/components/ui/form/form-field";
|
|
22
|
+
import { FormItem } from "@/components/ui/form/form-item";
|
|
23
|
+
import { FormLabel } from "@/components/ui/form/form-label";
|
|
24
|
+
import { FormMessage } from "@/components/ui/form/form-message";
|
|
25
|
+
import { FormControl } from "@/components/ui/form/form-control";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
import { mapApiErrorsToForm } from "@/lib/forms/map-errors";
|
|
28
|
+
import useCheckUnique from "@/components/hooks/useCheckUnique";
|
|
29
|
+
|
|
30
|
+
export interface SignupFormProps {
|
|
31
|
+
id?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
|
|
34
|
+
/** Where to redirect if the user is already authenticated. Default: "/dashboard" */
|
|
35
|
+
defaultRedirect?: string;
|
|
36
|
+
|
|
37
|
+
/** Show the OAuth (GitHub) provider button. Default: true */
|
|
38
|
+
showGithub?: boolean;
|
|
39
|
+
|
|
40
|
+
/** Show the divider between providers and form fields. Default: true */
|
|
41
|
+
showDivider?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Optional header text above the form */
|
|
44
|
+
headingText?: { text?: string; className?: string } | null;
|
|
45
|
+
/** Optional subheading text under the header */
|
|
46
|
+
subheadingText?: { text?: string; className?: string } | null;
|
|
47
|
+
|
|
48
|
+
/** Text labels you might want to override */
|
|
49
|
+
labels?: {
|
|
50
|
+
name?: string;
|
|
51
|
+
email?: string;
|
|
52
|
+
password?: string;
|
|
53
|
+
submit?: string;
|
|
54
|
+
submitting?: string;
|
|
55
|
+
github?: string;
|
|
56
|
+
success?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Slots for styling overrides (similar to Navbar) */
|
|
60
|
+
container?: { className?: string };
|
|
61
|
+
headerWrapper?: { className?: string };
|
|
62
|
+
providerWrapper?: { className?: string };
|
|
63
|
+
providerButton?: {
|
|
64
|
+
variant?:
|
|
65
|
+
| "default"
|
|
66
|
+
| "destructive"
|
|
67
|
+
| "outline"
|
|
68
|
+
| "secondary"
|
|
69
|
+
| "ghost"
|
|
70
|
+
| "link";
|
|
71
|
+
size?: "default" | "sm" | "lg" | "icon";
|
|
72
|
+
className?: string;
|
|
73
|
+
};
|
|
74
|
+
divider?: {
|
|
75
|
+
wrapperClassName?: string;
|
|
76
|
+
lineClassName?: string;
|
|
77
|
+
textClassName?: string;
|
|
78
|
+
};
|
|
79
|
+
form?: { className?: string };
|
|
80
|
+
field?: { className?: string };
|
|
81
|
+
label?: { className?: string };
|
|
82
|
+
input?: { className?: string };
|
|
83
|
+
submitButton?: {
|
|
84
|
+
variant?:
|
|
85
|
+
| "default"
|
|
86
|
+
| "destructive"
|
|
87
|
+
| "outline"
|
|
88
|
+
| "secondary"
|
|
89
|
+
| "ghost"
|
|
90
|
+
| "link";
|
|
91
|
+
size?: "default" | "sm" | "lg" | "icon";
|
|
92
|
+
className?: string;
|
|
93
|
+
};
|
|
94
|
+
alerts?: {
|
|
95
|
+
errorClassName?: string;
|
|
96
|
+
successClassName?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** ARIA label for the form */
|
|
100
|
+
ariaLabel?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default function SignupForm({
|
|
104
|
+
id,
|
|
105
|
+
className,
|
|
106
|
+
defaultRedirect = "/dashboard",
|
|
107
|
+
showGithub = true,
|
|
108
|
+
showDivider = true,
|
|
109
|
+
headingText = {
|
|
110
|
+
text: "Create your account",
|
|
111
|
+
className: "text-2xl font-bold text-foreground text-center",
|
|
112
|
+
},
|
|
113
|
+
subheadingText = {
|
|
114
|
+
text: "Start your free trial in minutes",
|
|
115
|
+
className: "mt-1 text-sm text-muted-foreground text-center",
|
|
116
|
+
},
|
|
117
|
+
labels = {
|
|
118
|
+
name: "Name",
|
|
119
|
+
email: "Email",
|
|
120
|
+
password: "Password",
|
|
121
|
+
submit: "Create account",
|
|
122
|
+
submitting: "Creating account...",
|
|
123
|
+
github: "Continue with GitHub",
|
|
124
|
+
success: "Account created. You can now sign in.",
|
|
125
|
+
},
|
|
126
|
+
container = { className: "mx-auto w-full max-w-md pt-6" },
|
|
127
|
+
headerWrapper = { className: "mb-4" },
|
|
128
|
+
providerWrapper = { className: "mb-6" },
|
|
129
|
+
providerButton = {
|
|
130
|
+
variant: "outline",
|
|
131
|
+
size: "default",
|
|
132
|
+
className:
|
|
133
|
+
"w-full shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md",
|
|
134
|
+
},
|
|
135
|
+
divider = {
|
|
136
|
+
wrapperClassName: "relative mb-6",
|
|
137
|
+
lineClassName: "w-full border-t",
|
|
138
|
+
textClassName: "bg-background text-muted-foreground px-2",
|
|
139
|
+
},
|
|
140
|
+
form = {
|
|
141
|
+
className:
|
|
142
|
+
"space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm",
|
|
143
|
+
},
|
|
144
|
+
field = { className: "space-y-2" },
|
|
145
|
+
label = { className: "" },
|
|
146
|
+
input = { className: "" },
|
|
147
|
+
submitButton = {
|
|
148
|
+
variant: "default",
|
|
149
|
+
size: "default",
|
|
150
|
+
className:
|
|
151
|
+
"w-full shadow-lg transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl",
|
|
152
|
+
},
|
|
153
|
+
alerts = {
|
|
154
|
+
errorClassName:
|
|
155
|
+
"mb-4 rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive",
|
|
156
|
+
successClassName:
|
|
157
|
+
"mb-4 rounded-md border border-primary/20 bg-primary/10 p-3 text-sm text-foreground",
|
|
158
|
+
},
|
|
159
|
+
ariaLabel = "Signup form",
|
|
160
|
+
}: SignupFormProps): JSX.Element {
|
|
161
|
+
const router = useRouter();
|
|
162
|
+
const searchParams = useSearchParams();
|
|
163
|
+
const { data: session, status } = useSession();
|
|
164
|
+
const redirectTo = searchParams.get("callbackUrl") || defaultRedirect;
|
|
165
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
166
|
+
const [success, setSuccess] = useState(false);
|
|
167
|
+
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
|
168
|
+
const [githubAvailable, setGithubAvailable] = useState(false);
|
|
169
|
+
|
|
170
|
+
const formMethods = useForm<SignupFormValues>({
|
|
171
|
+
resolver: zodResolver(signupSchema),
|
|
172
|
+
defaultValues: {
|
|
173
|
+
name: "",
|
|
174
|
+
email: "",
|
|
175
|
+
password: "",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const {
|
|
179
|
+
control,
|
|
180
|
+
handleSubmit,
|
|
181
|
+
setError: setFieldError,
|
|
182
|
+
formState: { isSubmitting },
|
|
183
|
+
} = formMethods;
|
|
184
|
+
|
|
185
|
+
const emailValue = formMethods.watch("email");
|
|
186
|
+
const {
|
|
187
|
+
loading: checkingEmail,
|
|
188
|
+
unique: emailUnique,
|
|
189
|
+
error: emailUniqueError,
|
|
190
|
+
} = useCheckUnique("email", emailValue, 500);
|
|
191
|
+
|
|
192
|
+
const useDebouncedValidator = (
|
|
193
|
+
fn: (v: any) => Promise<boolean | string | undefined>,
|
|
194
|
+
delay = 500,
|
|
195
|
+
) => {
|
|
196
|
+
const timer = useRef<number | undefined>(undefined);
|
|
197
|
+
return useCallback(
|
|
198
|
+
(value: any): Promise<boolean | string | undefined> => {
|
|
199
|
+
if (timer.current) window.clearTimeout(timer.current);
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
timer.current = window.setTimeout(async () => {
|
|
202
|
+
try {
|
|
203
|
+
const res = await fn(value);
|
|
204
|
+
resolve(res);
|
|
205
|
+
} catch {
|
|
206
|
+
resolve(true);
|
|
207
|
+
}
|
|
208
|
+
}, delay);
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
[fn, delay],
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const checkEmailAvailability = async (
|
|
216
|
+
email: string,
|
|
217
|
+
): Promise<boolean | string | undefined> => {
|
|
218
|
+
if (!email) return true;
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch(
|
|
221
|
+
`/api/users/check-email?email=${encodeURIComponent(email)}`,
|
|
222
|
+
);
|
|
223
|
+
const payload = await res.json().catch(() => null);
|
|
224
|
+
if (!res.ok || !payload?.success) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return payload.data?.available ? true : "Email already in use";
|
|
228
|
+
} catch {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const debouncedValidateEmail = useDebouncedValidator(
|
|
234
|
+
checkEmailAvailability,
|
|
235
|
+
500,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (status === "authenticated" && session) {
|
|
240
|
+
router.push(redirectTo);
|
|
241
|
+
}
|
|
242
|
+
}, [status, session, router, redirectTo]);
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
(async () => {
|
|
246
|
+
const providers = await getProviders();
|
|
247
|
+
setGithubAvailable(!!providers?.github);
|
|
248
|
+
})();
|
|
249
|
+
}, []);
|
|
250
|
+
|
|
251
|
+
const onSubmit = async (data: SignupFormValues) => {
|
|
252
|
+
setFormError(null);
|
|
253
|
+
setSuccess(false);
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetch("/api/signup", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { "Content-Type": "application/json" },
|
|
258
|
+
body: JSON.stringify(data),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const payload = await res.json().catch(() => null);
|
|
262
|
+
if (!res.ok || !payload?.success) {
|
|
263
|
+
const msg = payload
|
|
264
|
+
? mapApiErrorsToForm(formMethods, payload)
|
|
265
|
+
: undefined;
|
|
266
|
+
setFormError(msg || payload?.message || "Signup failed");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
router.push(
|
|
271
|
+
`/auth/login?signup=1&callbackUrl=${encodeURIComponent(redirectTo)}`,
|
|
272
|
+
);
|
|
273
|
+
} catch (e: unknown) {
|
|
274
|
+
const err = e as Error;
|
|
275
|
+
setFormError(err.message || "Unknown error");
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleGithubSignup = async () => {
|
|
280
|
+
setIsGithubLoading(true);
|
|
281
|
+
setFormError(null);
|
|
282
|
+
const result = await signIn("github", {
|
|
283
|
+
redirect: false,
|
|
284
|
+
callbackUrl: redirectTo,
|
|
285
|
+
});
|
|
286
|
+
if (result?.error) {
|
|
287
|
+
setFormError("GitHub login failed. Please try again.");
|
|
288
|
+
setIsGithubLoading(false);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
id={id}
|
|
295
|
+
className={cn(container.className, className)}
|
|
296
|
+
aria-label={ariaLabel}
|
|
297
|
+
>
|
|
298
|
+
{(headingText?.text || subheadingText?.text) && (
|
|
299
|
+
<div className={cn(headerWrapper.className)}>
|
|
300
|
+
{headingText?.text && (
|
|
301
|
+
<h2 className={cn("font-poppins", headingText.className)}>
|
|
302
|
+
{headingText.text}
|
|
303
|
+
</h2>
|
|
304
|
+
)}
|
|
305
|
+
{subheadingText?.text && (
|
|
306
|
+
<p className={cn(subheadingText.className)}>
|
|
307
|
+
{subheadingText.text}
|
|
308
|
+
</p>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
|
|
313
|
+
{formError && (
|
|
314
|
+
<div
|
|
315
|
+
className={cn(alerts.errorClassName)}
|
|
316
|
+
role="alert"
|
|
317
|
+
aria-live="polite"
|
|
318
|
+
>
|
|
319
|
+
{formError}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
{success && (
|
|
323
|
+
<div
|
|
324
|
+
className={cn(alerts.successClassName)}
|
|
325
|
+
role="status"
|
|
326
|
+
aria-live="polite"
|
|
327
|
+
>
|
|
328
|
+
{labels.success}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{showGithub && githubAvailable && (
|
|
333
|
+
<>
|
|
334
|
+
<div className={cn(providerWrapper.className)}>
|
|
335
|
+
<Button
|
|
336
|
+
variant={providerButton.variant}
|
|
337
|
+
size={providerButton.size}
|
|
338
|
+
className={cn(providerButton.className)}
|
|
339
|
+
onClick={handleGithubSignup}
|
|
340
|
+
disabled={isGithubLoading}
|
|
341
|
+
aria-label={labels.github}
|
|
342
|
+
>
|
|
343
|
+
{labels.github}
|
|
344
|
+
</Button>
|
|
345
|
+
</div>
|
|
346
|
+
{showDivider && (
|
|
347
|
+
<div className={cn(divider.wrapperClassName)}>
|
|
348
|
+
<div className="absolute inset-0 flex items-center">
|
|
349
|
+
<span className={cn(divider.lineClassName)} />
|
|
350
|
+
</div>
|
|
351
|
+
<div className="relative flex justify-center text-xs">
|
|
352
|
+
<span className={cn(divider.textClassName)}>or</span>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
<Form methods={formMethods}>
|
|
360
|
+
<form onSubmit={handleSubmit(onSubmit)} className={cn(form.className)}>
|
|
361
|
+
<FormField
|
|
362
|
+
control={control}
|
|
363
|
+
name="name"
|
|
364
|
+
render={({ field: f }) => (
|
|
365
|
+
<FormItem className={cn(field.className)}>
|
|
366
|
+
<FormLabel className={cn(label.className)}>
|
|
367
|
+
{labels.name}
|
|
368
|
+
</FormLabel>
|
|
369
|
+
<FormControl>
|
|
370
|
+
<Input
|
|
371
|
+
placeholder="Your name"
|
|
372
|
+
autoComplete="name"
|
|
373
|
+
className={cn(input.className)}
|
|
374
|
+
{...f}
|
|
375
|
+
/>
|
|
376
|
+
</FormControl>
|
|
377
|
+
<FormMessage />
|
|
378
|
+
</FormItem>
|
|
379
|
+
)}
|
|
380
|
+
/>
|
|
381
|
+
|
|
382
|
+
<FormField
|
|
383
|
+
control={control}
|
|
384
|
+
name="email"
|
|
385
|
+
rules={{ validate: debouncedValidateEmail }}
|
|
386
|
+
render={({ field: f }) => (
|
|
387
|
+
<FormItem className={cn(field.className)}>
|
|
388
|
+
<FormLabel className={cn(label.className)}>
|
|
389
|
+
{labels.email}
|
|
390
|
+
</FormLabel>
|
|
391
|
+
<FormControl>
|
|
392
|
+
<Input
|
|
393
|
+
type="email"
|
|
394
|
+
inputMode="email"
|
|
395
|
+
autoComplete="email"
|
|
396
|
+
placeholder="you@example.com"
|
|
397
|
+
className={cn(input.className)}
|
|
398
|
+
{...f}
|
|
399
|
+
/>
|
|
400
|
+
</FormControl>
|
|
401
|
+
|
|
402
|
+
<div aria-live="polite" className="mt-1">
|
|
403
|
+
{checkingEmail ? (
|
|
404
|
+
<span className="text-muted-foreground text-sm">
|
|
405
|
+
Checking…
|
|
406
|
+
</span>
|
|
407
|
+
) : emailUnique === true ? (
|
|
408
|
+
<span className="text-sm text-green-600">
|
|
409
|
+
Email available
|
|
410
|
+
</span>
|
|
411
|
+
) : emailUnique === false ? (
|
|
412
|
+
<span className="text-destructive text-sm">
|
|
413
|
+
Email already in use
|
|
414
|
+
</span>
|
|
415
|
+
) : null}
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<FormMessage />
|
|
419
|
+
</FormItem>
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
|
|
423
|
+
<FormField
|
|
424
|
+
control={control}
|
|
425
|
+
name="password"
|
|
426
|
+
render={({ field: f }) => (
|
|
427
|
+
<FormItem className={cn(field.className)}>
|
|
428
|
+
<FormLabel className={cn(label.className)}>
|
|
429
|
+
{labels.password}
|
|
430
|
+
</FormLabel>
|
|
431
|
+
<FormControl>
|
|
432
|
+
<Input
|
|
433
|
+
type="password"
|
|
434
|
+
autoComplete="new-password"
|
|
435
|
+
placeholder="At least 6 characters"
|
|
436
|
+
className={cn(input.className)}
|
|
437
|
+
{...f}
|
|
438
|
+
/>
|
|
439
|
+
</FormControl>
|
|
440
|
+
<FormMessage />
|
|
441
|
+
</FormItem>
|
|
442
|
+
)}
|
|
443
|
+
/>
|
|
444
|
+
|
|
445
|
+
<Button
|
|
446
|
+
type="submit"
|
|
447
|
+
variant={submitButton.variant}
|
|
448
|
+
size={submitButton.size}
|
|
449
|
+
className={cn(submitButton.className)}
|
|
450
|
+
disabled={isSubmitting || emailUnique === false}
|
|
451
|
+
aria-label={isSubmitting ? labels.submitting : labels.submit}
|
|
452
|
+
>
|
|
453
|
+
{isSubmitting ? labels.submitting : labels.submit}
|
|
454
|
+
</Button>
|
|
455
|
+
</form>
|
|
456
|
+
</Form>
|
|
457
|
+
<p className="mt-3 text-center text-sm">
|
|
458
|
+
Already have an account?{" "}
|
|
459
|
+
<Link
|
|
460
|
+
href={`/auth/login?callbackUrl=${encodeURIComponent(searchParams.get("callbackUrl") || defaultRedirect)}`}
|
|
461
|
+
className="text-primary underline"
|
|
462
|
+
>
|
|
463
|
+
Log in
|
|
464
|
+
</Link>
|
|
465
|
+
</p>
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { getServerSession } from "next-auth";
|
|
3
|
+
import { authOptions } from "@/lib/auth";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import AdminHeader from "@/components/admin/admin-header";
|
|
6
|
+
|
|
7
|
+
export interface RequireAuthProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
returnTo?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Controls whether a minimal admin header is shown.
|
|
12
|
+
* Defaults to true for DX. Consumers can disable per layout/page.
|
|
13
|
+
*/
|
|
14
|
+
showAdminHeader?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function RequireAuth({
|
|
18
|
+
children,
|
|
19
|
+
returnTo = "/dashboard",
|
|
20
|
+
showAdminHeader = true,
|
|
21
|
+
}: RequireAuthProps) {
|
|
22
|
+
const session = await getServerSession(authOptions);
|
|
23
|
+
if (!session?.user) {
|
|
24
|
+
redirect(`/auth/login?callbackUrl=${encodeURIComponent(returnTo)}`);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{showAdminHeader && <AdminHeader />}
|
|
29
|
+
{children}
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Named export to enforce admin-only access for server-rendered pages/layouts.
|
|
35
|
+
export async function RequireAdmin({
|
|
36
|
+
children,
|
|
37
|
+
returnTo = "/dashboard",
|
|
38
|
+
// By default don't render the AdminHeader here because ProtectedLayout already
|
|
39
|
+
// injects it via RequireAuth. Consumers can opt-in by passing showAdminHeader.
|
|
40
|
+
showAdminHeader = false,
|
|
41
|
+
}: RequireAuthProps) {
|
|
42
|
+
const session = await getServerSession(authOptions);
|
|
43
|
+
if (!session?.user) {
|
|
44
|
+
// Not signed in -> redirect to login
|
|
45
|
+
redirect(`/auth/login?callbackUrl=${encodeURIComponent(returnTo)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Signed in but not admin -> redirect away from admin area
|
|
49
|
+
if ((session.user as { role?: string }).role !== "admin") {
|
|
50
|
+
redirect(returnTo);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{showAdminHeader && <AdminHeader />}
|
|
56
|
+
{children}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder contains minimal UI primitives used by the Auth kit (Button, Input, Label). If your project already has equivalents, you can delete these and update imports.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: "h-10 px-4 py-2",
|
|
23
|
+
sm: "h-9 rounded-md px-3",
|
|
24
|
+
lg: "h-11 rounded-md px-8",
|
|
25
|
+
icon: "h-10 w-10",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export interface ButtonProps
|
|
36
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
42
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
43
|
+
const Comp = asChild ? Slot : "button";
|
|
44
|
+
return (
|
|
45
|
+
<Comp
|
|
46
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
47
|
+
ref={ref}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
Button.displayName = "Button";
|
|
54
|
+
|
|
55
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface InputProps
|
|
6
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
7
|
+
|
|
8
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
9
|
+
({ className, type, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
type={type}
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
ref={ref}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
Input.displayName = "Input";
|
|
24
|
+
|
|
25
|
+
export { Input };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
const Label = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<LabelPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn(
|
|
15
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
));
|
|
21
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
22
|
+
|
|
23
|
+
export { Label };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
|
|
3
|
+
export type FieldErrors = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
export function zodErrorToFieldErrors(error: ZodError): FieldErrors {
|
|
6
|
+
const fieldErrors: FieldErrors = {};
|
|
7
|
+
for (const issue of error.issues) {
|
|
8
|
+
const key = (issue.path?.[0]?.toString() || "form").toString();
|
|
9
|
+
if (!fieldErrors[key]) {
|
|
10
|
+
fieldErrors[key] = issue.message;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return fieldErrors;
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { authOptions } from "@/lib/auth";
|
|
2
|
+
import { getServerSession } from "next-auth";
|
|
3
|
+
import { redirect } from "next/navigation";
|
|
4
|
+
|
|
5
|
+
export async function requireAuth(returnTo: string = "/dashboard") {
|
|
6
|
+
const session = await getServerSession(authOptions);
|
|
7
|
+
if (!session?.user) {
|
|
8
|
+
redirect(`/auth/login?callbackUrl=${encodeURIComponent(returnTo)}`);
|
|
9
|
+
}
|
|
10
|
+
return session;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function requireAdmin(returnTo: string = "/dashboard") {
|
|
14
|
+
const session = await getServerSession(authOptions);
|
|
15
|
+
if (!session?.user) {
|
|
16
|
+
redirect(`/auth/login?callbackUrl=${encodeURIComponent(returnTo)}`);
|
|
17
|
+
}
|
|
18
|
+
if ((session.user as { role?: string }).role !== "admin") {
|
|
19
|
+
redirect(returnTo);
|
|
20
|
+
}
|
|
21
|
+
return session;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function requireAdminApi() {
|
|
25
|
+
const session = await getServerSession(authOptions);
|
|
26
|
+
if (!session?.user) return null;
|
|
27
|
+
if ((session.user as { role?: string }).role !== "admin") return null;
|
|
28
|
+
return session;
|
|
29
|
+
}
|