@marvs13/marvinel-nextjs-supabase-starting-kit 1.0.2

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.
Files changed (39) hide show
  1. package/cli.js +95 -0
  2. package/package.json +27 -0
  3. package/template/.env.local_example +5 -0
  4. package/template/src/app/(auth)/change-password/page.tsx +11 -0
  5. package/template/src/app/(auth)/login/page.tsx +11 -0
  6. package/template/src/app/(auth)/request-reset-password/page.tsx +11 -0
  7. package/template/src/app/(auth)/reset-password/page.tsx +11 -0
  8. package/template/src/app/(auth)/signup/page.tsx +11 -0
  9. package/template/src/app/(auth)/signup-success/page.tsx +34 -0
  10. package/template/src/app/favicon.ico +0 -0
  11. package/template/src/app/globals.css +124 -0
  12. package/template/src/app/home/page.tsx +35 -0
  13. package/template/src/app/layout.tsx +35 -0
  14. package/template/src/app/not-found.tsx +9 -0
  15. package/template/src/app/page.tsx +11 -0
  16. package/template/src/components/forms/change-password.tsx +197 -0
  17. package/template/src/components/forms/login-form.tsx +188 -0
  18. package/template/src/components/forms/request-reset-password-form.tsx +137 -0
  19. package/template/src/components/forms/reset-password-form.tsx +199 -0
  20. package/template/src/components/forms/signup-form.tsx +231 -0
  21. package/template/src/components/hero.tsx +38 -0
  22. package/template/src/components/logout-button.tsx +52 -0
  23. package/template/src/components/page-not-found.tsx +32 -0
  24. package/template/src/components/ui/button.tsx +65 -0
  25. package/template/src/components/ui/card.tsx +92 -0
  26. package/template/src/components/ui/field.tsx +249 -0
  27. package/template/src/components/ui/input-group.tsx +171 -0
  28. package/template/src/components/ui/input.tsx +21 -0
  29. package/template/src/components/ui/label.tsx +25 -0
  30. package/template/src/components/ui/separator.tsx +29 -0
  31. package/template/src/components/ui/spinner.tsx +16 -0
  32. package/template/src/components/ui/textarea.tsx +18 -0
  33. package/template/src/hooks/use-auth-form.ts +47 -0
  34. package/template/src/lib/supabase/client.ts +8 -0
  35. package/template/src/lib/supabase/proxy.ts +55 -0
  36. package/template/src/lib/supabase/server.ts +34 -0
  37. package/template/src/lib/utils.ts +6 -0
  38. package/template/src/proxy.ts +21 -0
  39. package/template/src/schema/form-schema.ts +55 -0
package/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require("child_process");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+
7
+ // Detect which package manager is being used
8
+ const userAgent = process.env.npm_config_user_agent || "";
9
+ let packageManager = "npm";
10
+ let execCommand = "npx";
11
+
12
+ if (userAgent.includes("bun")) {
13
+ packageManager = "bun";
14
+ execCommand = "bunx";
15
+ } else if (userAgent.includes("pnpm")) {
16
+ packageManager = "pnpm";
17
+ execCommand = "pnpm dlx";
18
+ }
19
+
20
+ const projectName = process.argv[2] || "my-auth-app";
21
+ const projectPath = path.join(process.cwd(), projectName);
22
+ const templatePath = path.join(__dirname, "template");
23
+
24
+ try {
25
+ console.log(`🚀 Using ${packageManager} to create: ${projectName}...`);
26
+
27
+ // 1. Run Next.js installer (Dynamic manager)
28
+ // We pass --use-${packageManager} to force Next.js to use the same manager
29
+ execSync(
30
+ `${execCommand} create-next-app@latest ${projectName} --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${packageManager} --yes`,
31
+ { stdio: "inherit" },
32
+ );
33
+
34
+ process.chdir(projectPath);
35
+
36
+ // 2. Install Dependencies
37
+ console.log(
38
+ `📦 Installing Supabase and Auth dependencies via ${packageManager}...`,
39
+ );
40
+ const deps = [
41
+ "zod",
42
+ "react-hook-form",
43
+ "lucide-react",
44
+ "@hookform/resolvers",
45
+ "@supabase/ssr",
46
+ ];
47
+ const installCmd =
48
+ packageManager === "bun" ? "bun add" : `${packageManager} install`;
49
+ execSync(`${installCmd} ${deps.join(" ")}`, { stdio: "inherit" });
50
+
51
+ // 3. Initialize shadcn/ui
52
+ console.log("🎨 Initializing shadcn UI...");
53
+ execSync(`${execCommand} shadcn@latest init -d`, { stdio: "inherit" });
54
+
55
+ // 4. Install shadcn components
56
+ console.log("🧩 Adding shadcn components...");
57
+ const components = [
58
+ "button",
59
+ "card",
60
+ "input",
61
+ "label",
62
+ "separator",
63
+ "textarea",
64
+ "field",
65
+ "input-group",
66
+ "spinner",
67
+ ];
68
+ execSync(`${execCommand} shadcn@latest add ${components.join(" ")} -y`, {
69
+ stdio: "inherit",
70
+ });
71
+
72
+ // 5. COPY TEMPLATE FILES
73
+ console.log("🛠️ 📂 Injecting Marvinel's Template files...");
74
+ const copyFolderSync = (from, to) => {
75
+ if (!fs.existsSync(to)) fs.mkdirSync(to, { recursive: true });
76
+ fs.readdirSync(from).forEach((element) => {
77
+ const stat = fs.lstatSync(path.join(from, element));
78
+ if (stat.isFile()) {
79
+ fs.copyFileSync(path.join(from, element), path.join(to, element));
80
+ } else if (stat.isDirectory()) {
81
+ copyFolderSync(path.join(from, element), path.join(to, element));
82
+ }
83
+ });
84
+ };
85
+
86
+ copyFolderSync(templatePath, path.join(projectPath, "src"));
87
+
88
+ console.log(`\n✅ Setup complete! Project is ready.`);
89
+ console.log(
90
+ `\nNext steps:\ncd ${projectName}\n${packageManager === "bun" ? "bun dev" : "npm run dev"}`,
91
+ );
92
+ } catch (error) {
93
+ console.error("❌ Installation failed:", error);
94
+ process.exit(1);
95
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@marvs13/marvinel-nextjs-supabase-starting-kit",
3
+ "version": "1.0.2",
4
+ "description": "A modern Next.js + Supabase starting kit with Tailwind, Zod, and shadcn/ui.",
5
+ "main": "cli.js",
6
+ "bin": {
7
+ "@marvs13/marvinel-nextjs-supabase-starting-kit": "./cli.js"
8
+ },
9
+ "type": "module",
10
+ "files": [
11
+ "cli.js",
12
+ "template/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "keywords": [
18
+ "nextjs",
19
+ "supabase",
20
+ "auth",
21
+ "shadcn",
22
+ "tailwind",
23
+ "starter-kit"
24
+ ],
25
+ "author": "Marvinel Torres Santos",
26
+ "license": "MIT"
27
+ }
@@ -0,0 +1,5 @@
1
+ # TODO : Rename this to .env.local technically remove the "_example"
2
+ # TODO : Copy your SUPABASE URL AND SUPABASE PUBLISHABLE KEY from your Supabase
3
+
4
+ NEXT_PUBLIC_SUPABASE_URL=<YOUR_SUPABASE_URL>
5
+ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<YOUR_SUPABASE_PUBLISHABLE_KEY>
@@ -0,0 +1,11 @@
1
+ import { ChangePasswordForm } from "@/components/forms/change-password";
2
+
3
+ const Page = () => {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center">
6
+ <ChangePasswordForm />
7
+ </div>
8
+ );
9
+ };
10
+
11
+ export default Page;
@@ -0,0 +1,11 @@
1
+ import { LogInForm } from "@/components/forms/login-form";
2
+
3
+ const Page = () => {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center px-2">
6
+ <LogInForm />
7
+ </div>
8
+ );
9
+ };
10
+
11
+ export default Page;
@@ -0,0 +1,11 @@
1
+ import { RequestResetPassword } from "@/components/forms/request-reset-password-form";
2
+
3
+ const Page = () => {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center">
6
+ <RequestResetPassword />
7
+ </div>
8
+ );
9
+ };
10
+
11
+ export default Page;
@@ -0,0 +1,11 @@
1
+ import { ResetPasswordForm } from "@/components/forms/reset-password-form";
2
+
3
+ const Page = () => {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center">
6
+ <ResetPasswordForm />
7
+ </div>
8
+ );
9
+ };
10
+
11
+ export default Page;
@@ -0,0 +1,11 @@
1
+ import SignUpForm from "@/components/forms/signup-form";
2
+
3
+ const Page = () => {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center px-2">
6
+ <SignUpForm />
7
+ </div>
8
+ );
9
+ };
10
+
11
+ export default Page;
@@ -0,0 +1,34 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ } from "@/components/ui/card";
8
+
9
+ const Page = () => {
10
+ return (
11
+ <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
12
+ <div className="w-full max-w-sm">
13
+ <div className="flex flex-col gap-6">
14
+ <Card>
15
+ <CardHeader>
16
+ <CardTitle className="text-2xl">
17
+ Thank you for signing up!
18
+ </CardTitle>
19
+ <CardDescription>Check your email to confirm</CardDescription>
20
+ </CardHeader>
21
+ <CardContent>
22
+ <p className="text-muted-foreground text-sm">
23
+ You&apos;ve successfully signed up. Please check your email to
24
+ confirm your account before signing in.
25
+ </p>
26
+ </CardContent>
27
+ </Card>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default Page;
Binary file
@@ -0,0 +1,124 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --radius-sm: calc(var(--radius) - 4px);
9
+ --radius-md: calc(var(--radius) - 2px);
10
+ --radius-lg: var(--radius);
11
+ --radius-xl: calc(var(--radius) + 4px);
12
+ --radius-2xl: calc(var(--radius) + 8px);
13
+ --radius-3xl: calc(var(--radius) + 12px);
14
+ --radius-4xl: calc(var(--radius) + 16px);
15
+ --color-background: var(--background);
16
+ --color-foreground: var(--foreground);
17
+ --color-card: var(--card);
18
+ --color-card-foreground: var(--card-foreground);
19
+ --color-popover: var(--popover);
20
+ --color-popover-foreground: var(--popover-foreground);
21
+ --color-primary: var(--primary);
22
+ --color-primary-foreground: var(--primary-foreground);
23
+ --color-secondary: var(--secondary);
24
+ --color-secondary-foreground: var(--secondary-foreground);
25
+ --color-muted: var(--muted);
26
+ --color-muted-foreground: var(--muted-foreground);
27
+ --color-accent: var(--accent);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-destructive: var(--destructive);
30
+ --color-border: var(--border);
31
+ --color-input: var(--input);
32
+ --color-ring: var(--ring);
33
+ --color-chart-1: var(--chart-1);
34
+ --color-chart-2: var(--chart-2);
35
+ --color-chart-3: var(--chart-3);
36
+ --color-chart-4: var(--chart-4);
37
+ --color-chart-5: var(--chart-5);
38
+ --color-sidebar: var(--sidebar);
39
+ --color-sidebar-foreground: var(--sidebar-foreground);
40
+ --color-sidebar-primary: var(--sidebar-primary);
41
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
42
+ --color-sidebar-accent: var(--sidebar-accent);
43
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
44
+ --color-sidebar-border: var(--sidebar-border);
45
+ --color-sidebar-ring: var(--sidebar-ring);
46
+ }
47
+
48
+ :root {
49
+ --radius: 0.625rem;
50
+ --background: oklch(1 0 0);
51
+ --foreground: oklch(0.145 0 0);
52
+ --card: oklch(1 0 0);
53
+ --card-foreground: oklch(0.145 0 0);
54
+ --popover: oklch(1 0 0);
55
+ --popover-foreground: oklch(0.145 0 0);
56
+ --primary: oklch(0.205 0 0);
57
+ --primary-foreground: oklch(0.985 0 0);
58
+ --secondary: oklch(0.97 0 0);
59
+ --secondary-foreground: oklch(0.205 0 0);
60
+ --muted: oklch(0.97 0 0);
61
+ --muted-foreground: oklch(0.556 0 0);
62
+ --accent: oklch(0.97 0 0);
63
+ --accent-foreground: oklch(0.205 0 0);
64
+ --destructive: oklch(0.577 0.245 27.325);
65
+ --border: oklch(0.922 0 0);
66
+ --input: oklch(0.922 0 0);
67
+ --ring: oklch(0.708 0 0);
68
+ --chart-1: oklch(0.646 0.222 41.116);
69
+ --chart-2: oklch(0.6 0.118 184.704);
70
+ --chart-3: oklch(0.398 0.07 227.392);
71
+ --chart-4: oklch(0.828 0.189 84.429);
72
+ --chart-5: oklch(0.769 0.188 70.08);
73
+ --sidebar: oklch(0.985 0 0);
74
+ --sidebar-foreground: oklch(0.145 0 0);
75
+ --sidebar-primary: oklch(0.205 0 0);
76
+ --sidebar-primary-foreground: oklch(0.985 0 0);
77
+ --sidebar-accent: oklch(0.97 0 0);
78
+ --sidebar-accent-foreground: oklch(0.205 0 0);
79
+ --sidebar-border: oklch(0.922 0 0);
80
+ --sidebar-ring: oklch(0.708 0 0);
81
+ }
82
+
83
+ .dark {
84
+ --background: oklch(0.145 0 0);
85
+ --foreground: oklch(0.985 0 0);
86
+ --card: oklch(0.205 0 0);
87
+ --card-foreground: oklch(0.985 0 0);
88
+ --popover: oklch(0.205 0 0);
89
+ --popover-foreground: oklch(0.985 0 0);
90
+ --primary: oklch(0.922 0 0);
91
+ --primary-foreground: oklch(0.205 0 0);
92
+ --secondary: oklch(0.269 0 0);
93
+ --secondary-foreground: oklch(0.985 0 0);
94
+ --muted: oklch(0.269 0 0);
95
+ --muted-foreground: oklch(0.708 0 0);
96
+ --accent: oklch(0.269 0 0);
97
+ --accent-foreground: oklch(0.985 0 0);
98
+ --destructive: oklch(0.704 0.191 22.216);
99
+ --border: oklch(1 0 0 / 10%);
100
+ --input: oklch(1 0 0 / 15%);
101
+ --ring: oklch(0.556 0 0);
102
+ --chart-1: oklch(0.488 0.243 264.376);
103
+ --chart-2: oklch(0.696 0.17 162.48);
104
+ --chart-3: oklch(0.769 0.188 70.08);
105
+ --chart-4: oklch(0.627 0.265 303.9);
106
+ --chart-5: oklch(0.645 0.246 16.439);
107
+ --sidebar: oklch(0.205 0 0);
108
+ --sidebar-foreground: oklch(0.985 0 0);
109
+ --sidebar-primary: oklch(0.488 0.243 264.376);
110
+ --sidebar-primary-foreground: oklch(0.985 0 0);
111
+ --sidebar-accent: oklch(0.269 0 0);
112
+ --sidebar-accent-foreground: oklch(0.985 0 0);
113
+ --sidebar-border: oklch(1 0 0 / 10%);
114
+ --sidebar-ring: oklch(0.556 0 0);
115
+ }
116
+
117
+ @layer base {
118
+ * {
119
+ @apply border-border outline-ring/50;
120
+ }
121
+ body {
122
+ @apply bg-background text-foreground;
123
+ }
124
+ }
@@ -0,0 +1,35 @@
1
+ import Link from "next/link";
2
+
3
+ import { LogOutButton } from "@/components/logout-button";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardDescription,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+
13
+ const Page = () => {
14
+ return (
15
+ <div className="flex min-h-screen w-full items-center justify-center">
16
+ <Card className="w-full max-w-xl rounded">
17
+ <CardHeader className="text-center">
18
+ <CardTitle className="text-2xl">Welcome to Home Page</CardTitle>
19
+ <CardDescription className="my-3 text-lg">
20
+ This is a protected page or routes. Only the authenticated user can
21
+ access this page. Customize this page depends on your needs
22
+ </CardDescription>
23
+ </CardHeader>
24
+ <CardFooter className="flex justify-center gap-3">
25
+ <LogOutButton />
26
+ <Link href={"/change-password"}>
27
+ <Button variant="outline">Change Password</Button>
28
+ </Link>
29
+ </CardFooter>
30
+ </Card>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default Page;
@@ -0,0 +1,35 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Authentication Form",
17
+ description:
18
+ "This is my authentication form template using NextJS with supabase",
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html lang="en">
28
+ <body
29
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30
+ >
31
+ {children}
32
+ </body>
33
+ </html>
34
+ );
35
+ }
@@ -0,0 +1,9 @@
1
+ import { PageNotFound } from "@/components/page-not-found";
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <div className="flex min-h-screen w-full items-center justify-center">
6
+ <PageNotFound />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,11 @@
1
+ import { Hero } from "@/components/hero";
2
+
3
+ const Main = () => {
4
+ return (
5
+ <main>
6
+ <Hero />
7
+ </main>
8
+ );
9
+ };
10
+
11
+ export default Main;
@@ -0,0 +1,197 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useState } from "react";
5
+
6
+ import { EyeIcon, EyeOffIcon } from "lucide-react";
7
+ import { Controller } from "react-hook-form";
8
+ import z from "zod";
9
+
10
+ import { useAuthForm } from "@/hooks/use-auth-form";
11
+ import { createClient } from "@/lib/supabase/client";
12
+ import { UpdatePasswordSchema } from "@/schema/form-schema";
13
+
14
+ import { Button } from "../ui/button";
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from "../ui/card";
22
+ import {
23
+ Field,
24
+ FieldContent,
25
+ FieldError,
26
+ FieldGroup,
27
+ FieldLabel,
28
+ } from "../ui/field";
29
+ import { Input } from "../ui/input";
30
+ import {
31
+ InputGroup,
32
+ InputGroupAddon,
33
+ InputGroupInput,
34
+ } from "../ui/input-group";
35
+ import { Spinner } from "../ui/spinner";
36
+
37
+ export const ChangePasswordForm = () => {
38
+ const { updatepasswordform: form } = useAuthForm();
39
+
40
+ const [open, setOpen] = useState<boolean>(false);
41
+ const [loading, setLoading] = useState<boolean>(false);
42
+ const [success, setSuccess] = useState<boolean>(false);
43
+ const [error, setError] = useState<string | null>(null);
44
+
45
+ // Show password toggle function
46
+ const isOpen = () => {
47
+ setOpen((prev) => !prev);
48
+ };
49
+
50
+ async function onSubmit(values: z.infer<typeof UpdatePasswordSchema>) {
51
+ // NOTE!!
52
+ //This change password form is only for the user who currently logged in
53
+ //This is a direct update of password so there is no link send thru the email of the user
54
+ const supabase = createClient();
55
+
56
+ setLoading(true);
57
+ setError(null);
58
+
59
+ try {
60
+ const { error } = await supabase.auth.updateUser({
61
+ password: values.password,
62
+ });
63
+
64
+ if (error) throw error;
65
+
66
+ setSuccess(true);
67
+ } catch (error) {
68
+ setError(error instanceof Error ? error.message : "An error occurred!");
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ }
73
+
74
+ if (success) {
75
+ return (
76
+ <Card className="w-full max-w-sm">
77
+ <CardHeader>
78
+ <CardTitle className="text-lg">
79
+ Password Successfully Change
80
+ </CardTitle>
81
+ <CardDescription>
82
+ <p>You can login to your account using your new password.</p>
83
+ <p className="mt-4">
84
+ Go back to
85
+ <Link
86
+ href={"/home"}
87
+ className="text-neutral-800 underline underline-offset-4 hover:text-neutral-950"
88
+ >
89
+ {" "}
90
+ Home
91
+ </Link>
92
+ </p>
93
+ </CardDescription>
94
+ </CardHeader>
95
+ </Card>
96
+ );
97
+ }
98
+
99
+ return (
100
+ // customize this depends of your needs (eg. background color, size, etc.)
101
+ // you can add props if you want too
102
+ <Card className="w-full max-w-sm">
103
+ <CardHeader>
104
+ <CardTitle className="text-lg">Change Your Password</CardTitle>
105
+ <CardDescription className="text-base">
106
+ Enter your new password below.
107
+ </CardDescription>
108
+ </CardHeader>
109
+ <CardContent>
110
+ <form id="reset-password-form" onSubmit={form.handleSubmit(onSubmit)}>
111
+ <FieldGroup>
112
+ {/* Password input */}
113
+ <Controller
114
+ control={form.control}
115
+ name="password"
116
+ render={({ field, fieldState }) => (
117
+ <Field data-invalid={fieldState.invalid}>
118
+ <FieldLabel htmlFor="new-password-input">
119
+ New Password
120
+ </FieldLabel>
121
+ <InputGroup>
122
+ <InputGroupInput
123
+ {...field}
124
+ id="new-password-input"
125
+ aria-invalid={fieldState.invalid}
126
+ placeholder="Enter new password"
127
+ autoComplete="off"
128
+ type={open ? "text" : "password"}
129
+ />
130
+ <InputGroupAddon align="inline-end">
131
+ <Button
132
+ type="button"
133
+ className="hover:text-muted-foreground hover:bg-transparent"
134
+ size="icon"
135
+ variant="ghost"
136
+ onClick={isOpen}
137
+ >
138
+ {open ? <EyeIcon /> : <EyeOffIcon />}
139
+ </Button>
140
+ </InputGroupAddon>
141
+ </InputGroup>
142
+ {fieldState.invalid && (
143
+ <FieldError errors={[fieldState.error]} />
144
+ )}
145
+ </Field>
146
+ )}
147
+ />
148
+
149
+ {/* Confirm Password input */}
150
+ <Controller
151
+ control={form.control}
152
+ name="confirmPassword"
153
+ render={({ field, fieldState }) => (
154
+ <Field data-invalid={fieldState.invalid}>
155
+ <FieldLabel htmlFor="confirm-password-input">
156
+ Confirm Password
157
+ </FieldLabel>
158
+ <Input
159
+ {...field}
160
+ id="confirm-password-input"
161
+ aria-invalid={fieldState.invalid}
162
+ placeholder="Confirm password"
163
+ autoComplete="off"
164
+ type="password"
165
+ />
166
+ {fieldState.invalid && (
167
+ <FieldError errors={[fieldState.error]} />
168
+ )}
169
+ </Field>
170
+ )}
171
+ />
172
+ {error && <p className="text-red-500">{error}</p>}
173
+ <FieldContent className="flex flex-row justify-end">
174
+ <Button
175
+ type="submit"
176
+ form="reset-password-form"
177
+ disabled={loading}
178
+ >
179
+ {loading ? (
180
+ <>
181
+ <Spinner /> Saving...
182
+ </>
183
+ ) : (
184
+ "Save password"
185
+ )}
186
+ </Button>
187
+
188
+ <Link href={"/home"}>
189
+ <Button variant="outline">Cancel</Button>
190
+ </Link>
191
+ </FieldContent>
192
+ </FieldGroup>
193
+ </form>
194
+ </CardContent>
195
+ </Card>
196
+ );
197
+ };