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;
}
window.location.href = `/verification-pending?email=${encodeURIComponent(email)}`;
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
)}`;
};
const onSubmit = (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError('');
fetch(`${import.meta.env.PUBLIC_API_URL}/v1-register`, {
method: 'POST',
headers: {

@ -49,7 +49,7 @@ export function VerificationEmailMessage() {
</h2>
<div class="text-sm sm:text-base">
<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
shortly, so please verify soon!
</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>
);

@ -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 EmailLoginForm from './EmailLoginForm';
import EmailLoginForm from '../AuthenticationFlow/EmailLoginForm';
import GithubLogin from './GithubLogin.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 GithubLogin from '../components/Login/GithubLogin.astro';
import GoogleLogin from '../components/Login/GoogleLogin.astro';
import EmailLoginForm from '../components/Login/EmailLoginForm';
import BaseLayout from '../layouts/BaseLayout.astro';
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
import EmailLoginForm from '../components/AuthenticationFlow/EmailLoginForm';
import SettingLayout from '../layouts/SettingLayout.astro';
---
<BaseLayout
title='Signup - roadmap.sh'
<SettingLayout
title='Login - roadmap.sh'
description='Register yourself to receive occasional emails about new roadmaps, updates, guides and videos'
permalink={'/signup'}
noIndex={true}
>
<div class='container'>
<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'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-4 sm:text-5xl'>Login</h1>
<p class='text-md block text-gray-600 sm:block'>
Welcome back! Login to your account.
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Login</h1>
<p class='text-base hidden text-gray-600 sm:block leading-6 mb-3'>
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>
</div>
<div class='flex w-full flex-col items-stretch space-y-2'>
<GithubLogin />
<GoogleLogin />
<div class='flex w-full flex-col gap-2'>
<GitHubButton client:load />
<GoogleButton client:load />
</div>
<Divider />
@ -37,22 +38,9 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<div class='mt-6 text-center text-sm text-slate-600'>
Don't have an account?{' '}
<a href='/signup' class='font-medium text-blue-700 hover:text-blue-600'>
Sign up</a
> / <a
href='/forgot-password'
class='font-medium text-blue-700 hover:text-blue-600'
>Forgot password?</a
>
Sign up
</a>
</div>
</div>
</div>
</BaseLayout>
<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>
</SettingLayout>

@ -1,14 +1,13 @@
---
import CaptchaScripts from '../components/Captcha/CaptchaScripts.astro';
import Divider from '../components/Login/Divider.astro';
import GoogleLogin from '../components/Login/GoogleLogin.astro';
import EmailSignupForm from '../components/Login/EmailSignupForm';
import BaseLayout from '../layouts/BaseLayout.astro';
import EmailSignupForm from '../components/AuthenticationFlow/EmailSignupForm';
import SettingLayout from '../layouts/SettingLayout.astro';
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
---
<BaseLayout
<SettingLayout
title='Signup - roadmap.sh'
description='Create an account to track your progress, showcase your skillset'
permalink={'/signup'}
@ -16,11 +15,11 @@ import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
>
<div class='container'>
<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'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-4 sm:text-5xl'>Sign Up</h1>
<p class='text-md hidden text-gray-600 sm:block'>
<h1 class='mb-2 text-3xl font-semibold sm:mb-5 sm:text-5xl'>Sign Up</h1>
<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.
</p>
<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>
</BaseLayout>
</SettingLayout>
<script>
import Cookies from 'js-cookie';

Loading…
Cancel
Save