@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.
- package/cli.js +95 -0
- package/package.json +27 -0
- package/template/.env.local_example +5 -0
- package/template/src/app/(auth)/change-password/page.tsx +11 -0
- package/template/src/app/(auth)/login/page.tsx +11 -0
- package/template/src/app/(auth)/request-reset-password/page.tsx +11 -0
- package/template/src/app/(auth)/reset-password/page.tsx +11 -0
- package/template/src/app/(auth)/signup/page.tsx +11 -0
- package/template/src/app/(auth)/signup-success/page.tsx +34 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +124 -0
- package/template/src/app/home/page.tsx +35 -0
- package/template/src/app/layout.tsx +35 -0
- package/template/src/app/not-found.tsx +9 -0
- package/template/src/app/page.tsx +11 -0
- package/template/src/components/forms/change-password.tsx +197 -0
- package/template/src/components/forms/login-form.tsx +188 -0
- package/template/src/components/forms/request-reset-password-form.tsx +137 -0
- package/template/src/components/forms/reset-password-form.tsx +199 -0
- package/template/src/components/forms/signup-form.tsx +231 -0
- package/template/src/components/hero.tsx +38 -0
- package/template/src/components/logout-button.tsx +52 -0
- package/template/src/components/page-not-found.tsx +32 -0
- package/template/src/components/ui/button.tsx +65 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/field.tsx +249 -0
- package/template/src/components/ui/input-group.tsx +171 -0
- package/template/src/components/ui/input.tsx +21 -0
- package/template/src/components/ui/label.tsx +25 -0
- package/template/src/components/ui/separator.tsx +29 -0
- package/template/src/components/ui/spinner.tsx +16 -0
- package/template/src/components/ui/textarea.tsx +18 -0
- package/template/src/hooks/use-auth-form.ts +47 -0
- package/template/src/lib/supabase/client.ts +8 -0
- package/template/src/lib/supabase/proxy.ts +55 -0
- package/template/src/lib/supabase/server.ts +34 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/proxy.ts +21 -0
- 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,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,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'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,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
|
+
};
|