feat: add authentication pages and layouts
This commit is contained in:
parent
0298d48f8d
commit
fd30aaecf4
BIN
public/placeholder.svg
Normal file
BIN
public/placeholder.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
15
src/app/(marketing)/auth/error/page.tsx
Normal file
15
src/app/(marketing)/auth/error/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { ErrorCard } from "@/components/auth/error-card";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Auth Error",
|
||||
description: "Auth Error",
|
||||
canonicalUrl: `${siteConfig.url}/auth/error`,
|
||||
});
|
||||
|
||||
const AuthErrorPage = () => {
|
||||
return <ErrorCard />;
|
||||
};
|
||||
|
||||
export default AuthErrorPage;
|
37
src/app/(marketing)/auth/layout.tsx
Normal file
37
src/app/(marketing)/auth/layout.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import BackButtonSmall from "@/components/shared/back-button-small";
|
||||
import Image from "next/image";
|
||||
|
||||
/**
|
||||
* auth layout is different from other public layouts,
|
||||
* so auth directory is not put in (public) directory.
|
||||
*
|
||||
* https://ui.shadcn.com/blocks#authentication-04
|
||||
*/
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full lg:grid lg:min-h-screen lg:grid-cols-2">
|
||||
{/* auth form */}
|
||||
<div className="flex items-center justify-center relative w-full h-full min-h-screen">
|
||||
<BackButtonSmall className="absolute top-6 left-6" />
|
||||
<div className="w-full max-w-md px-4">{children}</div>
|
||||
</div>
|
||||
|
||||
{/* brand image */}
|
||||
<div className="hidden bg-muted lg:block">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
width="1920"
|
||||
height="1080"
|
||||
className="h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
5
src/app/(marketing)/auth/loading.tsx
Normal file
5
src/app/(marketing)/auth/loading.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader2Icon className="my-32 mx-auto size-6 animate-spin" />;
|
||||
}
|
15
src/app/(marketing)/auth/login/page.tsx
Normal file
15
src/app/(marketing)/auth/login/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { LoginForm } from "@/components/auth/login-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Login",
|
||||
description: "Login to your account",
|
||||
canonicalUrl: `${siteConfig.url}/auth/login`,
|
||||
});
|
||||
|
||||
const LoginPage = () => {
|
||||
return <LoginForm className="border-none" />;
|
||||
};
|
||||
|
||||
export default LoginPage;
|
15
src/app/(marketing)/auth/new-password/page.tsx
Normal file
15
src/app/(marketing)/auth/new-password/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { NewPasswordForm } from "@/components/auth/new-password-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "New Password",
|
||||
description: "Set a new password",
|
||||
canonicalUrl: `${siteConfig.url}/auth/new-password`,
|
||||
});
|
||||
|
||||
const NewPasswordPage = () => {
|
||||
return <NewPasswordForm />;
|
||||
};
|
||||
|
||||
export default NewPasswordPage;
|
15
src/app/(marketing)/auth/new-verification/page.tsx
Normal file
15
src/app/(marketing)/auth/new-verification/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { NewVerificationForm } from "@/components/auth/new-verification-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "New Verification",
|
||||
description: "New Verification",
|
||||
canonicalUrl: `${siteConfig.url}/auth/new-verification`,
|
||||
});
|
||||
|
||||
const NewVerificationPage = () => {
|
||||
return <NewVerificationForm />;
|
||||
};
|
||||
|
||||
export default NewVerificationPage;
|
15
src/app/(marketing)/auth/register/page.tsx
Normal file
15
src/app/(marketing)/auth/register/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { RegisterForm } from "@/components/auth/register-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Register",
|
||||
description: "Create an account to get started",
|
||||
canonicalUrl: `${siteConfig.url}/auth/register`,
|
||||
});
|
||||
|
||||
const RegisterPage = () => {
|
||||
return <RegisterForm />;
|
||||
};
|
||||
|
||||
export default RegisterPage;
|
15
src/app/(marketing)/auth/reset/page.tsx
Normal file
15
src/app/(marketing)/auth/reset/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { ResetForm } from "@/components/auth/reset-form";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Reset Password",
|
||||
description: "Reset your password",
|
||||
canonicalUrl: `${siteConfig.url}/auth/reset`,
|
||||
});
|
||||
|
||||
const ResetPage = () => {
|
||||
return <ResetForm />;
|
||||
};
|
||||
|
||||
export default ResetPage;
|
@ -6,7 +6,7 @@ export default function Home() {
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
src="/logo.png"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
|
16
src/app/login/layout.tsx
Normal file
16
src/app/login/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
export const metadata = {
|
||||
title: 'Next.js',
|
||||
description: 'Generated by Next.js',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
37
src/components/shared/back-button-small.tsx
Normal file
37
src/components/shared/back-button-small.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface BackButtonSmallProps {
|
||||
href?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function BackButtonSmall({
|
||||
href,
|
||||
className,
|
||||
}: BackButtonSmallProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={cn("size-8 px-0", className)}
|
||||
asChild
|
||||
>
|
||||
{/* if href is provided, use it, otherwise use the router.back() */}
|
||||
<Link href={href || "#"} onClick={handleBack}>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
16
src/components/shared/form-error.tsx
Normal file
16
src/components/shared/form-error.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
|
||||
interface FormErrorProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const FormError = ({ message }: FormErrorProps) => {
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
|
||||
<TriangleAlertIcon className="h-4 w-4" />
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
16
src/components/shared/form-success.tsx
Normal file
16
src/components/shared/form-success.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { CircleCheckIcon } from "lucide-react";
|
||||
|
||||
interface FormSuccessProps {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const FormSuccess = ({ message }: FormSuccessProps) => {
|
||||
if (!message) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-emerald-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-emerald-500">
|
||||
<CircleCheckIcon className="h-4 w-4" />
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import type { SiteConfig } from "@/types";
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_APP_URL;
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
name: "Mkdirs",
|
||||
|
69
src/lib/metadata.ts
Normal file
69
src/lib/metadata.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { siteConfig } from "@/config/site";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
/**
|
||||
* Construct the metadata object for the current page (in docs/guides)
|
||||
*/
|
||||
export function constructMetadata({
|
||||
title = siteConfig.name,
|
||||
description = siteConfig.description,
|
||||
canonicalUrl,
|
||||
image = siteConfig.image,
|
||||
noIndex = false,
|
||||
}: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
canonicalUrl?: string;
|
||||
image?: string;
|
||||
noIndex?: boolean;
|
||||
} = {}): Metadata {
|
||||
const fullTitle = title
|
||||
? `${title} - ${siteConfig.title}`
|
||||
: siteConfig.title;
|
||||
return {
|
||||
title: fullTitle,
|
||||
description,
|
||||
keywords: siteConfig.keywords,
|
||||
creator: siteConfig.author,
|
||||
authors: [
|
||||
{
|
||||
name: siteConfig.author,
|
||||
},
|
||||
],
|
||||
alternates: canonicalUrl
|
||||
? {
|
||||
canonical: canonicalUrl,
|
||||
}
|
||||
: undefined,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: siteConfig.url,
|
||||
title: fullTitle,
|
||||
description,
|
||||
siteName: title,
|
||||
images: [image],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [image],
|
||||
site: siteConfig.url,
|
||||
creator: siteConfig.author,
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-32x32.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
metadataBase: new URL(siteConfig.url),
|
||||
manifest: `${siteConfig.url}/site.webmanifest`,
|
||||
...(noIndex && {
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
213
src/lib/schemas.ts
Normal file
213
src/lib/schemas.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import * as z from "zod";
|
||||
|
||||
/**
|
||||
* newsletter schema
|
||||
*/
|
||||
export const NewsletterFormSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Enter a valid email",
|
||||
}),
|
||||
});
|
||||
|
||||
export type NewsletterFormData = z.infer<typeof NewsletterFormSchema>;
|
||||
|
||||
/**
|
||||
* submit item
|
||||
*/
|
||||
export const SubmitSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { message: "Name is required" })
|
||||
.max(32, { message: "Name must be 32 or fewer characters long" }),
|
||||
link: z.string().url({ message: "Invalid url" }),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, { message: "Description is required" })
|
||||
.max(256, { message: "Description must be 256 or fewer characters long" }),
|
||||
introduction: z
|
||||
.string()
|
||||
.min(1, { message: "Introduction is required" })
|
||||
.max(4096, {
|
||||
message: "Introduction must be 4096 or fewer characters long",
|
||||
}),
|
||||
tags: z.array(z.string()).min(1, { message: "Must select at least one tag" }),
|
||||
categories: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Must select at least one category" }),
|
||||
imageId: z.string().min(1, { message: "Must upload an image" }),
|
||||
});
|
||||
|
||||
/**
|
||||
* edit item
|
||||
*/
|
||||
export const EditSchema = SubmitSchema.extend({
|
||||
id: z.string().min(1, { message: "ID is required" }),
|
||||
pricePlan: z.string().min(1, { message: "Price plan is required" }),
|
||||
planStatus: z.string().min(1, { message: "Plan status is required" }),
|
||||
});
|
||||
|
||||
/**
|
||||
* account settings
|
||||
*/
|
||||
export const SettingsSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
link: z.string().optional(),
|
||||
password: z.optional(z.string().min(6)),
|
||||
newPassword: z.optional(z.string().min(6)),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.password && !data.newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "New password is required!",
|
||||
path: ["newPassword"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.newPassword && !data.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Password is required!",
|
||||
path: ["password"],
|
||||
},
|
||||
);
|
||||
|
||||
export const UserNameSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { message: "Name is required" })
|
||||
.max(32, { message: "Name must be 32 or fewer characters long" }),
|
||||
});
|
||||
|
||||
export type UserNameData = z.infer<typeof UserNameSchema>;
|
||||
|
||||
export const UserLinkSchema = z.object({
|
||||
link: z
|
||||
.string()
|
||||
.min(0, { message: "Link is optional" })
|
||||
.max(128, { message: "Link must be 128 or fewer characters long" }),
|
||||
});
|
||||
|
||||
export type UserLinkData = z.infer<typeof UserLinkSchema>;
|
||||
|
||||
export const UserPasswordSchema = z
|
||||
.object({
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
}),
|
||||
newPassword: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
}),
|
||||
confirmPassword: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.newPassword && !data.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Password is required",
|
||||
path: ["password"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.password && !data.newPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "New password is required",
|
||||
path: ["newPassword"],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.newPassword !== data.confirmPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
},
|
||||
);
|
||||
|
||||
export type UserPasswordData = z.infer<typeof UserPasswordSchema>;
|
||||
|
||||
/**
|
||||
* auth related schemas
|
||||
*/
|
||||
export const NewPasswordSchema = z.object({
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum of 6 characters required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ResetSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const LoginSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email is required",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const RegisterSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: "Email is required",
|
||||
}),
|
||||
password: z.string().min(6, {
|
||||
message: "Minimum 6 characters required",
|
||||
}),
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export const ActivateSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "GitHub username is required",
|
||||
}),
|
||||
license: z.string().min(1, {
|
||||
message: "License key is required",
|
||||
}),
|
||||
});
|
||||
|
||||
export type ActivateFormData = z.infer<typeof ActivateSchema>;
|
||||
|
||||
/**
|
||||
* og image schema
|
||||
*/
|
||||
export const ogImageSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
mode: z.enum(["light", "dark"]).default("light"),
|
||||
});
|
Loading…
Reference in New Issue
Block a user