Skip to content

Commit 4b54a15

Browse files
committedNov 4, 2023
feat: add new user form
1 parent e2a8ee3 commit 4b54a15

File tree

15 files changed

+1729
-3548
lines changed

15 files changed

+1729
-3548
lines changed
 

‎app/(auth)/register/page.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import Link from "next/link"
33
import { cn } from "@/lib/utils"
44
import { buttonVariants } from "@/components/ui/button"
55
import { Icons } from "@/components/icons"
6-
import { UserAuthForm } from "@/components/user-auth-form"
6+
import { UserAuthNewForm } from "@/components/user-auth-new-form"
77

88
export const metadata = {
99
title: "Create an account",
1010
description: "Create an account to get started.",
1111
}
1212

13+
14+
15+
1316
export default function RegisterPage() {
1417
return (
1518
<div className="container grid h-screen w-screen flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0">
@@ -31,10 +34,10 @@ export default function RegisterPage() {
3134
Create an account
3235
</h1>
3336
<p className="text-sm text-muted-foreground">
34-
Enter your email below to create your account
37+
Enter your details below to create your account
3538
</p>
3639
</div>
37-
<UserAuthForm />
40+
<UserAuthNewForm />
3841
<p className="px-8 text-center text-sm text-muted-foreground">
3942
By clicking continue, you agree to our{" "}
4043
<Link

‎app/(public)/page.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ export default async function IndexPage() {
2121
Follow along on Twitter
2222
</Link>
2323
<h1 className="font-heading text-3xl sm:text-5xl md:text-6xl lg:text-7xl">
24-
An example app built using Next.js 13 server components.
24+
Issue official certificates without hassle
2525
</h1>
2626
<p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8">
27-
I&apos;m building a web app with Next.js 13 and open sourcing
28-
everything. Follow along as we figure this out together.
27+
Record, edit and generate student certificates in one platform.
2928
</p>
3029
<div className="space-x-4">
3130
<Link href="/login" className={cn(buttonVariants({ size: "lg" }))}>

‎app/api/posts/route.ts

-18
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as z from "zod"
44
import { authOptions } from "@/lib/auth"
55
import { db } from "@/lib/db"
66
import { RequiresProPlanError } from "@/lib/exceptions"
7-
import { getUserSubscriptionPlan } from "@/lib/subscription"
87

98
const postCreateSchema = z.object({
109
title: z.string(),
@@ -46,23 +45,6 @@ export async function POST(req: Request) {
4645
return new Response("Unauthorized", { status: 403 })
4746
}
4847

49-
const { user } = session
50-
const subscriptionPlan = await getUserSubscriptionPlan(user.id)
51-
52-
// If user is on a free plan.
53-
// Check if user has reached limit of 3 posts.
54-
if (!subscriptionPlan?.isPro) {
55-
const count = await db.post.count({
56-
where: {
57-
authorId: user.id,
58-
},
59-
})
60-
61-
if (count >= 3) {
62-
throw new RequiresProPlanError()
63-
}
64-
}
65-
6648
const json = await req.json()
6749
const body = postCreateSchema.parse(json)
6850

‎app/layout.tsx

+16-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Toaster } from "@/components/ui/toaster"
88
import { Analytics } from "@/components/analytics"
99
import { TailwindIndicator } from "@/components/tailwind-indicator"
1010
import { ThemeProvider } from "@/components/theme-provider"
11+
import Head from "next/head"
1112

1213
const fontSans = FontSans({
1314
subsets: ["latin"],
@@ -18,12 +19,21 @@ const fontSans = FontSans({
1819
const fontHeading = localFont({
1920
src: "../assets/fonts/CalSans-SemiBold.woff2",
2021
variable: "--font-heading",
22+
preload: true,
23+
2124
})
2225

2326
interface RootLayoutProps {
2427
children: React.ReactNode
2528
}
2629

30+
export const viewport = {
31+
themeColor: [
32+
{ media: "(prefers-color-scheme: light)", color: "white" },
33+
{ media: "(prefers-color-scheme: dark)", color: "black" },
34+
],
35+
}
36+
2737
export const metadata = {
2838
title: {
2939
default: siteConfig.name,
@@ -44,23 +54,20 @@ export const metadata = {
4454
},
4555
],
4656
creator: "amjed-ali-k",
47-
themeColor: [
48-
{ media: "(prefers-color-scheme: light)", color: "white" },
49-
{ media: "(prefers-color-scheme: dark)", color: "black" },
50-
],
5157
openGraph: {
5258
type: "website",
5359
locale: "en_US",
5460
url: siteConfig.url,
5561
title: siteConfig.name,
5662
description: siteConfig.description,
5763
siteName: siteConfig.name,
64+
images: '/api/og.jpg'
5865
},
5966
twitter: {
6067
card: "summary_large_image",
6168
title: siteConfig.name,
6269
description: siteConfig.description,
63-
images: [`${siteConfig.url}/og.jpg`],
70+
images: [`/og.jpg`],
6471
creator: "@amjed_ali_k",
6572
},
6673
icons: {
@@ -69,12 +76,15 @@ export const metadata = {
6976
apple: "/apple-touch-icon.png",
7077
},
7178
manifest: `${siteConfig.url}/site.webmanifest`,
79+
metadataBase: new URL('localhost:3000')
7280
}
7381

7482
export default function RootLayout({ children }: RootLayoutProps) {
7583
return (
7684
<html lang="en" suppressHydrationWarning>
77-
<head />
85+
<Head>
86+
{" "}
87+
</Head>
7888
<body
7989
className={cn(
8090
"min-h-screen bg-background font-sans antialiased",

‎app/page.tsx

-200
This file was deleted.

‎components/icons.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MoreVertical,
1818
Pizza,
1919
Plus,
20+
ScrollText,
2021
Settings,
2122
SunMedium,
2223
Trash,
@@ -29,7 +30,7 @@ import {
2930
export type Icon = LucideIcon
3031

3132
export const Icons = {
32-
logo: Command,
33+
logo: ScrollText,
3334
close: X,
3435
spinner: Loader2,
3536
chevronLeft: ChevronLeft,

‎components/user-auth-new-form.tsx

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { useSearchParams } from "next/navigation"
5+
import { zodResolver } from "@hookform/resolvers/zod"
6+
import { signIn } from "next-auth/react"
7+
import { useForm } from "react-hook-form"
8+
import * as z from "zod"
9+
10+
import { cn } from "@/lib/utils"
11+
import { userNewAuthSchema } from "@/lib/validations/auth"
12+
import { buttonVariants } from "@/components/ui/button"
13+
import { Input } from "@/components/ui/input"
14+
import { Label } from "@/components/ui/label"
15+
import { toast } from "@/components/ui/use-toast"
16+
import { Icons } from "@/components/icons"
17+
import { db } from "@/lib/db"
18+
import { registerUser } from "@/lib/server/register"
19+
20+
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
21+
22+
type FormData = z.infer<typeof userNewAuthSchema>
23+
24+
export function UserAuthNewForm({ className, ...props }: UserAuthFormProps) {
25+
const {
26+
register,
27+
handleSubmit,
28+
formState: { errors },
29+
} = useForm<FormData>({
30+
resolver: zodResolver(userNewAuthSchema),
31+
})
32+
const [isLoading, setIsLoading] = React.useState<boolean>(false)
33+
const searchParams = useSearchParams()
34+
35+
async function onSubmit(data: FormData) {
36+
setIsLoading(true)
37+
38+
// const signInResult = await signIn("email", {
39+
// email: data.email.toLowerCase(),
40+
// redirect: false,
41+
// callbackUrl: searchParams?.get("from") || "/dashboard",
42+
// })
43+
const res = await registerUser(data)
44+
setIsLoading(false)
45+
46+
if (res !== true) {
47+
return toast({
48+
title: res,
49+
description: "Your registration request failed. Please try again.",
50+
variant: "destructive",
51+
})
52+
}
53+
54+
return toast({
55+
title: "Your account created successfully",
56+
description: "You can login once we approve your account.",
57+
})
58+
}
59+
60+
return (
61+
<div className={cn("grid gap-6", className)} {...props}>
62+
<form onSubmit={handleSubmit(onSubmit)}>
63+
<div className="grid gap-2">
64+
<div className="grid gap-1">
65+
<Label className="sr-only" htmlFor="name">
66+
Name
67+
</Label>
68+
<Input
69+
id="name"
70+
placeholder="Name"
71+
type="text"
72+
autoCapitalize="none"
73+
autoCorrect="off"
74+
disabled={isLoading}
75+
{...register("name")}
76+
/>
77+
{errors?.name && (
78+
<p className="px-1 text-xs text-red-600">
79+
{errors.name.message}
80+
</p>
81+
)}
82+
</div>
83+
<div className="grid gap-1">
84+
<Label className="sr-only" htmlFor="email">
85+
Email
86+
</Label>
87+
<Input
88+
id="email"
89+
placeholder="Email"
90+
type="email"
91+
autoCapitalize="none"
92+
autoComplete="email"
93+
autoCorrect="off"
94+
disabled={isLoading}
95+
{...register("email")}
96+
/>
97+
{errors?.email && (
98+
<p className="px-1 text-xs text-red-600">
99+
{errors.email.message}
100+
</p>
101+
)}
102+
</div>
103+
<div className="grid gap-1">
104+
<Label className="sr-only" htmlFor="password">
105+
Password
106+
</Label>
107+
<Input
108+
id="password"
109+
placeholder="Password"
110+
type="password"
111+
autoCapitalize="none"
112+
autoComplete="password"
113+
autoCorrect="off"
114+
disabled={isLoading}
115+
{...register("password")}
116+
/>
117+
{errors?.password && (
118+
<p className="px-1 text-xs text-red-600">
119+
{errors.password.message}
120+
</p>
121+
)}
122+
</div>
123+
<div className="grid gap-1">
124+
<Label className="sr-only" htmlFor="confirmPassword">
125+
ConfirmPassword
126+
</Label>
127+
<Input
128+
id="confirmPassword"
129+
placeholder="Confirm Password"
130+
type="password"
131+
autoCapitalize="none"
132+
autoComplete="none"
133+
autoCorrect="off"
134+
disabled={isLoading}
135+
{...register("confirmPassword")}
136+
/>
137+
{errors?.confirmPassword && (
138+
<p className="px-1 text-xs text-red-600">
139+
{errors.confirmPassword.message}
140+
</p>
141+
)}
142+
</div>
143+
<button className={cn(buttonVariants())} disabled={isLoading}>
144+
{isLoading && (
145+
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
146+
)}
147+
Sign up with Email
148+
</button>
149+
</div>
150+
</form>
151+
{/* <div className="relative">
152+
<div className="absolute inset-0 flex items-center">
153+
<span className="w-full border-t" />
154+
</div>
155+
<div className="relative flex justify-center text-xs uppercase">
156+
<span className="bg-background px-2 text-muted-foreground">
157+
Or continue with
158+
</span>
159+
</div>
160+
</div> */}
161+
{/* <button
162+
type="button"
163+
className={cn(buttonVariants({ variant: "outline" }))}
164+
onClick={() => {
165+
setIsGitHubLoading(true)
166+
signIn("github")
167+
}}
168+
disabled={isLoading}
169+
>
170+
{isGitHubLoading ? (
171+
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
172+
) : (
173+
<Icons.gitHub className="mr-2 h-4 w-4" />
174+
)}{" "}
175+
Github
176+
</button> */}
177+
</div>
178+
)
179+
}

‎lib/auth.ts

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export const authOptions: NextAuthOptions = {
4242
},
4343
})
4444

45+
if (!user) return null
46+
if(!user.password) return null
47+
4548
// verify password
4649
try {
4750
const res = await bcrypt.compare(credentials.password, user.password)

‎lib/server/register.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use server'
2+
3+
import { z } from "zod"
4+
import { db } from "../db"
5+
import { userNewAuthSchema } from "../validations/auth"
6+
7+
type FormData = z.infer<typeof userNewAuthSchema>
8+
9+
export async function registerUser(data: FormData) {
10+
11+
const validated = userNewAuthSchema.parse(data)
12+
13+
if (!validated) {
14+
return "Invalid data"
15+
}
16+
17+
const { name, email, password } = validated
18+
19+
20+
const existingUser = await db.user.findFirst({
21+
where: {
22+
email: email,
23+
},
24+
})
25+
if (existingUser) {
26+
return "User already exists"
27+
}
28+
29+
const user = await db.user.create({
30+
data: {
31+
name: name,
32+
email: email,
33+
password: password,
34+
isApproved: true,
35+
},
36+
})
37+
return true
38+
39+
}

‎lib/validations/auth.ts

+11
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,14 @@ import * as z from "zod"
33
export const userAuthSchema = z.object({
44
email: z.string().email(),
55
})
6+
export const userNewAuthSchema = z.object({
7+
email: z.string().email(),
8+
name: z.string().min(3).max(255),
9+
password: z.string().min(8).max(255).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/, {
10+
message: "Password must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number",
11+
}),
12+
confirmPassword: z.string().min(8).max(255),
13+
}).refine((data) => data.password === data.confirmPassword, {
14+
message: "Passwords do not match",
15+
path: ["confirmPassword"],
16+
})

‎next.config.mjs

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const nextConfig = {
88
domains: ["avatars.githubusercontent.com"],
99
},
1010
experimental: {
11-
appDir: true,
1211
serverComponentsExternalPackages: ["@prisma/client"],
1312
},
1413
}

‎package.json

+39-38
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
"postinstall": "prisma generate"
1717
},
1818
"dependencies": {
19-
"@hookform/resolvers": "^3.1.0",
20-
"@next-auth/prisma-adapter": "^1.0.6",
21-
"@prisma/client": "^4.13.0",
19+
"@hookform/resolvers": "^3.3.2",
20+
"@next-auth/prisma-adapter": "^1.0.7",
21+
"@prisma/client": "^4.16.2",
2222
"@radix-ui/react-accessible-icon": "^1.0.2",
2323
"@radix-ui/react-accordion": "^1.1.1",
2424
"@radix-ui/react-alert-dialog": "^1.0.3",
@@ -47,55 +47,56 @@
4747
"@radix-ui/react-toggle": "^1.0.2",
4848
"@radix-ui/react-toggle-group": "^1.0.3",
4949
"@radix-ui/react-tooltip": "^1.0.5",
50-
"@t3-oss/env-nextjs": "^0.2.2",
51-
"@typescript-eslint/parser": "^5.59.0",
50+
"@t3-oss/env-nextjs": "^0.7.1",
51+
"@typescript-eslint/parser": "^5.62.0",
5252
"@vercel/analytics": "^1.0.0",
53-
"@vercel/og": "^0.0.21",
53+
"@vercel/og": "^0.5.20",
54+
"axios": "^1.6.0",
5455
"bcrypt": "^5.1.1",
55-
"class-variance-authority": "^0.4.0",
56+
"class-variance-authority": "^0.7.0",
5657
"clsx": "^1.2.1",
5758
"cmdk": "^0.1.22",
58-
"date-fns": "^2.29.3",
59-
"lucide-react": "^0.92.0",
60-
"next": "13.3.2-canary.13",
61-
"next-auth": "4.22.1",
59+
"date-fns": "^2.30.0",
60+
"lucide-react": "^0.292.0",
61+
"next": "14.0.1",
62+
"next-auth": "4.24.4",
6263
"next-themes": "^0.2.1",
63-
"nodemailer": "^6.9.1",
64+
"nodemailer": "^6.9.7",
6465
"prop-types": "^15.8.1",
6566
"react": "^18.2.0",
66-
"react-day-picker": "^8.7.1",
67+
"react-day-picker": "^8.9.1",
6768
"react-dom": "^18.2.0",
6869
"react-editor-js": "^2.1.0",
69-
"react-hook-form": "^7.43.9",
70-
"react-textarea-autosize": "^8.4.1",
71-
"sharp": "^0.31.3",
72-
"tailwind-merge": "^1.12.0",
73-
"tailwindcss-animate": "^1.0.5",
74-
"zod": "^3.21.4"
70+
"react-hook-form": "^7.47.0",
71+
"react-textarea-autosize": "^8.5.3",
72+
"sharp": "^0.32.6",
73+
"tailwind-merge": "^2.0.0",
74+
"tailwindcss-animate": "^1.0.7",
75+
"zod": "^3.22.4"
7576
},
7677
"devDependencies": {
77-
"@commitlint/cli": "^17.6.1",
78-
"@commitlint/config-conventional": "^17.6.1",
79-
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",
78+
"@commitlint/cli": "^18.2.0",
79+
"@commitlint/config-conventional": "^18.1.0",
80+
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
8081
"@tailwindcss/line-clamp": "^0.4.4",
81-
"@tailwindcss/typography": "^0.5.9",
82+
"@tailwindcss/typography": "^0.5.10",
8283
"@types/bcrypt": "^5.0.1",
83-
"@types/node": "^18.16.0",
84-
"@types/react": "18.0.15",
85-
"@types/react-dom": "18.0.6",
86-
"autoprefixer": "^10.4.14",
87-
"eslint": "^8.39.0",
88-
"eslint-config-next": "13.0.0",
89-
"eslint-config-prettier": "^8.8.0",
90-
"eslint-plugin-react": "^7.32.2",
91-
"eslint-plugin-tailwindcss": "^3.11.0",
84+
"@types/node": "^20.8.10",
85+
"@types/react": "18.2.34",
86+
"@types/react-dom": "18.2.14",
87+
"autoprefixer": "^10.4.16",
88+
"eslint": "^8.53.0",
89+
"eslint-config-next": "14.0.1",
90+
"eslint-config-prettier": "^9.0.0",
91+
"eslint-plugin-react": "^7.33.2",
92+
"eslint-plugin-tailwindcss": "^3.13.0",
9293
"husky": "^8.0.3",
93-
"postcss": "^8.4.23",
94-
"prettier": "^2.8.8",
95-
"prettier-plugin-tailwindcss": "^0.1.13",
94+
"postcss": "^8.4.31",
95+
"prettier": "^3.0.3",
96+
"prettier-plugin-tailwindcss": "^0.5.6",
9697
"pretty-quick": "^3.1.3",
97-
"prisma": "^4.13.0",
98-
"tailwindcss": "^3.3.1",
99-
"typescript": "4.7.4"
98+
"prisma": "^5.5.2",
99+
"tailwindcss": "^3.3.5",
100+
"typescript": "5.2.2"
100101
}
101102
}

‎pnpm-lock.yaml

+1,421-3,278
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- AlterTable
2+
ALTER TABLE "users" ADD COLUMN "approved_at" TIMESTAMP(3),
3+
ADD COLUMN "approved_by" TEXT,
4+
ADD COLUMN "is_approved" BOOLEAN NOT NULL DEFAULT false,
5+
ADD COLUMN "roles" TEXT[] DEFAULT ARRAY['USER']::TEXT[];

‎prisma/schema.prisma

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ model User {
5252
updatedAt DateTime @default(now()) @map(name: "updated_at")
5353
password String?
5454
55+
isApproved Boolean @default(false) @map(name: "is_approved")
56+
approvedBy String? @map(name: "approved_by")
57+
approvedAt DateTime? @map(name: "approved_at")
58+
59+
roles String[] @default(["USER"])
60+
5561
accounts Account[]
5662
sessions Session[]
5763
Post Post[]

0 commit comments

Comments
 (0)
Please sign in to comment.