Add login page

pull/3813/head
Kamran Ahmed 2 years ago
parent 1950404b20
commit 3cd8468c8f
  1. 102
      src/components/AuthenticationFlow/EmailLoginForm.tsx
  2. 6
      src/components/AuthenticationFlow/EmailSignupForm.tsx
  3. 4
      src/components/AuthenticationFlow/VerificationEmailMessage.tsx
  4. 210
      src/components/Login/EmailLoginForm.tsx
  5. 2
      src/components/Login/LoginCopmponent.astro
  6. 50
      src/pages/login.astro
  7. 15
      src/pages/signup.astro

@ -0,0 +1,102 @@
import Cookies from 'js-cookie';
import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { TOKEN_COOKIE_NAME } from '../../lib/constants';
import Spinner from '../Spinner';
const EmailLoginForm: FunctionComponent<{}> = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleFormSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError('');
const res = await fetch('http://localhost:8080/v1-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
});
const json = await res.json();
if (json.type === 'user_not_verified') {
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
)}`;
return;
}
if (!json.token) {
setIsLoading(false);
setError(json.message || 'Something went wrong. Please try again later.');
return;
}
// If the response is ok, we'll set the token in a cookie
Cookies.set(TOKEN_COOKIE_NAME, json.token);
window.location.href = '/';
};
return (
<form className="w-full" onSubmit={handleFormSubmit}>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
name="email"
type="email"
autoComplete="email"
required
className="block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Email Address"
value={email}
onInput={(e) => setEmail(String((e.target as any).value))}
/>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
name="password"
type="password"
autoComplete="current-password"
required
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Password"
value={password}
onInput={(e) => setPassword(String((e.target as any).value))}
/>
<p class="mb-3 mt-2 text-sm text-gray-500">
<a
href="/forgot-password"
className="text-blue-800 hover:text-blue-600"
>
Reset your password?
</a>
</p>
{error && <p className="bg-red-100 text-red-800">{error}</p>}
<button
type="submit"
disabled={isLoading}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Continue'}
</button>
</form>
);
};
export default EmailLoginForm;

@ -19,13 +19,17 @@ const EmailSignupForm: FunctionComponent = () => {
return; return;
} }
window.location.href = `/verification-pending?email=${encodeURIComponent(email)}`; window.location.href = `/verification-pending?email=${encodeURIComponent(
email
)}`;
}; };
const onSubmit = (e: Event) => { const onSubmit = (e: Event) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
setError('');
fetch(`${import.meta.env.PUBLIC_API_URL}/v1-register`, { fetch(`${import.meta.env.PUBLIC_API_URL}/v1-register`, {
method: 'POST', method: 'POST',
headers: { headers: {

@ -49,7 +49,7 @@ export function VerificationEmailMessage() {
</h2> </h2>
<div class="text-sm sm:text-base"> <div class="text-sm sm:text-base">
<p> <p>
We have you an email at <span className="font-bold">{email}</span>. We have sent you an email at <span className="font-bold">{email}</span>.
Please click the link to verify your account. This link will expire Please click the link to verify your account. This link will expire
shortly, so please verify soon! shortly, so please verify soon!
</p> </p>
@ -77,7 +77,7 @@ export function VerificationEmailMessage() {
</> </>
)} )}
{isEmailResent && <p class="text-green-700">Email sent!</p>} {isEmailResent && <p class="text-green-700">Verification email has been sent!</p>}
</div> </div>
</div> </div>
); );

@ -1,210 +0,0 @@
import Cookies from 'js-cookie';
import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { TOKEN_COOKIE_NAME } from '../../lib/constants';
import Spinner from '../Spinner';
const EmailLoginForm: FunctionComponent<{}> = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [message, setMessage] = useState<{
type: 'success' | 'error' | 'verification' | 'warning';
message: string;
} | null>(null);
const handleFormSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setMessage(null);
// Check if the verification-email-sent-at is less than 5 seconds ago
const verificationEmailSentAt = localStorage.getItem(
'verification-email-sent-at'
);
if (verificationEmailSentAt) {
const now = new Date();
if (Number(verificationEmailSentAt) > now.getTime()) {
setIsLoading(false);
setEmail('');
setPassword('');
return setMessage({
type: 'error',
message: 'Please wait before sending another verification email.',
});
}
}
const res = await fetch('http://localhost:8080/v1-login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
});
// TODO: Remove this on production
// Simulate slow network - 2 seconds
// await new Promise((resolve) => setTimeout(resolve, 2000));
const json = await res.json();
// If the response isn't ok, we'll throw an error
if (json.type === 'user_not_verified') {
setIsLoading(false);
return setMessage({
type: 'verification',
message:
'Your account is not verified. Please click the verification link in your email. Or resend verification email.',
});
}
if (json.token) {
// If the response is ok, we'll set the token in a cookie
Cookies.set(TOKEN_COOKIE_NAME, json.token);
window.location.href = '/';
} else {
setMessage({
type: 'error',
message: json.message,
});
}
setIsLoading(false);
};
const handleResendVerificationEmail = async () => {
setIsLoading(true);
// Check if the verification-email-sent-at is less than 5 seconds ago
const verificationEmailSentAt = localStorage.getItem(
'verification-email-sent-at'
);
if (verificationEmailSentAt) {
const now = new Date();
if (Number(verificationEmailSentAt) > now.getTime()) {
setIsLoading(false);
return setMessage({
type: 'error',
message: 'Please wait before sending another verification email.',
});
}
}
const res = await fetch(
'http://localhost:8080/v1-send-verification-email',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
}),
}
);
const json = await res.json();
// If the response isn't ok, we'll throw an error
if (!res.ok) {
return setMessage({
type: 'error',
message: json.message,
});
}
// If the response is ok, we'll set the token in a cookie
setEmail('');
setPassword('');
setMessage({
type: 'success',
message: 'Verification instructions have been sent to your email.',
});
// Current time + 5 seconds, save it to localStorage
const now = new Date();
const time = now.getTime();
const expireTime = time + 5000;
now.setTime(expireTime);
localStorage.setItem(
'verification-email-sent-at',
now.getTime().toString()
);
setIsLoading(false);
};
return (
<form className="w-full" onSubmit={handleFormSubmit}>
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
className="block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="john@example.com"
value={email}
onInput={(e) => setEmail(String((e.target as any).value))}
/>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="mt-2 block w-full appearance-none rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none transition duration-150 ease-in-out placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Enter you password"
value={password}
onInput={(e) => setPassword(String((e.target as any).value))}
/>
{message && (
<>
{message.type === 'verification' ? (
<div className="mt-2 rounded-lg bg-yellow-100 p-2 text-sm text-yellow-800">
Your account is not verified. Please click the verification link
in your email. Or{' '}
<button
type="button"
className="font-semibold text-yellow-900 underline hover:text-yellow-800 hover:no-underline"
onClick={handleResendVerificationEmail}
>
resend verification email.
</button>
</div>
) : (
<div
className={`mt-2 rounded-lg p-2 ${
message.type === 'success' && 'bg-green-100 text-green-800'
} ${message.type === 'error' && 'bg-red-100 text-red-800'} ${
message.type === 'warning' && 'bg-yellow-100 text-yellow-800'
}`}
>
{message.message}
</div>
)}
</>
)}
<button
disabled={isLoading || !email || !password}
type="submit"
className="mt-3 inline-flex h-10 w-full items-center justify-center rounded-lg border border-slate-300 bg-black p-2 text-sm font-medium text-white outline-none transition duration-150 ease-in-out focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:opacity-60"
>
{isLoading ? <Spinner className="text-white" /> : 'Continue'}
</button>
</form>
);
};
export default EmailLoginForm;

@ -1,6 +1,6 @@
--- ---
import Divider from './Divider.astro'; import Divider from './Divider.astro';
import EmailLoginForm from './EmailLoginForm'; import EmailLoginForm from '../AuthenticationFlow/EmailLoginForm';
import GithubLogin from './GithubLogin.astro'; import GithubLogin from './GithubLogin.astro';
import GoogleLogin from './GoogleLogin.astro'; import GoogleLogin from './GoogleLogin.astro';
--- ---

@ -1,33 +1,34 @@
--- ---
import CaptchaFields from '../components/Captcha/CaptchaFields.astro';
import CaptchaScripts from '../components/Captcha/CaptchaScripts.astro';
import Divider from '../components/Login/Divider.astro'; import Divider from '../components/Login/Divider.astro';
import GithubLogin from '../components/Login/GithubLogin.astro'; import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
import GoogleLogin from '../components/Login/GoogleLogin.astro'; import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
import EmailLoginForm from '../components/Login/EmailLoginForm'; import EmailLoginForm from '../components/AuthenticationFlow/EmailLoginForm';
import BaseLayout from '../layouts/BaseLayout.astro'; import SettingLayout from '../layouts/SettingLayout.astro';
--- ---
<BaseLayout <SettingLayout
title='Signup - roadmap.sh' title='Login - roadmap.sh'
description='Register yourself to receive occasional emails about new roadmaps, updates, guides and videos' description='Register yourself to receive occasional emails about new roadmaps, updates, guides and videos'
permalink={'/signup'} permalink={'/signup'}
noIndex={true} noIndex={true}
> >
<div class='container'> <div class='container'>
<div <div
class='mx-auto flex flex-col items-start justify-start py-12 sm:max-w-[400px] sm:items-center sm:justify-center' class='mx-auto flex flex-col items-start justify-start pb-28 pt-10 sm:max-w-[400px] sm:items-center sm:justify-center sm:pt-20'
> >
<div class='mb-2 text-left sm:mb-5 sm:text-center'> <div class='mb-2 text-left sm:mb-5 sm:text-center'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-4 sm:text-5xl'>Login</h1> <h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Login</h1>
<p class='text-md block text-gray-600 sm:block'> <p class='text-base hidden text-gray-600 sm:block leading-6 mb-3'>
Welcome back! Login to your account. Welcome back! Let's take you to your account.
</p>
<p class='block text-sm text-gray-600 mb-3 sm:hidden'>
Welcome back! Let's take you to your account.
</p> </p>
</div> </div>
<div class='flex w-full flex-col items-stretch space-y-2'> <div class='flex w-full flex-col gap-2'>
<GithubLogin /> <GitHubButton client:load />
<GoogleLogin /> <GoogleButton client:load />
</div> </div>
<Divider /> <Divider />
@ -37,22 +38,9 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<div class='mt-6 text-center text-sm text-slate-600'> <div class='mt-6 text-center text-sm text-slate-600'>
Don't have an account?{' '} Don't have an account?{' '}
<a href='/signup' class='font-medium text-blue-700 hover:text-blue-600'> <a href='/signup' class='font-medium text-blue-700 hover:text-blue-600'>
Sign up</a Sign up
> / <a </a>
href='/forgot-password'
class='font-medium text-blue-700 hover:text-blue-600'
>Forgot password?</a
>
</div> </div>
</div> </div>
</div> </div>
</BaseLayout> </SettingLayout>
<script>
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../lib/constants';
const token = Cookies.get(TOKEN_COOKIE_NAME);
if (token) {
window.location.href = '/';
}
</script>

@ -1,14 +1,13 @@
--- ---
import CaptchaScripts from '../components/Captcha/CaptchaScripts.astro';
import Divider from '../components/Login/Divider.astro'; import Divider from '../components/Login/Divider.astro';
import GoogleLogin from '../components/Login/GoogleLogin.astro'; import GoogleLogin from '../components/Login/GoogleLogin.astro';
import EmailSignupForm from '../components/Login/EmailSignupForm'; import EmailSignupForm from '../components/AuthenticationFlow/EmailSignupForm';
import BaseLayout from '../layouts/BaseLayout.astro'; import SettingLayout from '../layouts/SettingLayout.astro';
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton'; import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton'; import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
--- ---
<BaseLayout <SettingLayout
title='Signup - roadmap.sh' title='Signup - roadmap.sh'
description='Create an account to track your progress, showcase your skillset' description='Create an account to track your progress, showcase your skillset'
permalink={'/signup'} permalink={'/signup'}
@ -16,11 +15,11 @@ import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
> >
<div class='container'> <div class='container'>
<div <div
class='mx-auto flex flex-col items-start justify-start py-12 sm:max-w-[400px] sm:items-center sm:justify-center' class='mx-auto flex flex-col items-start justify-start pt-10 sm:pt-20 pb-28 sm:max-w-[400px] sm:items-center sm:justify-center'
> >
<div class='mb-2 text-left sm:mb-5 sm:text-center'> <div class='mb-2 text-left sm:mb-5 sm:text-center'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-4 sm:text-5xl'>Sign Up</h1> <h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Sign Up</h1>
<p class='text-md hidden text-gray-600 sm:block'> <p class='text-base hidden text-gray-600 sm:block leading-6 mb-3'>
Create an account to track your progress, showcase your skill-set and be a part of the community. Create an account to track your progress, showcase your skill-set and be a part of the community.
</p> </p>
<p class='block text-sm text-gray-600 mb-3 sm:hidden'> <p class='block text-sm text-gray-600 mb-3 sm:hidden'>
@ -46,7 +45,7 @@ import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
</div> </div>
</div> </div>
</div> </div>
</BaseLayout> </SettingLayout>
<script> <script>
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';

Loading…
Cancel
Save