Skip to content

Commit

Permalink
feat: add self signup page for new users (#34)
Browse files Browse the repository at this point in the history
* feat: create signup page

* feat: add invite funcitonality to users table
  • Loading branch information
fgmadeira authored Feb 29, 2024
1 parent a847160 commit f213722
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import React from "react";

import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import UserForm from '@/components/user/user-form';
import { useRouter } from 'next/navigation';
import UserInviteForm from '@/components/user/user-invitation-form';

export default function NewUserModal() {
const router = useRouter();
Expand All @@ -17,7 +17,7 @@ export default function NewUserModal() {
Create a new user
</DialogDescription>
</DialogHeader>
<UserForm onComplete={router.back}/>
<UserInviteForm />
</DialogContent>
</Dialog>
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Separator } from "@/components/ui/separator";
import React from 'react';
import UserForm from '@/components/user/user-form';
import UserInviteForm from '@/components/user/user-invitation-form';

export default async function NewUser({ params }: any) {
return (
<div className="container space-y-6">
<div>
<h3 className="text-lg font-medium">Projects</h3>
<h3 className="text-lg font-medium">Invites</h3>
<p className="text-sm text-muted-foreground">
Create a new User
Invite a new User
</p>
</div>
<Separator />
<UserForm />
<UserInviteForm />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async function Teams() {
)
}>
<PlusCircledIcon className="mr-2 h-4 w-4" />
Create user
Invite user
</Link>
}
columns={columns}
Expand Down
10 changes: 6 additions & 4 deletions ethereal-nexus-dashboard/src/app/(layout)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React from "react";
import DashboardLayout from "@/components/layout";
import { ThemeProvider } from '@/components/theme-provider';

export default async function RootLayout({
children,
}: {
children,
}: {
children: React.ReactNode;
}) {
const env = process.env.NODE_ENV
const env = process.env.NODE_ENV

return <DashboardLayout>{children}</DashboardLayout>;
return <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<DashboardLayout>{children}</DashboardLayout> </ThemeProvider>;
}
4 changes: 1 addition & 3 deletions ethereal-nexus-dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ export default async function RootLayout({
<NewRelicSnippet/>
</head>): null}
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
{children}
</body>
</html>;
}
45 changes: 45 additions & 0 deletions ethereal-nexus-dashboard/src/app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Metadata } from 'next';
import Link from 'next/link';

import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
import { Meteors } from '@/components/ui/meteors';
import UserForm from '@/components/user/user-form';

export const metadata: Metadata = {
title: "Authentication",
description: "Authentication forms built using the components.",
}

export default function AuthenticationPage() {
return (
<div
className="container relative hidden h-screen flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<Link
href="/api/auth/signin"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Login
</Link>
<div className="relative hidden h-full flex-col bg-slate-600 p-10 text-white lg:flex dark:border-r">
<Meteors />
</div>
<div className="lg:p-8">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-sm text-muted-foreground">
Enter your email below to create your account
</p>
</div>
<UserForm />
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useCallback, useState } from 'react'

type CopiedValue = string | null
type CopyFn = (text: string) => Promise<boolean>


export function useCopyToClipboard(): [CopiedValue, CopyFn] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null)

const copy: CopyFn = useCallback(async text => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return false
}

// Try to save to clipboard then save it in the state if worked
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
return true
} catch (error) {
console.warn('Copy failed', error)
setCopiedText(null)
return false
}
}, [])

return [copiedText, copy]
}
32 changes: 32 additions & 0 deletions ethereal-nexus-dashboard/src/components/ui/meteors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { cn } from '@/lib/utils';

export const Meteors = ({
number,
className,
}: {
number?: number;
className?: string;
}) => {
const meteors = new Array(number || 20).fill(true);
return (
<>
{meteors.map((el, idx) => (
<span
key={"meteor" + idx}
className={cn(
"animate-meteor-effect absolute top-1/2 left-1/2 h-0.5 w-0.5 rounded-[9999px] bg-slate-500 shadow-[0_0_0_1px_#ffffff10] rotate-[215deg]",
"before:content-[''] before:absolute before:top-1/2 before:transform before:-translate-y-[50%] before:w-[50px] before:h-[1px] before:bg-gradient-to-r before:from-violet-300 before:to-transparent",
className
)}
style={{
top: 0,
left: Math.floor(Math.random() * (800 - -800) + -800) + "px",
animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + "s",
animationDuration: Math.floor(Math.random() * (10 - 2) + 2) + "s",
}}
></span>
))}
</>
);
};
31 changes: 16 additions & 15 deletions ethereal-nexus-dashboard/src/components/user/user-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, Form
import {Input} from "@/components/ui/input";
import {zodResolver} from "@hookform/resolvers/zod";
import React from "react";
import { newUserSchema } from '@/data/users/dto';
import { insertUser } from '@/data/users/actions';
import { useRouter } from 'next/navigation';
import { NewUser, newUserSchema } from '@/data/users/dto';
import { insertInvitedUser, insertUser } from '@/data/users/actions';
import { useRouter, useSearchParams } from 'next/navigation';
import { useToast } from '@/components/ui/use-toast';
import { PasswordInput } from '@/components/ui/password-input';

Expand All @@ -19,40 +19,41 @@ type UserFormProps = {
}

export default function UserForm({ onComplete }: UserFormProps) {
const searchParams = useSearchParams()
const router = useRouter();
const { toast } = useToast()

const form: any = useForm<z.infer<typeof newUserSchema>>({
const form = useForm<NewUser>({
resolver: zodResolver(newUserSchema)
});

async function handler(formdata) {
const user = await insertUser(formdata);
const user = await insertInvitedUser(formdata, searchParams.get('key'));
if (user.success) {
toast({
title: 'User created successfully!',
});
if(onComplete) onComplete();
router.push("/users");
} else {
toast({
title: 'Failed to create user.',
description: user.error.message,
});
return router.push("/api/auth/signin");
}

form.setError('email', {
type: "manual",
message: user.error.message,
})
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handler)} className="space-y-8">
<form onSubmit={form.handleSubmit(handler)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
<Input placeholder="John Doe" {...field} value={field.value ?? ''} />
</FormControl>
<FormDescription>
This is the name user.
Expand Down Expand Up @@ -84,7 +85,7 @@ export default function UserForm({ onComplete }: UserFormProps) {
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="****" {...field} />
<PasswordInput placeholder="****" {...field} value={field.value ?? ''} />
</FormControl>
<FormDescription>
Please select a password.
Expand All @@ -93,7 +94,7 @@ export default function UserForm({ onComplete }: UserFormProps) {
</FormItem>
)}
/>
<Button type="submit">Create user</Button>
<Button className='w-full' type="submit">Create user</Button>
</form>
</Form>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";

import { useForm } from 'react-hook-form';
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 { zodResolver } from '@hookform/resolvers/zod';
import React, { useCallback, useState } from 'react';
import { NewInvite, newInviteSchema } from '@/data/users/dto';
import { insertInvite } from '@/data/users/actions';
import { useToast } from '@/components/ui/use-toast';
import { Separator } from '@/components/ui/separator';
import { Clipboard, ClipboardCheck } from 'lucide-react';
import { useCopyToClipboard } from '@/components/hooks/useCopyToClipboard';

type UserInviteFormProps = {
onComplete?: () => void
}

export default function UserInviteForm({ onComplete }: UserInviteFormProps) {
const { toast } = useToast()
const [inviteKey, setKey] = useState<string | null>(null);
const [copiedText, copy] = useCopyToClipboard()

const inviteUrl = typeof window !== 'undefined' ? `${window?.location.protocol}//${window.location.host}/signup?key=${inviteKey}` : ''

const form: any = useForm<NewInvite>({
resolver: zodResolver(newInviteSchema)
});

async function handler(formdata) {
const invite = await insertInvite(formdata);
if (invite.success) {
setKey(invite.data.key);
toast({
title: 'User invite created successfully!',
});
if(onComplete) onComplete();
} else {
toast({
title: 'Failed to create invite.',
description: invite.error.message,
});
}
}

const handleCopy = useCallback(async () => {
await copy(inviteUrl)
toast({
title: 'Copied!',
});
}, [inviteUrl, copy, toast])

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handler)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="johndoe@yourcompany.com" {...field} />
</FormControl>
<FormDescription>
This is the email of the user.
</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormMessage />
{
inviteKey ?
<>
<Separator className="my-4" />
<div className="flex gap-2">
<Input className="w-full" disabled value={inviteUrl} />
<Button variant="secondary" type="button" onClick={handleCopy}>
{
copiedText ? <ClipboardCheck className="h-4 w-4" /> : <Clipboard className="h-4 w-4" />
}
</Button>
</div>
</>
: null
}
<Button className='w-full' type="submit">Create Invite</Button>
</form>
</Form>
);
}
Loading

0 comments on commit f213722

Please sign in to comment.