Refactor account pages

pull/3985/head
Kamran Ahmed 2 years ago
parent 24c262282e
commit c06c236da5
  1. 85
      src/components/AccountSidebar.astro
  2. 4
      src/components/Authenticator/authenticator.ts
  3. 5
      src/components/Captcha/CaptchaFields.astro
  4. 36
      src/components/Captcha/CaptchaScripts.astro
  5. 49
      src/components/Captcha/captcha.js
  6. 10
      src/components/CommandMenu/CommandMenu.tsx
  7. 2
      src/components/Navigation/AccountDropdown.astro
  8. 2
      src/components/Navigation/Navigation.astro
  9. 81
      src/components/Setting/SettingSidebar.astro
  10. 12
      src/components/UpdatePassword/UpdatePasswordForm.tsx
  11. 8
      src/components/UpdateProfile/UpdateProfileForm.tsx
  12. 5
      src/components/UpdateProfile/UploadProfilePicture.tsx
  13. 0
      src/layouts/AccountLayout.astro
  14. 16
      src/pages/account/update-password.astro
  15. 15
      src/pages/account/update-profile.astro
  16. 6
      src/pages/forgot-password.astro
  17. 8
      src/pages/login.astro
  18. 6
      src/pages/reset-password.astro
  19. 16
      src/pages/settings/update-password.astro
  20. 15
      src/pages/settings/update-profile.astro
  21. 7
      src/pages/signup.astro
  22. 6
      src/pages/verification-pending.astro
  23. 6
      src/pages/verify-account.astro

@ -0,0 +1,85 @@
---
import Icon from './AstroIcon.astro';
const { activePageId, activePageTitle } = Astro.props;
export interface Props {
activePageId: string;
activePageTitle: string;
}
---
<div class='relative block mb-5 md:hidden p-4 border-b shadow-inner'>
<button
class='flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center font-medium text-gray-900'
id='settings-menu'
>
{activePageTitle}
<Icon icon='dropdown' />
</button>
<ul
id='settings-menu-dropdown'
class='absolute mt-1 hidden left-0 right-0 space-y-1.5 bg-white p-2 shadow-lg z-10'
>
<li>
<a
href='/account/update-profile'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${activePageId === 'profile' ? 'bg-slate-100' : ''}`
>Profile</a
>
</li>
<li>
<a
href='/account/update-password'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${activePageId === 'change-password' ? 'bg-slate-100' : ''}`
>Change password</a
>
</li>
</ul>
</div>
<div class='container flex min-h-screen items-stretch'>
<!-- Start Desktop Sidebar -->
<aside class='hidden w-56 border-r border-slate-200 py-10 pr-5 md:block'>
<nav>
<ul class='space-y-1'>
<li>
<a
href='/account/update-profile'
class=`block w-full rounded px-2 py-1.5 font-regular text-slate-900 hover:bg-slate-100 ${activePageId === 'profile' ? 'bg-slate-100' : ''}`
>
Profile
</a>
</li>
<li>
<a
href='/account/update-password'
class=`block w-full rounded px-2 py-1.5 font-regular text-slate-900 hover:bg-slate-100 ${activePageId === 'change-password' ? 'bg-slate-100' : ''}`
>
Security
</a>
</li>
</ul>
</nav>
</aside>
<!-- /End Desktop Sidebar -->
<div class='grow px-0 py-0 md:px-10 md:py-10'>
<slot />
</div>
</div>
<script>
const menuButton = document.getElementById('settings-menu');
const menuDropdown = document.getElementById('settings-menu-dropdown');
menuButton?.addEventListener('click', () => {
menuDropdown?.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!menuButton?.contains(e.target as Node)) {
menuDropdown?.classList.add('hidden');
}
});
</script>

@ -32,8 +32,8 @@ function showHideGuestElements(hideOrShow: 'hide' | 'show' = 'hide') {
// Prepares the UI for the user who is logged in // Prepares the UI for the user who is logged in
function handleGuest() { function handleGuest() {
const authenticatedRoutes = [ const authenticatedRoutes = [
'/settings/update-profile', '/account/update-profile',
'/settings/update-password', '/account/update-password',
]; ];
showHideAuthElements('hide'); showHideAuthElements('hide');

@ -1,5 +0,0 @@
---
---
<div class='recaptcha-field mb-2'></div>
<input type='hidden' name='g-recaptcha-response' class='recaptcha-response' />

@ -1,36 +0,0 @@
---
---
<script src='./captcha.js'></script>
<script is:inline>
window.onCaptchaLoad = function () {
if (!window.grecaptcha) {
console.warn('window.grecaptcha is not defined');
return;
}
const recaptchaFields = document.querySelectorAll('.recaptcha-field');
// render recaptcha on fields
recaptchaFields.forEach((field) => {
// If captcha already rendered for this field
if (field.hasAttribute('data-recaptcha-id')) {
return;
}
const renderedId = window.grecaptcha.render(field, {
sitekey: '6Ldn2YsjAAAAABlUxNxukAuDAUIuZIhO0hRVxzJW',
});
field.setAttribute('data-recaptcha-id', renderedId);
});
};
</script>
<script
src='https://www.google.com/recaptcha/api.js?onload=onCaptchaLoad&render=explicit'
async
defer
></script>

@ -1,49 +0,0 @@
class Captcha {
constructor() {
this.onDOMLoaded = this.onDOMLoaded.bind(this);
this.bindValidation = this.bindValidation.bind(this);
this.validateCaptchaBeforeSubmit =
this.validateCaptchaBeforeSubmit.bind(this);
}
validateCaptchaBeforeSubmit(e) {
const target = e.target;
const captchaField = target.querySelector('.recaptcha-field');
if (captchaField) {
const captchaId = captchaField.dataset.recaptchaId;
const captchaResponse = window.grecaptcha.getResponse(captchaId);
// If valid captcha is not present, prevent form submission
if (!captchaResponse) {
e.preventDefault();
alert('Please verify that you are human first');
return false;
}
target.querySelector('.recaptcha-response').value = captchaResponse;
}
target.closest('.popup').classList.add('hidden');
return true;
}
bindValidation() {
const forms = document.querySelectorAll('[captcha-form]');
forms.forEach((form) => {
form.addEventListener('submit', this.validateCaptchaBeforeSubmit);
});
}
onDOMLoaded() {
this.bindValidation();
}
init() {
window.addEventListener('DOMContentLoaded', this.onDOMLoaded);
}
}
const captcha = new Captcha();
captcha.init();

@ -1,14 +1,14 @@
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import BestPracticesIcon from '../../icons/best-practices.svg'; import BestPracticesIcon from '../../icons/best-practices.svg';
import GuideIcon from '../../icons/guide.svg';
import HomeIcon from '../../icons/home.svg'; import HomeIcon from '../../icons/home.svg';
import UserIcon from '../../icons/user.svg';
import RoadmapIcon from '../../icons/roadmap.svg'; import RoadmapIcon from '../../icons/roadmap.svg';
import GuideIcon from '../../icons/guide.svg'; import UserIcon from '../../icons/user.svg';
import VideoIcon from '../../icons/video.svg'; import VideoIcon from '../../icons/video.svg';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { useKeydown } from '../../hooks/use-keydown';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { useOutsideClick } from '../../hooks/use-outside-click';
type PageType = { type PageType = {
url: string; url: string;
@ -21,7 +21,7 @@ type PageType = {
const defaultPages: PageType[] = [ const defaultPages: PageType[] = [
{ url: '/', title: 'Home', group: 'Pages', icon: HomeIcon }, { url: '/', title: 'Home', group: 'Pages', icon: HomeIcon },
{ {
url: '/settings/update-profile', url: '/account/update-profile',
title: 'Account', title: 'Account',
group: 'Pages', group: 'Pages',
icon: UserIcon, icon: UserIcon,

@ -24,7 +24,7 @@ import Icon from '../AstroIcon.astro';
<ul> <ul>
<li class='px-1'> <li class='px-1'>
<a <a
href='/settings/update-profile' href='/account/update-profile'
class='block rounded px-4 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700' class='block rounded px-4 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700'
> >
Settings Settings

@ -98,7 +98,7 @@ import AccountDropdown from './AccountDropdown.astro';
<!-- Links for logged in users --> <!-- Links for logged in users -->
<li data-auth-required class='hidden'> <li data-auth-required class='hidden'>
<a <a
href='/settings/update-profile' href='/account/update-profile'
class='text-xl hover:text-blue-300 md:text-lg' class='text-xl hover:text-blue-300 md:text-lg'
> >
Settings Settings

@ -1,81 +0,0 @@
---
import Icon from '../AstroIcon.astro';
const { pageUrl, name } = Astro.props;
export interface Props {
pageUrl: string;
name: string;
}
---
<div
class='container flex min-h-[calc(100vh-37px-70px)] items-stretch sm:min-h-[calc(100vh-37px-96px)]'
>
<aside class='hidden w-56 border-r border-slate-200 py-10 pr-5 md:block'>
<nav>
<ul class='space-y-1'>
<li>
<a
href='/settings/update-profile'
class=`block w-full rounded px-2 py-1.5 font-regular text-slate-900 hover:bg-slate-100 ${pageUrl === 'profile' ? 'bg-slate-100' : ''}`
>Profile</a
>
</li>
<li>
<a
href='/settings/update-password'
class=`block w-full rounded px-2 py-1.5 font-regular text-slate-900 hover:bg-slate-100 ${pageUrl === 'change-password' ? 'bg-slate-100' : ''}`
>Security</a
>
</li>
</ul>
</nav>
</aside>
<div class='grow py-10 pl-0 md:p-10 md:pr-0'>
<div class='relative mb-5 md:hidden'>
<button
class='flex h-10 w-full items-center justify-between rounded-md bg-slate-800 px-2 text-center font-medium text-slate-100'
id='settings-menu'
>
{name}
<Icon icon='dropdown' />
</button>
<ul
id='settings-menu-dropdown'
class='absolute mt-1 hidden w-full space-y-1.5 rounded-md bg-white p-2 shadow-lg'
>
<li>
<a
href='/settings/update-profile'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'profile' ? 'bg-slate-100' : ''}`
>Profile</a
>
</li>
<li>
<a
href='/settings/update-password'
class=`block w-full rounded px-2 py-1.5 font-medium text-slate-900 hover:bg-slate-200 ${pageUrl === 'change-password' ? 'bg-slate-100' : ''}`
>Change password</a
>
</li>
</ul>
</div>
<slot />
</div>
</div>
<script>
const menuButton = document.getElementById('settings-menu');
const menuDropdown = document.getElementById('settings-menu-dropdown');
menuButton?.addEventListener('click', () => {
menuDropdown?.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!menuButton?.contains(e.target as Node)) {
menuDropdown?.classList.add('hidden');
}
});
</script>

@ -78,9 +78,11 @@ export default function UpdatePasswordForm() {
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2> <div class="hidden md:block mb-8">
<p className="mt-2">Use the form below to update your password.</p> <h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
<div className="mt-8 space-y-4"> <p className="mt-2">Use the form below to update your password.</p>
</div>
<div className="space-y-4">
{authProvider === 'email' && ( {authProvider === 'email' && (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<label <label
@ -132,7 +134,7 @@ export default function UpdatePasswordForm() {
for="new-password-confirmation" for="new-password-confirmation"
className="text-sm leading-none text-slate-500" className="text-sm leading-none text-slate-500"
> >
New Password Confirm Confirm New Password
</label> </label>
<input <input
type="password" type="password"
@ -141,7 +143,7 @@ export default function UpdatePasswordForm() {
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" 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"
required required
minLength={6} minLength={6}
placeholder="New password confirm" placeholder="Confirm New Password"
value={newPasswordConfirmation} value={newPasswordConfirmation}
onInput={(e) => onInput={(e) =>
setNewPasswordConfirmation((e.target as HTMLInputElement).value) setNewPasswordConfirmation((e.target as HTMLInputElement).value)

@ -1,7 +1,7 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';
import { httpGet, httpPost } from '../../lib/http'; import { httpGet, httpPost } from '../../lib/http';
import { pageLoadingMessage } from '../../stores/page'; import { pageLoadingMessage } from '../../stores/page';
import UploadProfilePicture from '../Profile/UploadProfilePicture'; import UploadProfilePicture from './UploadProfilePicture';
export function UpdateProfileForm() { export function UpdateProfileForm() {
const [name, setName] = useState(''); const [name, setName] = useState('');
@ -81,8 +81,10 @@ export function UpdateProfileForm() {
return ( return (
<div> <div>
<h2 className="text-3xl font-bold sm:text-4xl">Profile</h2> <div className="mb-8 hidden md:block">
<p className="mt-2">Update your profile details below.</p> <h2 className="text-3xl font-bold sm:text-4xl">Profile</h2>
<p className="mt-2">Update your profile details below.</p>
</div>
<UploadProfilePicture <UploadProfilePicture
avatarUrl={ avatarUrl={
avatar avatar

@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useEffect, useRef, useState } from 'preact/hooks';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt'; import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpCall, httpPost } from '../../lib/http';
interface PreviewFile extends File { interface PreviewFile extends File {
preview: string; preview: string;
@ -131,7 +130,7 @@ export default function UploadProfilePicture(props: UploadProfilePictureProps) {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
encType="multipart/form-data" encType="multipart/form-data"
className="mt-8 flex flex-col gap-2" className="flex flex-col gap-2"
> >
<label htmlFor="avatar" className="text-sm leading-none text-slate-500"> <label htmlFor="avatar" className="text-sm leading-none text-slate-500">
Profile Picture Profile Picture

@ -0,0 +1,16 @@
---
import AccountSidebar from '../../components/AccountSidebar.astro';
import UpdatePasswordForm from '../../components/UpdatePassword/UpdatePasswordForm';
import AccountLayout from '../../layouts/AccountLayout.astro';
---
<AccountLayout
title='Change Password'
description=''
noIndex={true}
initialLoadingMessage={'Loading profile'}
>
<AccountSidebar activePageId='change-password' activePageTitle='Change Password'>
<UpdatePasswordForm client:load />
</AccountSidebar>
</AccountLayout>

@ -0,0 +1,15 @@
---
import AccountSidebar from '../../components/AccountSidebar.astro';
import { UpdateProfileForm } from '../../components/UpdateProfile/UpdateProfileForm';
import AccountLayout from '../../layouts/AccountLayout.astro';
---
<AccountLayout
title='Update Profile'
noIndex={true}
initialLoadingMessage={'Loading profile'}
>
<AccountSidebar activePageId='profile' activePageTitle='Profile'>
<UpdateProfileForm client:load />
</AccountSidebar>
</AccountLayout>

@ -1,9 +1,9 @@
--- ---
import { ForgotPasswordForm } from '../components/AuthenticationFlow/ForgotPasswordForm'; import { ForgotPasswordForm } from '../components/AuthenticationFlow/ForgotPasswordForm';
import SettingLayout from '../layouts/SettingLayout.astro'; import AccountLayout from '../layouts/AccountLayout.astro';
--- ---
<SettingLayout title='Forgot Password' noIndex={true}> <AccountLayout title='Forgot Password' noIndex={true}>
<div class='container'> <div class='container'>
<div <div
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' 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'
@ -29,4 +29,4 @@ import SettingLayout from '../layouts/SettingLayout.astro';
</div> </div>
</div> </div>
</div> </div>
</SettingLayout> </AccountLayout>

@ -1,12 +1,12 @@
--- ---
import Divider from '../components/AuthenticationFlow/Divider.astro'; import Divider from '../components/AuthenticationFlow/Divider.astro';
import EmailLoginForm from '../components/AuthenticationFlow/EmailLoginForm';
import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton'; import { GitHubButton } from '../components/AuthenticationFlow/GitHubButton';
import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton'; import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
import EmailLoginForm from '../components/AuthenticationFlow/EmailLoginForm'; import AccountLayout from '../layouts/AccountLayout.astro';
import SettingLayout from '../layouts/SettingLayout.astro';
--- ---
<SettingLayout <AccountLayout
title='Login - 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'}
@ -38,4 +38,4 @@ import SettingLayout from '../layouts/SettingLayout.astro';
</div> </div>
</div> </div>
</div> </div>
</SettingLayout> </AccountLayout>

@ -1,9 +1,9 @@
--- ---
import ResetPasswordForm from '../components/AuthenticationFlow/ResetPasswordForm'; import ResetPasswordForm from '../components/AuthenticationFlow/ResetPasswordForm';
import SettingLayout from '../layouts/SettingLayout.astro'; import AccountLayout from '../layouts/AccountLayout.astro';
--- ---
<SettingLayout title='Reset Password' noIndex={true}> <AccountLayout title='Reset Password' noIndex={true}>
<div class='container'> <div class='container'>
<div <div
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' 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'
@ -20,4 +20,4 @@ import SettingLayout from '../layouts/SettingLayout.astro';
<ResetPasswordForm client:load /> <ResetPasswordForm client:load />
</div> </div>
</div> </div>
</SettingLayout> </AccountLayout>

@ -1,16 +0,0 @@
---
import SettingSidebar from '../../components/Setting/SettingSidebar.astro';
import UpdatePasswordForm from '../../components/Setting/UpdatePasswordForm';
import SettingLayout from '../../layouts/SettingLayout.astro';
---
<SettingLayout
title='Change Password'
description=''
noIndex={true}
initialLoadingMessage={'Loading profile'}
>
<SettingSidebar pageUrl='change-password' name='Change Password'>
<UpdatePasswordForm client:load />
</SettingSidebar>
</SettingLayout>

@ -1,15 +0,0 @@
---
import SettingSidebar from '../../components/Setting/SettingSidebar.astro';
import { UpdateProfileForm } from '../../components/Setting/UpdateProfileForm';
import SettingLayout from '../../layouts/SettingLayout.astro';
---
<SettingLayout
title='Update Profile'
noIndex={true}
initialLoadingMessage={'Loading profile'}
>
<SettingSidebar pageUrl='profile' name='Profile'>
<UpdateProfileForm client:load />
</SettingSidebar>
</SettingLayout>

@ -1,13 +1,12 @@
--- ---
import Divider from '../components/AuthenticationFlow/Divider.astro'; import Divider from '../components/AuthenticationFlow/Divider.astro';
import GoogleLogin from '../components/Login/GoogleLogin.astro';
import EmailSignupForm from '../components/AuthenticationFlow/EmailSignupForm'; import EmailSignupForm from '../components/AuthenticationFlow/EmailSignupForm';
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';
import AccountLayout from '../layouts/AccountLayout.astro';
--- ---
<SettingLayout <AccountLayout
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'}
@ -46,4 +45,4 @@ import { GoogleButton } from '../components/AuthenticationFlow/GoogleButton';
</div> </div>
</div> </div>
</div> </div>
</SettingLayout> </AccountLayout>

@ -1,10 +1,10 @@
--- ---
import { VerificationEmailMessage } from '../components/AuthenticationFlow/VerificationEmailMessage'; import { VerificationEmailMessage } from '../components/AuthenticationFlow/VerificationEmailMessage';
import SettingLayout from '../layouts/SettingLayout.astro'; import AccountLayout from '../layouts/AccountLayout.astro';
--- ---
<SettingLayout title='Verify Email' noIndex={true}> <AccountLayout title='Verify Email' noIndex={true}>
<section class='container py-8 sm:py-20'> <section class='container py-8 sm:py-20'>
<VerificationEmailMessage client:load /> <VerificationEmailMessage client:load />
</section> </section>
</SettingLayout> </AccountLayout>

@ -1,10 +1,10 @@
--- ---
import { TriggerVerifyAccount } from '../components/AuthenticationFlow/TriggerVerifyAccount'; import { TriggerVerifyAccount } from '../components/AuthenticationFlow/TriggerVerifyAccount';
import SettingLayout from '../layouts/SettingLayout.astro'; import AccountLayout from '../layouts/AccountLayout.astro';
--- ---
<SettingLayout title='Verify account' noIndex={true}> <AccountLayout title='Verify account' noIndex={true}>
<div class='container py-16'> <div class='container py-16'>
<TriggerVerifyAccount client:load /> <TriggerVerifyAccount client:load />
</div> </div>
</SettingLayout> </AccountLayout>

Loading…
Cancel
Save