@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
@@ -0,0 +1,188 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useRouter } from "next/navigation";
5
+ import { useState } from "react";
6
+
7
+ import { EyeIcon, EyeOffIcon } from "lucide-react";
8
+ import { Controller } from "react-hook-form";
9
+ import z from "zod";
10
+
11
+ import { useAuthForm } from "@/hooks/use-auth-form";
12
+ import { createClient } from "@/lib/supabase/client";
13
+ import { LogInFormSchema } from "@/schema/form-schema";
14
+
15
+ import { Button } from "../ui/button";
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from "../ui/card";
23
+ import {
24
+ Field,
25
+ FieldDescription,
26
+ FieldError,
27
+ FieldGroup,
28
+ FieldLabel,
29
+ } from "../ui/field";
30
+ import { Input } from "../ui/input";
31
+ import {
32
+ InputGroup,
33
+ InputGroupAddon,
34
+ InputGroupInput,
35
+ } from "../ui/input-group";
36
+ import { Spinner } from "../ui/spinner";
37
+
38
+ export const LogInForm = () => {
39
+ const { loginform: form } = useAuthForm();
40
+
41
+ const router = useRouter();
42
+
43
+ const [loading, setLoading] = useState<boolean>(false);
44
+ const [open, setOpen] = useState<boolean>(false);
45
+ const [error, setError] = useState<string | null>(null);
46
+
47
+ // Show password toggle function
48
+ const isOpen = () => {
49
+ setOpen((prev) => !prev);
50
+ };
51
+
52
+ async function onSubmit(values: z.infer<typeof LogInFormSchema>) {
53
+ const supabase = createClient();
54
+ setLoading(true);
55
+ setError(null);
56
+
57
+ try {
58
+ const { error } = await supabase.auth.signInWithPassword({
59
+ email: values.email,
60
+ password: values.password,
61
+ });
62
+
63
+ if (error) throw error;
64
+
65
+ router.push("/home"); // TODO : customize this url depends on where do you want to redirect after a successful sign up
66
+ } catch (error) {
67
+ setError(error instanceof Error ? error.message : "An error occurred!");
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ }
72
+
73
+ return (
74
+ // customize this depends of your needs (eg. background color, size, etc.)
75
+ // you can add props if you want too
76
+ <Card className="w-full max-w-sm">
77
+ <CardHeader>
78
+ <CardTitle className="text-xl font-semibold">
79
+ Login to your account
80
+ </CardTitle>
81
+ <CardDescription>
82
+ Enter your credentials below to login to your account
83
+ </CardDescription>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <form id="login-form" onSubmit={form.handleSubmit(onSubmit)}>
87
+ <FieldGroup>
88
+ {/* Email input */}
89
+ <Controller
90
+ control={form.control}
91
+ name="email"
92
+ render={({ field, fieldState }) => (
93
+ <Field data-invalid={fieldState.invalid}>
94
+ <FieldLabel htmlFor="email-input">Email</FieldLabel>
95
+ <Input
96
+ {...field}
97
+ id="email-input"
98
+ aria-invalid={fieldState.invalid}
99
+ placeholder="example@mail.com"
100
+ autoComplete="off"
101
+ />
102
+
103
+ {fieldState.invalid && (
104
+ <FieldError errors={[fieldState.error]} />
105
+ )}
106
+ </Field>
107
+ )}
108
+ />
109
+
110
+ {/* Password input */}
111
+ <Controller
112
+ control={form.control}
113
+ name="password"
114
+ render={({ field, fieldState }) => (
115
+ <Field data-invalid={fieldState.invalid}>
116
+ <FieldLabel htmlFor="password-input">Password</FieldLabel>
117
+ <InputGroup>
118
+ <InputGroupInput
119
+ {...field}
120
+ id="password-input"
121
+ aria-invalid={fieldState.invalid}
122
+ type={open ? "text" : "password"}
123
+ autoComplete="off"
124
+ />
125
+
126
+ <InputGroupAddon align="inline-end">
127
+ <Button
128
+ variant="ghost"
129
+ size="icon"
130
+ className="hover:text-muted-foreground hover:bg-transparent"
131
+ type="button"
132
+ onClick={isOpen}
133
+ >
134
+ {open ? <EyeIcon /> : <EyeOffIcon />}
135
+ </Button>
136
+ </InputGroupAddon>
137
+ </InputGroup>
138
+ <FieldLabel className="flex justify-end">
139
+ <Link
140
+ href="request-reset-password"
141
+ className="font-normal underline-offset-4 hover:underline"
142
+ >
143
+ Forgot your password?
144
+ </Link>
145
+ </FieldLabel>
146
+
147
+ {fieldState.invalid && (
148
+ <FieldError errors={[fieldState.error]} />
149
+ )}
150
+ </Field>
151
+ )}
152
+ />
153
+
154
+ {/* Displaying the error that comes from the server */}
155
+ {error && <Field className="text-red-500">{error}</Field>}
156
+
157
+ <Field>
158
+ <Button
159
+ size="lg"
160
+ type="submit"
161
+ form="login-form"
162
+ disabled={loading}
163
+ >
164
+ {loading ? (
165
+ <>
166
+ <Spinner /> Logging in...
167
+ </>
168
+ ) : (
169
+ "Login"
170
+ )}
171
+ </Button>
172
+
173
+ <FieldDescription className="text-center">
174
+ Don&apos;t have an account?{" "}
175
+ <Link
176
+ href={"/signup"}
177
+ className="text-sm text-neutral-800 hover:text-black"
178
+ >
179
+ Sign up
180
+ </Link>
181
+ </FieldDescription>
182
+ </Field>
183
+ </FieldGroup>
184
+ </form>
185
+ </CardContent>
186
+ </Card>
187
+ );
188
+ };
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { useState } from "react";
5
+
6
+ import { Controller } from "react-hook-form";
7
+ import z from "zod";
8
+
9
+ import { useAuthForm } from "@/hooks/use-auth-form";
10
+ import { createClient } from "@/lib/supabase/client";
11
+ import { ReqResetPasswordSchema } from "@/schema/form-schema";
12
+
13
+ import { Button } from "../ui/button";
14
+ import {
15
+ Card,
16
+ CardContent,
17
+ CardDescription,
18
+ CardHeader,
19
+ CardTitle,
20
+ } from "../ui/card";
21
+ import { Field, FieldError, FieldGroup, FieldLabel } from "../ui/field";
22
+ import { Input } from "../ui/input";
23
+ import { Spinner } from "../ui/spinner";
24
+
25
+ export const RequestResetPassword = () => {
26
+ const { reqresetpasswordform: form } = useAuthForm();
27
+ const router = useRouter();
28
+
29
+ const [loading, setLoading] = useState<boolean>(false);
30
+ const [error, setError] = useState<null | string>(null);
31
+ const [success, setSuccess] = useState<boolean>(false);
32
+
33
+ async function onSubmit(values: z.infer<typeof ReqResetPasswordSchema>) {
34
+ const supabase = createClient();
35
+
36
+ setLoading(true);
37
+ setError(null);
38
+
39
+ try {
40
+ const { error } = await supabase.auth.resetPasswordForEmail(
41
+ values.email,
42
+ {
43
+ redirectTo: `${window.location.origin}/reset-password`,
44
+ }
45
+ );
46
+
47
+ if (error) throw error;
48
+
49
+ setSuccess(true);
50
+ } catch (error) {
51
+ setError(error instanceof Error ? error.message : "An error occurred!");
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ }
56
+
57
+ if (success) {
58
+ return (
59
+ // customize this depends of your needs (eg. background color, size, etc.)
60
+ // you can add props if you want too
61
+ <Card className="w-full max-w-md">
62
+ <CardHeader>
63
+ <CardTitle className="text-lg">Check Your Email</CardTitle>
64
+ <CardDescription className="text-base">
65
+ Reset password request link already sent to your email.
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ <p className="text-muted-foreground text-sm">
70
+ If you registered using your email and password, you will receive a
71
+ password reset email.
72
+ </p>
73
+ </CardContent>
74
+ </Card>
75
+ );
76
+ }
77
+
78
+ return (
79
+ // customize this depends of your needs (eg. background color, size, etc.)
80
+ // you can add props if you want too
81
+ <Card className="w-full max-w-sm">
82
+ <CardHeader>
83
+ <CardTitle className="text-lg">Request Reset Password</CardTitle>
84
+ <CardDescription>
85
+ Enter your email and we&apos;ll send you a link to reset your
86
+ password.
87
+ </CardDescription>
88
+ </CardHeader>
89
+ <CardContent>
90
+ <form id="email-form" onSubmit={form.handleSubmit(onSubmit)}>
91
+ <FieldGroup>
92
+ <Controller
93
+ control={form.control}
94
+ name="email"
95
+ render={({ field, fieldState }) => (
96
+ <Field data-invalid={fieldState.invalid}>
97
+ <FieldLabel htmlFor="email-input">Email</FieldLabel>
98
+
99
+ <Input
100
+ {...field}
101
+ id="email-input"
102
+ aria-invalid={fieldState.invalid}
103
+ autoComplete="off"
104
+ placeholder="example@mail.com"
105
+ />
106
+
107
+ {fieldState.invalid && (
108
+ <FieldError errors={[fieldState.error]} />
109
+ )}
110
+ </Field>
111
+ )}
112
+ />
113
+ {error && <Field className="text-red-500">{error}</Field>}
114
+ <Field>
115
+ <Button type="submit" form="email-form" disabled={loading}>
116
+ {loading ? (
117
+ <>
118
+ <Spinner /> Sending...
119
+ </>
120
+ ) : (
121
+ "Send reset email"
122
+ )}
123
+ </Button>
124
+ <Button
125
+ variant={"outline"}
126
+ onClick={() => router.push("/login")}
127
+ type="button"
128
+ >
129
+ Back to login
130
+ </Button>
131
+ </Field>
132
+ </FieldGroup>
133
+ </form>
134
+ </CardContent>
135
+ </Card>
136
+ );
137
+ };
@@ -0,0 +1,199 @@
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 ResetPasswordForm = () => {
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
+ const supabase = createClient();
52
+
53
+ setLoading(true);
54
+ setError(null);
55
+
56
+ try {
57
+ const { error } = await supabase.auth.updateUser({
58
+ password: values.password,
59
+ });
60
+
61
+ if (error) {
62
+ throw error;
63
+ }
64
+
65
+ // Force the user to login after the successful reset password
66
+ //NOTE : In this case the scope is local so that the user will only sign out in the device their
67
+ //are currently using. Use a global scope if you want the user to signout all the devices that they currently login
68
+ await supabase.auth.signOut({
69
+ scope: "local",
70
+ });
71
+
72
+ setSuccess(true);
73
+ } catch (error) {
74
+ setError(error instanceof Error ? error.message : "An error occurred!");
75
+ } finally {
76
+ setLoading(false);
77
+ }
78
+ }
79
+
80
+ if (success) {
81
+ return (
82
+ // customize this depends of your needs (eg. background color, size, etc.)
83
+ // you can add props if you want too
84
+ <Card className="w-full max-w-sm">
85
+ <CardHeader>
86
+ <CardTitle className="text-lg">Password Successfully Reset</CardTitle>
87
+ <CardDescription>
88
+ <p>You can now login to your account using your new password.</p>
89
+ <p className="mt-4">
90
+ Go back to
91
+ <Link
92
+ href={"/login"}
93
+ className="text-neutral-800 underline underline-offset-4 hover:text-neutral-950"
94
+ >
95
+ {" "}
96
+ Login
97
+ </Link>
98
+ </p>
99
+ </CardDescription>
100
+ </CardHeader>
101
+ </Card>
102
+ );
103
+ }
104
+
105
+ return (
106
+ // customize this depends of your needs (eg. background color, size, etc.)
107
+ // you can add props if you want too
108
+ <Card className="w-full max-w-sm">
109
+ <CardHeader>
110
+ <CardTitle className="text-lg">Reset Your Password</CardTitle>
111
+ <CardDescription className="text-base">
112
+ Enter your new password below.
113
+ </CardDescription>
114
+ </CardHeader>
115
+ <CardContent>
116
+ <form id="reset-password-form" onSubmit={form.handleSubmit(onSubmit)}>
117
+ <FieldGroup>
118
+ {/* Password input */}
119
+ <Controller
120
+ control={form.control}
121
+ name="password"
122
+ render={({ field, fieldState }) => (
123
+ <Field data-invalid={fieldState.invalid}>
124
+ <FieldLabel htmlFor="new-password-input">
125
+ New Password
126
+ </FieldLabel>
127
+ <InputGroup>
128
+ <InputGroupInput
129
+ {...field}
130
+ id="new-password-input"
131
+ aria-invalid={fieldState.invalid}
132
+ placeholder="Enter new password"
133
+ autoComplete="off"
134
+ type={open ? "text" : "password"}
135
+ />
136
+ <InputGroupAddon align="inline-end">
137
+ <Button
138
+ type="button"
139
+ className="hover:text-muted-foreground hover:bg-transparent"
140
+ size="icon"
141
+ variant="ghost"
142
+ onClick={isOpen}
143
+ >
144
+ {open ? <EyeIcon /> : <EyeOffIcon />}
145
+ </Button>
146
+ </InputGroupAddon>
147
+ </InputGroup>
148
+ {fieldState.invalid && (
149
+ <FieldError errors={[fieldState.error]} />
150
+ )}
151
+ </Field>
152
+ )}
153
+ />
154
+
155
+ {/* Confirm Password input */}
156
+ <Controller
157
+ control={form.control}
158
+ name="confirmPassword"
159
+ render={({ field, fieldState }) => (
160
+ <Field data-invalid={fieldState.invalid}>
161
+ <FieldLabel htmlFor="confirm-password-input">
162
+ Confirm Password
163
+ </FieldLabel>
164
+ <Input
165
+ {...field}
166
+ id="confirm-password-input"
167
+ aria-invalid={fieldState.invalid}
168
+ placeholder="Confirm password"
169
+ autoComplete="off"
170
+ type="password"
171
+ />
172
+ {fieldState.invalid && (
173
+ <FieldError errors={[fieldState.error]} />
174
+ )}
175
+ </Field>
176
+ )}
177
+ />
178
+ {error && <p className="text-red-500">{error}</p>}
179
+ <FieldContent>
180
+ <Button
181
+ type="submit"
182
+ form="reset-password-form"
183
+ disabled={loading}
184
+ >
185
+ {loading ? (
186
+ <>
187
+ <Spinner /> Saving...
188
+ </>
189
+ ) : (
190
+ "Save new password"
191
+ )}
192
+ </Button>
193
+ </FieldContent>
194
+ </FieldGroup>
195
+ </form>
196
+ </CardContent>
197
+ </Card>
198
+ );
199
+ };