Skip to content

Commit 76ae662

Browse files
Merge pull request #5 from DeveloperMK07/Auth-UI
Implement Auth UI & Layout ( Sign In and Sign Up Page )
2 parents 0b17f2f + bec2014 commit 76ae662

16 files changed

Lines changed: 1528 additions & 0 deletions

app/(auth)/layout.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Image from "next/image"
2+
import Link from "next/link"
3+
4+
const Layout = ({children} : {children:React.ReactNode} ) => {
5+
return (
6+
<main className="auth-layout">
7+
<section className="auth-left-section scrollbar-hide-default">
8+
<Link href='/' className="auth-logo">
9+
<Image src="/assets/icons/logo.svg" alt="App-logo" width={140} height={32} className="h-8 w-auto" />
10+
</Link>
11+
12+
<div className="pb-6 lg:pb-8 flex-1">
13+
{children}
14+
</div>
15+
</section>
16+
17+
<section className="auth-right-section">
18+
<div className="z-10 relative lg:mt-4 lg:mb-16">
19+
<blockquote className="auth-blockquote">
20+
"Be fearful when others are greedy, and greedy when others are fearful."
21+
22+
</blockquote>
23+
<div className="flex items-center justify-between">
24+
<div>
25+
<cite className="auth-testimonial-author">— Warren Buffett</cite>
26+
<p className="max-md:text-xs text-gray-500">Chairman and CEO of Berkshire Hathaway</p>
27+
</div>
28+
</div>
29+
</div>
30+
31+
<div className="flex-1 relative">
32+
<Image src="/assets/images/dashboard.png" alt="Dashboard" width={1440} height={1150} className="auth-dashboard-preview absolute top-0"/>
33+
</div>
34+
</section>
35+
</main>
36+
)
37+
}
38+
export default Layout

app/(auth)/sign-in/page.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client'
2+
import React from 'react'
3+
import InputField from '@/components/forms/InputField'
4+
import FooterLink from '@/components/forms/FooterLink'
5+
import { Button } from '@/components/ui/button'
6+
import { SubmitHandler, useForm } from 'react-hook-form'
7+
8+
type SignInFormData = {
9+
email: string
10+
password: string
11+
}
12+
13+
const SignIn = () => {
14+
const {
15+
register,
16+
handleSubmit,
17+
formState: { errors, isSubmitting },
18+
} = useForm<SignInFormData>({
19+
defaultValues: {
20+
email: '',
21+
password: '',
22+
},
23+
mode: 'onBlur'
24+
})
25+
26+
const onSubmit: SubmitHandler<SignInFormData> = async (data) => {
27+
try {
28+
console.log(data)
29+
} catch (e) {
30+
console.error(e)
31+
}
32+
}
33+
34+
return (
35+
<>
36+
<h1 className='form-title'>Welcome Back</h1>
37+
38+
<form onSubmit={handleSubmit(onSubmit)} className='space-y-5'>
39+
<InputField
40+
name="email"
41+
label="Email"
42+
placeholder="contact@example.com"
43+
register={register}
44+
error={errors.email}
45+
validation={{ required: 'Email is required', pattern: { value: /^\w+@\w+\.\w+$/, message: 'Invalid email format' } }}
46+
/>
47+
48+
<InputField
49+
name="password"
50+
label="Password"
51+
placeholder="Enter your password"
52+
type="password"
53+
register={register}
54+
error={errors.password}
55+
validation={{ required: 'Password is required' }}
56+
/>
57+
58+
<Button type='submit' disabled={isSubmitting} className='yellow-btn w-full mt-5'>
59+
{isSubmitting ? 'Signing In...' : 'Sign In'}
60+
</Button>
61+
62+
<FooterLink text="Don't have an account?" linkText='Sign-up' href='/sign-up' />
63+
</form>
64+
</>
65+
)
66+
}
67+
68+
export default SignIn

app/(auth)/sign-up/page.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use client'
2+
import { CountrySelectField } from '@/components/forms/CountrySelectField';
3+
import FooterLink from '@/components/forms/FooterLink';
4+
import InputField from '@/components/forms/InputField';
5+
import SelectField from '@/components/forms/SelectField';
6+
import { Button } from '@/components/ui/button';
7+
import { INVESTMENT_GOALS, PREFERRED_INDUSTRIES, RISK_TOLERANCE_OPTIONS } from '@/lib/constants';
8+
import { SubmitHandler, useForm } from 'react-hook-form'
9+
10+
const SignUp = () => {
11+
const {
12+
register,
13+
handleSubmit,
14+
watch,
15+
control,
16+
17+
formState: { errors, isSubmitting },
18+
} = useForm<SignUpFormData>({
19+
20+
defaultValues: {
21+
fullName: '',
22+
email: '',
23+
password: '',
24+
country: 'INDIA',
25+
investmentGoals: 'Growth',
26+
riskTolerance: 'Medium',
27+
preferredIndustry: 'Technology'
28+
},
29+
mode: 'onBlur'
30+
}, );
31+
32+
const onSubmit = async(data: SignUpFormData) => {
33+
try {
34+
console.log(data);
35+
}catch (e){
36+
console.error(e);
37+
}
38+
}
39+
return (
40+
<>
41+
<h1 className='form-title'>Sign Up & Personalize</h1>
42+
43+
<form onSubmit={handleSubmit(onSubmit)} className='space-y-5'>
44+
{/* Input */}
45+
<InputField
46+
name="fullName"
47+
label="Full Name"
48+
placeholder="John Doe"
49+
register={register}
50+
error={errors.fullName}
51+
validation={{ required: 'Full name is required', minLength: 2 }}
52+
/>
53+
54+
<InputField
55+
name="email"
56+
label="Email"
57+
placeholder="contact@jsmastery.com"
58+
register={register}
59+
error={errors.email}
60+
validation={{ required: 'Email name is required', pattern: /^\w+@\w+\.\w+$/, message: 'Email address is required' }}
61+
/>
62+
63+
<InputField
64+
name="password"
65+
label="Password"
66+
placeholder="Enter a strong password"
67+
type="password"
68+
register={register}
69+
error={errors.password}
70+
validation={{ required: 'Password is required', minLength: 8 }}
71+
/>
72+
73+
<CountrySelectField
74+
name="country"
75+
label="Country"
76+
control={control}
77+
error={errors.country}
78+
required
79+
/>
80+
<SelectField
81+
name="investmentGoals"
82+
label="Investment Goals"
83+
placeholder="Select your investment goal"
84+
options={INVESTMENT_GOALS}
85+
control={control}
86+
error={errors.investmentGoals}
87+
required
88+
89+
/>
90+
91+
<SelectField
92+
name="riskTolerance"
93+
label="Risk Tolerance"
94+
placeholder="Select your risk level"
95+
options={RISK_TOLERANCE_OPTIONS}
96+
control={control}
97+
error={errors.riskTolerance}
98+
required
99+
100+
/>
101+
102+
<SelectField
103+
name="preferredIndustry"
104+
label="Preferred Industry"
105+
placeholder="Select your preferred industry"
106+
options={PREFERRED_INDUSTRIES}
107+
control={control}
108+
error={errors.preferredIndustry}
109+
required
110+
111+
/>
112+
113+
<Button type='submit' disabled={isSubmitting} className='yellow-btn w-full mt-5'>
114+
{isSubmitting ? 'Creating Account' : 'Start Your Investing Journey'}
115+
</Button>
116+
117+
<FooterLink text='Already Have an Account ?' linkText='Sign-in' href='/sign-in' />
118+
</form>
119+
</>
120+
)
121+
}
122+
123+
export default SignUp
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
'use client';
3+
4+
import { useState } from 'react';
5+
import { Control, Controller, FieldError } from 'react-hook-form';
6+
import {
7+
Popover,
8+
PopoverContent,
9+
PopoverTrigger,
10+
} from '@/components/ui/popover';
11+
import {
12+
Command,
13+
CommandEmpty,
14+
CommandGroup,
15+
CommandInput,
16+
CommandItem,
17+
CommandList,
18+
} from '@/components/ui/command';
19+
import { Button } from '@/components/ui/button';
20+
import { Label } from '@/components/ui/label';
21+
import { Check, ChevronsUpDown } from 'lucide-react';
22+
import { cn } from '@/lib/utils';
23+
import countryList from 'react-select-country-list';
24+
25+
type CountrySelectProps = {
26+
name: string;
27+
label: string;
28+
control: Control<any>;
29+
error?: FieldError;
30+
required?: boolean;
31+
};
32+
33+
const CountrySelect = ({
34+
value,
35+
onChange,
36+
}: {
37+
value: string;
38+
onChange: (value: string) => void;
39+
}) => {
40+
const [open, setOpen] = useState(false);
41+
42+
// Get country options with flags
43+
const countries = countryList().getData();
44+
45+
// Helper function to get flag emoji
46+
const getFlagEmoji = (countryCode: string) => {
47+
const codePoints = countryCode
48+
.toUpperCase()
49+
.split('')
50+
.map((char) => 127397 + char.charCodeAt(0));
51+
return String.fromCodePoint(...codePoints);
52+
};
53+
54+
return (
55+
<Popover open={open} onOpenChange={setOpen}>
56+
<PopoverTrigger asChild>
57+
<Button
58+
variant='outline'
59+
role='combobox'
60+
aria-expanded={open}
61+
className='country-select-trigger'
62+
>
63+
{value ? (
64+
<span className='flex items-center gap-2'>
65+
<span>{getFlagEmoji(value)}</span>
66+
<span>{countries.find((c) => c.value === value)?.label}</span>
67+
</span>
68+
) : (
69+
'Select your country...'
70+
)}
71+
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
72+
</Button>
73+
</PopoverTrigger>
74+
<PopoverContent
75+
className='w-full p-0 bg-gray-800 border-gray-600'
76+
align='start'
77+
>
78+
<Command className='bg-gray-800 border-gray-600'>
79+
<CommandInput
80+
placeholder='Search countries...'
81+
className='country-select-input'
82+
/>
83+
<CommandEmpty className='country-select-empty'>
84+
No country found.
85+
</CommandEmpty>
86+
<CommandList className='max-h-60 bg-gray-800 scrollbar-hide-default'>
87+
<CommandGroup className='bg-gray-800'>
88+
{countries.map((country) => (
89+
<CommandItem
90+
key={country.value}
91+
value={`${country.label} ${country.value}`}
92+
onSelect={() => {
93+
onChange(country.value);
94+
setOpen(false);
95+
}}
96+
className='country-select-item'
97+
>
98+
<Check
99+
className={cn(
100+
'mr-2 h-4 w-4 text-yellow-500',
101+
value === country.value ? 'opacity-100' : 'opacity-0'
102+
)}
103+
/>
104+
<span className='flex items-center gap-2'>
105+
<span>{getFlagEmoji(country.value)}</span>
106+
<span>{country.label}</span>
107+
</span>
108+
</CommandItem>
109+
))}
110+
</CommandGroup>
111+
</CommandList>
112+
</Command>
113+
</PopoverContent>
114+
</Popover>
115+
);
116+
};
117+
118+
export const CountrySelectField = ({
119+
name,
120+
label,
121+
control,
122+
error,
123+
required = false,
124+
}: CountrySelectProps) => {
125+
return (
126+
<div className='space-y-2'>
127+
<Label htmlFor={name} className='form-label'>
128+
{label}
129+
</Label>
130+
<Controller
131+
name={name}
132+
control={control}
133+
rules={{
134+
required: required ? `Please select ${label.toLowerCase()}` : false,
135+
}}
136+
render={({ field }) => (
137+
<CountrySelect value={field.value} onChange={field.onChange} />
138+
)}
139+
/>
140+
{error && <p className='text-sm text-red-500'>{error.message}</p>}
141+
<p className='text-xs text-gray-500'>
142+
Helps us show market data and news relevant to you.
143+
</p>
144+
</div>
145+
);
146+
};

components/forms/FooterLink.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
import Link from 'next/link'
3+
import React from 'react'
4+
5+
const FooterLink = ({text, linkText , href} : FooterLinkProps) => (
6+
<div className='text-center pt-4'>
7+
<p className='text-sm text-gray-500'>
8+
{text}{` `}
9+
<Link href={href} className='footer-link'>
10+
{linkText}
11+
</Link>
12+
</p>
13+
</div>
14+
)
15+
16+
export default FooterLink

0 commit comments

Comments
 (0)