mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 05:31:56 +00:00
feat: add authentication feature
This commit is contained in:
@@ -39,3 +39,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# idea
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "multitenant-ecommerce",
|
"name": "multitenant-ecommerce",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 736 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
import { SignInView } from "@/modules/auth/ui/views/sign-in-view";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <SignInView />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SignUpView } from "@/modules/auth/ui/views/sign-up-view";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <SignUpView />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -12,109 +12,109 @@ import { Button } from '@/components/ui/button';
|
|||||||
import NavbarSidebar from './navbar-sidebar';
|
import NavbarSidebar from './navbar-sidebar';
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
weight: ['700'],
|
weight: ['700'],
|
||||||
});
|
});
|
||||||
|
|
||||||
interface NavbarItemProps {
|
interface NavbarItemProps {
|
||||||
href: string;
|
href: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navbarItems = [
|
const navbarItems = [
|
||||||
{
|
{
|
||||||
href: '/',
|
href: '/',
|
||||||
children: 'Home',
|
children: 'Home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/about',
|
href: '/about',
|
||||||
children: 'About',
|
children: 'About',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/features',
|
href: '/features',
|
||||||
children: 'Features',
|
children: 'Features',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/pricing',
|
href: '/pricing',
|
||||||
children: 'Pricing',
|
children: 'Pricing',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/contact',
|
href: '/contact',
|
||||||
children: 'Contact',
|
children: 'Contact',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => {
|
const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
||||||
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href={href}>{children}</Link>
|
<Link href={href}>{children}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
||||||
<Link href="/" className="pl-6 flex items-center">
|
<Link href="/" className="pl-6 flex items-center">
|
||||||
<span className={cn('text-5xl font-semibold', poppins.className)}>
|
<span className={cn('text-5xl font-semibold', poppins.className)}>
|
||||||
funroad
|
funroad
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<NavbarSidebar
|
<NavbarSidebar
|
||||||
items={navbarItems}
|
items={navbarItems}
|
||||||
open={isSidebarOpen}
|
open={isSidebarOpen}
|
||||||
onOpenChange={setIsSidebarOpen}
|
onOpenChange={setIsSidebarOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="items-center gap-4 hidden lg:flex">
|
<div className="items-center gap-4 hidden lg:flex">
|
||||||
{navbarItems.map((item) => (
|
{navbarItems.map((item) => (
|
||||||
<NavbarItem
|
<NavbarItem
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
isActive={pathname === item.href}
|
isActive={pathname === item.href}
|
||||||
>
|
>
|
||||||
{item.children}
|
{item.children}
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
||||||
>
|
>
|
||||||
<Link href="/sign-in">Log in</Link>
|
<Link prefetch href="/sign-in">Log in</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
||||||
>
|
>
|
||||||
<Link href="/sign-up">Start Selling</Link>
|
<Link prefetch href="/sign-up">Start Selling</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex lg:hidden items-center justify-center'>
|
<div className='flex lg:hidden items-center justify-center'>
|
||||||
<Button variant="ghost" className="size-12 border-transparent bg-white" onClick={() => setIsSidebarOpen(true)}>
|
<Button variant="ghost" className="size-12 border-transparent bg-white" onClick={() => setIsSidebarOpen(true)}>
|
||||||
<MenuIcon />
|
<MenuIcon/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Navbar;
|
export default Navbar;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { useTRPC } from "@/trpc/client";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <div>Home page</div>;
|
const trpc = useTRPC();
|
||||||
|
const { data } = useQuery(trpc.auth.session.queryOptions());
|
||||||
|
|
||||||
|
return <div>{JSON.stringify(data?.user, null, 2)}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-12
@@ -1,29 +1,36 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { DM_Sans } from 'next/font/google';
|
import { DM_Sans } from 'next/font/google';
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { TRPCReactProvider } from '@/trpc/client';
|
import { TRPCReactProvider } from '@/trpc/client';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const dmSans = DM_Sans({
|
const dmSans = DM_Sans({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Create Next App',
|
title: 'Create Next App',
|
||||||
description: 'Generated by create next app',
|
description: 'Generated by create next app',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang='en'>
|
||||||
<body className={`${dmSans.className} antialiased`}>
|
<body className={`${dmSans.className} antialiased`}>
|
||||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
<TRPCReactProvider>
|
||||||
</body>
|
{children}
|
||||||
</html>
|
<Toaster />
|
||||||
);
|
</TRPCReactProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ export const Users: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
auth: true,
|
auth: true,
|
||||||
fields: [
|
fields: [
|
||||||
// Email added by default
|
{
|
||||||
// Add more fields as needed
|
name: 'username',
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const AUTH_COOKIE = 'payload-token';
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const registerSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(3),
|
||||||
|
username: z.string()
|
||||||
|
.min(3, 'Username must be at lease 3 characters')
|
||||||
|
.max(63, 'Username must be less than 63 characters')
|
||||||
|
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/,
|
||||||
|
'Username can only contain lowercase letters, numbers and hyphens. It must start and end a letter or number'
|
||||||
|
).refine(
|
||||||
|
(val) => !val.includes('--'),
|
||||||
|
'Username cannot contain consecutive hyphens'
|
||||||
|
)
|
||||||
|
.transform((val) => val.toLowerCase()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RegisterSchema = z.infer<typeof registerSchema>;
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginSchema = z.infer<typeof loginSchema>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { headers as getHeaders, cookies as getCookies } from "next/headers";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { AUTH_COOKIE } from "@/modules/auth/constants";
|
||||||
|
import { loginSchema, registerSchema } from "@/modules/auth/schemas";
|
||||||
|
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
|
||||||
|
|
||||||
|
export const authRouter = createTRPCRouter({
|
||||||
|
session: baseProcedure.query(async ({ ctx }) => {
|
||||||
|
const headers = await getHeaders();
|
||||||
|
|
||||||
|
return await ctx.db.auth({ headers });
|
||||||
|
}),
|
||||||
|
logout: baseProcedure.mutation((async () => {
|
||||||
|
const cookies = await getCookies();
|
||||||
|
|
||||||
|
cookies.delete(AUTH_COOKIE);
|
||||||
|
})),
|
||||||
|
register: baseProcedure
|
||||||
|
.input(
|
||||||
|
registerSchema
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const existingData = await ctx.db.find({
|
||||||
|
collection: 'users',
|
||||||
|
limit: 1,
|
||||||
|
where: {
|
||||||
|
username: { equals: input.username }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingUser = existingData.docs[0];
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Username already taken',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: input.email,
|
||||||
|
username: input.username,
|
||||||
|
password: input.password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await ctx.db.login({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: input.email,
|
||||||
|
password: input.password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data.token) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'Failed to login',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = await getCookies();
|
||||||
|
cookies.set({
|
||||||
|
name: AUTH_COOKIE,
|
||||||
|
value: data.token,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
login: baseProcedure
|
||||||
|
.input(loginSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const data = await ctx.db.login({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: input.email,
|
||||||
|
password: input.password,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data.token) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'Failed to login',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = await getCookies();
|
||||||
|
cookies.set({
|
||||||
|
name: AUTH_COOKIE,
|
||||||
|
value: data.token,
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { LoginSchema, loginSchema } from "@/modules/auth/schemas";
|
||||||
|
import { useTRPC } from "@/trpc/client";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { Poppins } from "next/font/google";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['700']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SignInView = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const login = useMutation(trpc.auth.login.mutationOptions({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const form = useForm<LoginSchema>({
|
||||||
|
mode: 'all',
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (values: LoginSchema) => {
|
||||||
|
login.mutate(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-1 lg:grid-cols-5'>
|
||||||
|
<div className='bg-[#F4F4F0] h-screen w-full lg:col-span-3 overflow-y-auto'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-8 p-4 lg:p-16'>
|
||||||
|
<div className='flex items-center justify-between mb-8'>
|
||||||
|
<Link href='/'>
|
||||||
|
<span className={cn('text-2xl font-semibold', poppins.className)}>funroad</span>
|
||||||
|
</Link>
|
||||||
|
<Button asChild variant='ghost' size='sm' className='text-base border-none underline'>
|
||||||
|
<Link prefetch href='/sign-up'>
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className='text-4xl font-medium'>
|
||||||
|
Welcome back to Funroad.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name='email' render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-base'>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name='password' render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-base'>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} type='password' />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit' size='lg' variant='elevated'
|
||||||
|
className='bg-black text-white hover:bg-pink-400 hover:text-primary'
|
||||||
|
disabled={login.isPending}
|
||||||
|
>Log in</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='h-screen w-full lg:col-span-2 hidden lg:block'
|
||||||
|
style={{ backgroundImage: "url('/auth-bg.png')", backgroundSize: 'cover', backgroundPosition: 'center' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { registerSchema, RegisterSchema } from "@/modules/auth/schemas";
|
||||||
|
import { useTRPC } from "@/trpc/client";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { Poppins } from "next/font/google";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
const poppins = Poppins({
|
||||||
|
subsets: ['latin'],
|
||||||
|
weight: ['700']
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SignUpView = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const trpc = useTRPC();
|
||||||
|
const register = useMutation(trpc.auth.register.mutationOptions({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const form = useForm<RegisterSchema>({
|
||||||
|
mode: 'all',
|
||||||
|
resolver: zodResolver(registerSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
username: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (values: RegisterSchema) => {
|
||||||
|
register.mutate(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = form.watch('username');
|
||||||
|
const usernameErrors = form.formState.errors.username;
|
||||||
|
const showPreview = username && !usernameErrors;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-1 lg:grid-cols-5'>
|
||||||
|
<div className='bg-[#F4F4F0] h-screen w-full lg:col-span-3 overflow-y-auto'>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-8 p-4 lg:p-16'>
|
||||||
|
<div className='flex items-center justify-between mb-8'>
|
||||||
|
<Link href='/'>
|
||||||
|
<span className={cn('text-2xl font-semibold', poppins.className)}>funroad</span>
|
||||||
|
</Link>
|
||||||
|
<Button asChild variant='ghost' size='sm' className='text-base border-none underline'>
|
||||||
|
<Link prefetch href='/sign-in'>
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className='text-4xl font-medium'>
|
||||||
|
Join over 1,580 creators earning money on Funroad.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name='username' render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-base'>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription className={cn('hidden', showPreview && 'block')}>
|
||||||
|
Your store will be available at
|
||||||
|
{/* TODO: Use proper method to generate the preview url */}
|
||||||
|
<strong>{username}</strong>.shop.com
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name='email' render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-base'>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name='password' render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className='text-base'>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} type='password' />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit' size='lg' variant='elevated'
|
||||||
|
className='bg-black text-white hover:bg-pink-400 hover:text-primary'
|
||||||
|
disabled={register.isPending}
|
||||||
|
>Create account</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='h-screen w-full lg:col-span-2 hidden lg:block'
|
||||||
|
style={{ backgroundImage: "url('/auth-bg.png')", backgroundSize: 'cover', backgroundPosition: 'center' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -125,6 +125,7 @@ export interface UserAuthOperations {
|
|||||||
*/
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
username: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -239,6 +240,7 @@ export interface PayloadMigration {
|
|||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
*/
|
*/
|
||||||
export interface UsersSelect<T extends boolean = true> {
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
username?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
email?: T;
|
email?: T;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { buildConfig } from 'payload'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
|
|
||||||
import { Users } from './collections/Users'
|
import { Users } from './collections/Users';
|
||||||
import { Media } from './collections/Media'
|
import { Media } from './collections/Media'
|
||||||
import { Categories } from './collections/Categories'
|
import { Categories } from './collections/Categories'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { authRouter } from "@/modules/auth/server/procedures";
|
||||||
import { categoriesRouter } from '@/modules/categories/server/procedures';
|
import { categoriesRouter } from '@/modules/categories/server/procedures';
|
||||||
|
|
||||||
|
|
||||||
import { createTRPCRouter } from '../init';
|
import { createTRPCRouter } from '../init';
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
|
auth: authRouter,
|
||||||
categories: categoriesRouter,
|
categories: categoriesRouter,
|
||||||
});
|
});
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
Reference in New Issue
Block a user