Auth - Login/Signup/Forgot/Rest

This commit is contained in:
Rotimi Best
2022-12-03 22:45:01 +01:00
parent 8e169241e1
commit 3b0ff79c60
21 changed files with 881 additions and 283 deletions
+18 -9
View File
@@ -7,21 +7,27 @@
export let handleSubmit;
export let isLogin = true;
export let showOnlyContent = false;
export let showLogo = false;
export let formRef;
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
});
try {
console.log('signInWithGoogle');
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
});
console.log('data', data);
console.log('error', error);
} catch (error) {}
}
</script>
<div class="w-full min-h-screen flex items-center justify-center mt-4">
<div
class="container border border-gray bg-white {showOnlyContent && 'h-2/4'}"
>
<div class="container border border-gray bg-white">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-4">
{#if !showOnlyContent}
<div class="flex items-center w-full justify-center">
{#if !showOnlyContent || showLogo}
<div class="flex items-center w-full justify-center pt-4">
<img
src="/logo-192.png"
alt="ClassroomIO logo"
@@ -33,6 +39,7 @@
</div>
{/if}
<form
bind:this={formRef}
on:submit|preventDefault={handleSubmit}
class="flex items-center flex-col w-10/12"
>
@@ -47,7 +54,9 @@
className="py-3 sm:w-full w-full"
>
<GoogleIconColored />
<span class="ml-2">Sign up with Google</span>
<span class="ml-2"
>{isLogin ? 'Sign in' : 'Sign up'} with Google</span
>
</PrimaryButton>
</div>
{/if}
+2 -1
View File
@@ -9,6 +9,7 @@
export let onKeyDown = () => {};
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let type = 'text';
export let isPassword = false;
export let autoFocus = false;
@@ -50,7 +51,7 @@
<label class="block relative {className}">
{#if label}
<label for="text-field" class="m-0 font-light">
<label for="text-field" class="m-0 font-light {labelClassName}">
{label}
{#if isRequired}
@@ -0,0 +1,23 @@
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_174_5581)">
<path
d="M16 8C15.0111 8 14.0444 8.29325 13.2222 8.84265C12.3999 9.39206 11.759 10.173 11.3806 11.0866C11.0022 12.0002 10.9031 13.0055 11.0961 13.9755C11.289 14.9454 11.7652 15.8363 12.4645 16.5355C13.1637 17.2348 14.0546 17.711 15.0246 17.9039C15.9945 18.0969 16.9998 17.9978 17.9134 17.6194C18.8271 17.241 19.6079 16.6001 20.1574 15.7779C20.7068 14.9556 21 13.9889 21 13C21 11.6739 20.4732 10.4021 19.5355 9.46447C18.5979 8.52678 17.3261 8 16 8Z"
fill="#ADADAD"
/>
<path
d="M16 2C13.2311 2 10.5243 2.82109 8.22202 4.35943C5.91973 5.89777 4.12532 8.08427 3.06569 10.6424C2.00607 13.2006 1.72882 16.0155 2.26901 18.7313C2.80921 21.447 4.14258 23.9416 6.10051 25.8995C8.05845 27.8574 10.553 29.1908 13.2687 29.731C15.9845 30.2712 18.7994 29.9939 21.3576 28.9343C23.9157 27.8747 26.1022 26.0803 27.6406 23.778C29.1789 21.4757 30 18.7689 30 16C29.9958 12.2883 28.5195 8.72973 25.8949 6.10512C23.2703 3.48052 19.7118 2.00418 16 2ZM23.9925 24.9258C23.9726 23.614 23.4382 22.3626 22.5043 21.4412C21.5704 20.5198 20.3119 20.0022 19 20H13C11.6881 20.0022 10.4296 20.5198 9.49573 21.4412C8.56186 22.3626 8.02739 23.614 8.00751 24.9258C6.19407 23.3065 4.91524 21.1747 4.34034 18.8125C3.76545 16.4503 3.92161 13.9691 4.78814 11.6977C5.65468 9.42618 7.19073 7.47147 9.19288 6.09237C11.195 4.71327 13.5688 3.97483 16 3.97483C18.4312 3.97483 20.805 4.71327 22.8071 6.09237C24.8093 7.47147 26.3453 9.42618 27.2119 11.6977C28.0784 13.9691 28.2346 16.4503 27.6597 18.8125C27.0848 21.1747 25.8059 23.3065 23.9925 24.9258Z"
fill="#ADADAD"
/>
</g>
<defs>
<clipPath id="clip0_174_5581">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+3 -3
View File
@@ -1,5 +1,5 @@
<script>
import { BarLoader } from 'svelte-loading-spinners';
import { Moon } from 'svelte-loading-spinners';
import { VARIANTS, VARIANTS_CLASS } from './constants';
export let label = '';
@@ -22,14 +22,14 @@
variant
]} {!disablePadding &&
'py-1 px-5'} rounded-none {className} py-2 w-full justify-center sm:w-auto {isLoading &&
'py-4 border-blue-700'} transition delay-150 duration-300 ease-in-out"
'py-2 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
on:click={onClick}
{name}
{type}
disabled={isDisabled}
>
{#if isLoading}
<BarLoader size="30" color="#1d4ed8" unit="px" duration="1s" />
<Moon size="30" color="#1d4ed8" unit="px" duration="1s" />
{:else if !!label}
{label}
{:else}
@@ -6,7 +6,7 @@
import TextField from '../../Form/TextField.svelte';
export let title = '';
export let code;
export let code = '';
export let name = '';
export let options = [];
export let onSubmit = () => {};
@@ -16,9 +16,11 @@
export let disabled = false;
export let isPreview = false;
export let nextButtonProps = {};
export let grade;
export let grade = undefined;
export let gradeMax = 0;
export let disableGrading = false;
export let labelClassName = '';
export let disableOptContainerMargin = false;
function getRadioVal(form, name) {
let val;
@@ -60,7 +62,7 @@
<form on:submit|preventDefault={handleFormSubmit}>
<div class="flex items-center justify-between">
<h3 class={!isNaN(grade) && 'w-3/4'}>{title}</h3>
<h3 class="{labelClassName} {!isNaN(grade) && 'w-3/4'}">{title}</h3>
{#if !isNaN(grade)}
<div class="flex items-center">
<TextField
@@ -79,7 +81,7 @@
<CodeSnippet {code} />
{/if}
<div class="ml-4">
<div class={!disableOptContainerMargin && 'ml-4'}>
{#each options as option}
<button
class="cursor-pointer text-left my-2 border-2 border-gray-300 rounded-md hover:bg-gray-200 w-full {getValidationClassName(
+32 -29
View File
@@ -1,6 +1,6 @@
<script context="module">
export function preload(page, { config }) {
return { config, isMvpUser: !!page.query.mvp };
return { config };
}
</script>
@@ -21,16 +21,15 @@
import { user, profile } from '../utils/store/user';
import { getSupabase } from '../utils/functions/supabase';
import { isMobile } from '../utils/store/useMobile';
import { ROUTES_TO_HIDE_NAV, ROUTE } from '../utils/constants/routes';
export let segment;
export let config;
// export let isMvpUser;
let { page, preloading } = stores();
let supabase = getSupabase(config);
let path = $page.path.replace('/', '');
let allowUser = false;
const delayedPreloading = derived(preloading, (currentPreloading, set) => {
setTimeout(() => set(currentPreloading), 250);
@@ -60,7 +59,7 @@
const { data, error } = await supabase.from('profile').insert({
id: authUser.id,
username: regexUsernameMatch[1],
username: regexUsernameMatch[1] + '1669991321770',
fullname: regexUsernameMatch[1],
email: authUser.email,
});
@@ -75,12 +74,8 @@
// Set user in sentry
Sentry.setUser($profile);
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/profile/' + $profile.id);
} else if (!path.length) {
goto('/dashboard');
}
goto(ROUTE.ONBOARDING);
}
$user.fetchingUser = false;
} else if (profileData) {
@@ -94,9 +89,9 @@
Sentry.setUser($profile);
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/profile/' + $profile.id);
} else if (!path.length) {
if (!$profile.role) {
goto(ROUTE.ONBOARDING);
} else {
goto('/dashboard');
}
}
@@ -120,31 +115,27 @@
handleResize();
const _isMvpUser = localStorage.getItem('mvp');
if (_isMvpUser || window.location.search.includes('mvp')) {
localStorage.setItem('mvp', 'true');
allowUser = true;
} else if (!_isMvpUser) {
// if (!!path) {
// goto('/courses');
// }
return;
}
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
if (config.isProd) {
handleAuthChange(event, session);
}
console.log(`event`, event);
if (event == 'PASSWORD_RECOVERY') {
console.log('PASSWORD RESET');
}
if (path.includes('reset')) {
console.log('Dont change auth when on reset page');
return;
}
if (event === 'SIGNED_IN') {
$user.fetchingUser = true;
getProfile();
} else {
console.log('not logged in, go to login');
return goto('/courses');
return goto('/login');
}
}
);
@@ -154,6 +145,18 @@
};
});
// if (typeof window !== 'undefined' && typeof document !== 'undefined') {
// page.subscribe(() => {
// const from = window.location.pathname;
// const redirect = (href) => {
// goto(href);
// };
// if (!$user.isLoggedIn && !PUBLIC_ROUTES.includes(from)) {
// redirect('/login');
// }
// });
// }
$: path = $page.path.replace('/', '');
</script>
@@ -169,8 +172,8 @@
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
{#if !['login', 'signup'].includes(path) && !isCoursePage(path)}
<Navigation {segment} disableLogin={!allowUser} />
{#if !ROUTES_TO_HIDE_NAV.includes($page.path) && !isCoursePage(path)}
<Navigation {segment} />
{/if}
<div class="flex justify-between">
+1 -1
View File
@@ -69,7 +69,7 @@
<section class="root">
<PageNav title="All courses">
<div slot="widget">
{#if $profile.can_add_course}
{#if $profile.role === 'educator'}
<PrimaryButton
label="New"
onClick={() => ($createCourseModal.open = true)}
+117
View File
@@ -0,0 +1,117 @@
<script context="module">
export function preload(page, { config }) {
return { config };
}
</script>
<script>
import { goto } from '@sapper/app';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { VARIANTS } from '../components/PrimaryButton/constants';
import { getSupabase } from '../utils/functions/supabase';
import { forgotValidation } from '../utils/functions/validator';
import { ROUTE } from '../utils/constants/routes';
import { FORGOT_PASSWORD_FIELDS } from '../utils/constants/authentication';
import AuthUI from '../components/AuthUI/index.svelte';
import EmailSentIcon from '../components/Icons/EmailSentIcon.svelte';
export let config;
let supabase = getSupabase(config);
let fields = Object.assign({}, FORGOT_PASSWORD_FIELDS);
let submitError;
let loading = false;
let success = false;
let errors = {};
async function handleSubmit() {
errors = forgotValidation(fields);
console.log('errors', errors);
if (Object.keys(errors).length) {
return;
}
try {
loading = true;
const { data, error } = await supabase.auth.api.resetPasswordForEmail(
fields.email,
{
redirectTo: window.location.origin + ROUTE.RESET,
}
);
console.log('data', data);
if (error) throw error;
success = true;
} catch (error) {
submitError = error.error_description || error.message;
} finally {
loading = false;
}
}
</script>
<svelte:head>
<title>Reset Password - ClassroomIO</title>
</svelte:head>
<AuthUI {supabase} {handleSubmit} showOnlyContent={true} showLogo={!success}>
{#if success}
<div class="mt-4 w-full flex items-center justify-center flex-col">
<EmailSentIcon />
<h3 class="text-xl font-semibold my-3">Email Sent!</h3>
<p class="text-md mb-6 text-center">
We have sent a confirmation email to <span class="text-blue-700"
>{fields.email}</span
>. Kindly check your inbox to reset password.
</p>
</div>
<div class="my-4 w-full flex justify-end items-center flex-col">
<PrimaryButton
label="Okay"
type="button"
className="sm:w-full w-full mb-4"
onClick={() => goto(ROUTE.LOGIN)}
/>
</div>
{:else}
<div class="mt-4 w-full">
<h3 class="text-xl font-semibold my-3">Forgot Password</h3>
<p class="text-sm mb-6">We will send you a reset link to your email</p>
<TextField
label="Your email"
bind:value={fields.email}
type="email"
autoFocus={true}
placeholder="[email protected]"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
</div>
<div class="my-4 w-full flex justify-end items-center flex-col">
<PrimaryButton
label="Reset Password"
type="submit"
className="sm:w-full w-full mb-4"
isDisabled={loading}
isLoading={loading}
/>
<PrimaryButton
label="Cancel"
type="button"
className="sm:w-full w-full text-blue-700"
variant={VARIANTS.NONE}
onClick={() => goto(ROUTE.LOGIN)}
/>
</div>
{/if}
</AuthUI>
+64 -73
View File
@@ -5,47 +5,47 @@
</script>
<script>
import { Chasing } from 'svelte-loading-spinners';
import { goto } from '@sapper/app';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
import { authValidation } from '../utils/functions/validator';
import { ROUTE } from '../utils/constants/routes';
import { LOGIN_FIELDS } from '../utils/constants/authentication';
import AuthUI from '../components/AuthUI/index.svelte';
export let config;
let formRef;
let supabase = getSupabase(config);
let email;
let password;
let fields = Object.assign({}, LOGIN_FIELDS);
let submitError;
let loading = false;
let success = false;
let errors = {};
async function handleSubmit() {
if (!email) {
errors = {
email: 'Email is required',
password: 'Password is required',
};
const validationRes = authValidation(fields);
console.log('validationRes', validationRes);
if (Object.keys(validationRes).length) {
errors = Object.assign(errors, validationRes);
return;
}
try {
loading = true;
const { data, error } = await supabase.auth.signIn(
{ email },
{
redirectTo: window.location.origin + '/login',
}
);
const { data, error } = await supabase.auth.signIn({
email: fields.email,
password: fields.password,
});
console.log('data', data);
if (error) throw error;
success = true;
email = {};
return goto(ROUTE.DASHBOARD);
} catch (error) {
errors = {
email: error.error_description || error.message,
};
submitError = error.error_description || error.message;
} finally {
loading = false;
}
@@ -53,64 +53,55 @@
</script>
<svelte:head>
<title>Login - ClassroomIO</title>
<title>Welcome back to ClassroomIO</title>
</svelte:head>
<AuthUI {supabase} isLogin={true} {handleSubmit}>
<AuthUI
{supabase}
isLogin={true}
{handleSubmit}
showOnlyContent={loading}
bind:formRef
>
<div class="mt-4 w-full">
{#if $user.fetchingUser}
<div class="h-40 text-md flex items-center justify-center">
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</div>
{:else if success}
<div class="h-40 text-md flex items-center justify-center flex-col">
<p class="">
A link has been sent to your email. Please check both your
<strong>spam </strong>and your<strong>&nbsp; inbox</strong>.
</p>
<p class="mt-2 underline">
If our email is in your spam, do make sure you mark our email address
as safe, then move it from your spam to your inbox.
</p>
</div>
{:else}
<p class="text-lg font-semibold mb-6">Welcome back</p>
<TextField
label="Your email"
bind:value={email}
type="email"
autoFocus={true}
placeholder="[email protected]"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
/>
<TextField
label="Your password"
bind:value={password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
/>
<div class="w-full text-right">
<a class="text-md text-blue-700" href="/forgot"> Forgot password? </a>
</div>
<p class="text-lg font-semibold mb-6">Welcome back</p>
<TextField
label="Your email"
bind:value={fields.email}
type="email"
autoFocus={true}
placeholder="[email protected]"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
/>
<TextField
label="Your password"
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
<div class="w-full text-right">
<a class="text-md text-blue-700" href="/forgot"> Forgot password? </a>
</div>
</div>
{#if !success}
<div class="my-4 w-full flex justify-end items-center">
<!-- <a href="/login" class="text-blue-700 text-sm">Create an account</a> -->
<PrimaryButton
label={loading ? 'Creating...' : 'Log In'}
type="submit"
className="py-3 sm:w-full w-full"
isDisabled={loading}
/>
</div>
{/if}
<div class="my-4 w-full flex justify-end items-center">
<!-- <a href="/login" class="text-blue-700 text-sm">Create an account</a> -->
<PrimaryButton
label="Log In"
type="submit"
className="sm:w-full w-full"
isDisabled={loading}
isLoading={loading}
/>
</div>
</AuthUI>
+310
View File
@@ -0,0 +1,310 @@
<script lang="ts">
import { goto } from '@sapper/app';
import CheckmarkFilled20 from 'carbon-icons-svelte/lib/CheckmarkFilled20';
import TextField from '../components/Form/TextField.svelte';
import UserProfileIcon from '../components/Icons/UserProfileIcon.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { VARIANTS } from '../components/PrimaryButton/constants';
import { profile } from '../utils/store/user';
import { ROUTE } from '../utils/constants/routes';
import { onboardingValidation } from '../utils/functions/validator';
import { supabase } from '../utils/functions/supabase';
enum OnboardingRoles {
Educator = 'educator',
Student = 'student',
}
interface OnboardingField {
fullname?: string;
role?: OnboardingRoles;
goal?: string;
source?: string;
}
interface Goal {
label: string;
value: string;
}
const maxSteps = 2;
let fields: OnboardingField = {
fullname: '',
};
let errors: OnboardingField = {};
let progress = 50;
let step = 1;
let goals: Goal[] = [];
let loading = false;
const roles = [OnboardingRoles.Educator, OnboardingRoles.Student];
const educatorGoals: Goal[] = [
{
label: 'Sell courses online',
value: 'sell-online',
},
{
label: 'Teach my current students online',
value: 'teach-online',
},
{
label: 'On another platform, expanding here',
value: 'expanding-platform',
},
{
label: 'Just here to explore',
value: 'explore',
},
];
const studentGoals: Goal[] = [
{
label: 'Find new courses',
value: 'find-course',
},
{
label: 'Join my class online',
value: 'join-class',
},
{
label: 'Just here to explore',
value: 'explore',
},
];
const sources = [
{
label: 'Articles',
value: 'articles',
},
{
label: 'Search engine',
value: 'search-engine',
},
{
label: 'Social media',
value: 'social-media',
},
{
label: 'Friends and family',
value: 'friends-family',
},
];
const onGoalInputChange = (event: KeyboardEvent) => {
console.log('event', event);
fields.goal = (event.target as HTMLInputElement).value;
};
const onSourceInputChange = (event: KeyboardEvent) => {
console.log('event', event);
fields.source = (event.target as HTMLInputElement).value;
};
const handleSubmit = async () => {
// Run validation
console.log(fields);
errors = onboardingValidation(fields, step);
console.log('errors', errors);
if (Object.keys(errors).length) {
return;
}
if (step === maxSteps) {
// Submit filed
loading = true;
let { data, error } = await supabase
.from('profile')
.update(fields)
.match({ id: $profile.id });
loading = false;
console.log('data', data);
console.log('error', error);
return goto(ROUTE.DASHBOARD);
}
// Move to next step
step = step + 1;
};
$: {
progress = Math.round((step / maxSteps) * 100);
goals =
fields.role === OnboardingRoles.Educator ? educatorGoals : studentGoals;
}
</script>
<div class="w-full min-h-screen flex justify-center">
<div class="flex justify-center items-center flex-col w-9/12 lg:w-2/5">
<!-- Header With Logo -->
<div class="flex flex-col items-center">
<div class="flex items-center w-full justify-center mb-4">
<img
src="/logo-192.png"
alt="ClassroomIO logo"
height="50"
width="50"
data-atf="1"
/>
<h4 class="text-xl">ClassroomIO</h4>
</div>
<!-- Loggedin Email -->
<div
class="w-64 flex items-center justify-center mb-6 py-6 bg-gray-100 rounded-2xl border border-gray-300"
>
<UserProfileIcon />
<p class="text-sm ml-2">{$profile.email}</p>
</div>
</div>
<div class="form-container overflow-y-auto w-full pl-2">
{#if step === 1}
<!-- Role Question -->
<div id="role-question" class="flex items-start flex-col mb-6">
<!-- Full name questionn -->
<TextField
label="Full Name"
bind:value={fields.fullname}
type="text"
placeholder="e.g Joke Silva"
className="mb-5 w-7/12"
labelClassName="text-lg font-normal"
errorMessage={errors.fullname}
/>
<!-- Roal question -->
<div>
<p class="text-lg font-normal mb-2">What's your main goal today?</p>
<p class="text-sm mb-6">
This will help us personalize your experience on Classroomio
</p>
<!-- Roles -->
<div class="flex items-center">
{#each roles as role, index}
<button
class="w-52 h-44 p-4 {index === 0 &&
'mr-4'} flex flex-col justify-between bg-gray-200 rounded-md {fields.role ===
role &&
'border-2 border-blue-700'} hover:shadow-xl hover:border-blue-700 transition delay-150 duration-300 ease-in-out"
type="button"
on:click={() => (fields.role = role)}
>
<div class="flex flex-col items-end w-full">
{#if fields.role === role}
<CheckmarkFilled20 class="text-blue-700" />
{/if}
</div>
<div>
<h4 class="text-lg text-left capitalize">{role}</h4>
<p class="text-sm text-black-200">
{role === OnboardingRoles.Educator ? 'Teach' : 'Learn'} on
ClassroomIO
</p>
</div>
</button>
{/each}
</div>
{#if errors.role}
<p class="text-sm text-red-500">{errors.role}</p>
{/if}
</div>
</div>
{:else}
<!-- Goal/Source Question -->
<div id="goal-question" class="flex items-center flex-col mb-6">
<div class="w-10/12">
<!-- Goal Question -->
<div class="w-full flex items-start flex-col justify-between mb-10">
<label for="text-field" class="m-0 text-lg font-normal mb-3">
What brings you to ClassroomIO?
</label>
<!-- Loop through Goals -->
{#each goals as goal}
<label class="w-full inline-flex items-center mb-1 font-light">
<input
type="radio"
bind:group={fields.goal}
name="goal"
value={goal.value}
class="mr-2"
/>
{goal.label}
</label>
{/each}
<!-- Goal: Error message -->
{#if errors.goal}
<p class="text-sm text-red-500">{errors.goal}</p>
{/if}
</div>
<!-- Source Question -->
<div class="w-full flex items-start flex-col justify-between">
<label for="text-field" class="m-0 text-lg font-normal mb-3">
How did you hear about us?
</label>
<!-- Loop through Goals -->
{#each sources as source}
<label class="w-full inline-flex items-center mb-1 font-light">
<input
type="radio"
bind:group={fields.source}
name="source"
value={source.value}
class="mr-2"
/>
{source.label}
</label>
{/each}
<!-- Goal: Error message -->
{#if errors.source}
<p class="text-sm text-red-500">{errors.source}</p>
{/if}
</div>
</div>
</div>
{/if}
</div>
<!-- Footer -->
<div class="flex justify-between items-center mt-8 w-full">
<div class="w-24 h-2 bg-gray-300 relative">
<span
class="progress absolute top-0 left-0 bg-blue-700 h-full"
style="width: {progress}%;"
/>
</div>
<div class="flex">
{#if step > 1}
<PrimaryButton
label="Back"
variant={VARIANTS.NONE}
className="py-2 px-6 mr-2 text-blue-700"
onClick={() => (step = step - 1)}
/>
{/if}
<PrimaryButton
label="Continue"
type="button"
className="px-5 py-3"
isLoading={loading}
onClick={handleSubmit}
/>
</div>
</div>
</div>
</div>
<style>
.form-container {
max-height: 66%;
}
</style>
+113
View File
@@ -0,0 +1,113 @@
<script context="module">
export function preload(page, { config }) {
return { config };
}
</script>
<script>
import { goto } from '@sapper/app';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import {
resetValidation,
getConfirmPasswordError,
getDisableSubmit,
} from '../utils/functions/validator';
import { RESET_FIELDS } from '../utils/constants/authentication';
import AuthUI from '../components/AuthUI/index.svelte';
export let config;
let supabase = getSupabase(config);
let fields = Object.assign({}, RESET_FIELDS);
let loading = false;
let success = false;
let errors = {};
let submitError;
let disableSubmit = false;
let formRef;
async function handleSubmit(e) {
errors = resetValidation(fields);
console.log('errors', errors);
if (Object.keys(errors).length) {
return;
}
try {
loading = true;
const { data, error } = await supabase.auth.update({
password: fields.password,
});
console.log('data', data);
if (error) throw error;
formRef?.reset();
return goto('/login');
} catch (error) {
submitError = error.error_description || error.message;
} finally {
loading = false;
}
}
$: errors.confirmPassword = getConfirmPasswordError(fields);
$: disableSubmit = getDisableSubmit(fields);
</script>
<svelte:head>
<title>Join ClassroomIO</title>
</svelte:head>
<AuthUI
{supabase}
isLogin={false}
{handleSubmit}
showOnlyContent={true}
showLogo={true}
bind:formRef
>
<div class="mt-4 w-full">
<h3 class="text-xl font-semibold my-3">New Password</h3>
<p class="text-sm mb-6">Enter your new password details</p>
<TextField
label="Your Password"
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
isRequired
/>
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
</div>
<div class="my-4 w-full flex justify-end items-center">
<PrimaryButton
label="Reset Password"
type="submit"
className="sm:w-full w-full"
isDisabled={disableSubmit || loading}
isLoading={loading}
/>
</div>
</AuthUI>
+73 -91
View File
@@ -6,19 +6,16 @@
<script>
import { goto } from '@sapper/app';
import { Chasing } from 'svelte-loading-spinners';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
import {
signUpValidation,
authValidation,
getConfirmPasswordError,
getDisableSubmit,
} from '../utils/functions/validator';
import { SIGNUP_FIELDS } from '../utils/constants/authentication';
import AuthUI from '../components/AuthUI/index.svelte';
import EmailSentIcon from '../components/Icons/EmailSentIcon.svelte';
export let config;
@@ -29,9 +26,10 @@
let errors = {};
let submitError;
let disableSubmit = false;
let formRef;
async function handleSubmit() {
const validationRes = signUpValidation(fields);
async function handleSubmit(e) {
const validationRes = authValidation(fields);
console.log('validationRes', validationRes);
if (Object.keys(validationRes).length) {
@@ -44,14 +42,12 @@
const { data, error } = await supabase.auth.signUp({
email: fields.email,
password: fields.password,
options: {
data: {
fullname: fields.name,
},
},
});
console.log('data', data);
if (error) throw error;
formRef?.reset();
return goto('/login');
// success = true;
// fields = Object.assign({}, SIGNUP_FIELDS);
@@ -67,90 +63,76 @@
</script>
<svelte:head>
<title>Sign Up - ClassroomIO</title>
<title>Join ClassroomIO</title>
</svelte:head>
<AuthUI {supabase} isLogin={false} {handleSubmit} showOnlyContent={success}>
<AuthUI
{supabase}
isLogin={false}
{handleSubmit}
showOnlyContent={success}
bind:formRef
>
<div class="mt-4 w-full">
{#if $user.fetchingUser}
<div class="h-40 text-md flex items-center justify-center">
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</div>
{:else if success}
<div class="text-md flex items-center justify-center flex-col">
<EmailSentIcon />
<h2 class="text-xl">Email sent!</h2>
<p class="mt-4 text-center">
We have sent a confirmation email to {fields.email}. Please check both
your
<strong>spam </strong>and your<strong>&nbsp; inbox</strong>.
</p>
</div>
{:else}
<p class="text-lg font-semibold mb-6">Create a free account</p>
<TextField
label="Full Name"
bind:value={fields.name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
isRequired
/>
<TextField
label="Your Email"
bind:value={fields.email}
type="email"
placeholder="[email protected]"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
isRequired
/>
<TextField
label="Your Password"
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
isRequired
/>
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
<div class="w-full text-right">
<a class="text-md text-blue-700" href="/forgot"> Forgot password? </a>
</div>
<p class="text-lg font-semibold mb-6">Create a free account</p>
<!-- <TextField
label="Full Name"
bind:value={fields.name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
isRequired
/> -->
<TextField
label="Your Email"
bind:value={fields.email}
type="email"
placeholder="[email protected]"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
isRequired
/>
<TextField
label="Your Password"
bind:value={fields.password}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.password}
helperMessage="Password must be more than 6 characters"
isRequired
/>
<TextField
label="Confirm Password"
bind:value={fields.confirmPassword}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.confirmPassword}
isRequired
/>
{#if submitError}
<p class="text-sm text-red-500">{submitError}</p>
{/if}
</div>
{#if !success}
<div class="my-4 w-full flex justify-end items-center">
<PrimaryButton
label={loading ? 'Creating...' : 'Create Account'}
type="submit"
className="py-3 sm:w-full w-full"
isDisabled={disableSubmit || loading}
/>
</div>
{/if}
<div class="my-4 w-full flex justify-end items-center">
<PrimaryButton
label="Create Account"
type="submit"
className="sm:w-full w-full"
isDisabled={disableSubmit || loading}
isLoading={loading}
/>
</div>
</AuthUI>
-62
View File
@@ -1,62 +0,0 @@
<script>
import TimeTable from "../components/TimeTable/index.svelte";
import { stores } from "@sapper/app";
const { page } = stores();
let tab = "";
$: tab = $page.query.tab || "mine";
</script>
<svelte:head>
<title>Расписание</title>
</svelte:head>
<div class="root">
<div class="header flex justify-between mb-2">
<h4 class="title">Расписание</h4>
<div class="flex justify-evenly items-center">
<a
class="mr-5 text-sm {tab === 'mine' && 'active'}"
href="/timetable?tab=mine"
>
Мои
</a>
<a
class="mr-5 text-sm {tab === 'saved' && 'active'}"
href="/timetable?tab=saved"
>
Сохранение
</a>
</div>
</div>
<TimeTable />
<TimeTable />
<TimeTable />
</div>
<style>
.root {
margin: 0 auto;
border-left: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
}
.root .header {
border-bottom: 1px solid var(--border-color);
padding: 15px;
}
.active {
position: relative;
display: inline-block;
}
.active::after {
position: absolute;
content: "";
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
display: block;
}
</style>
+14 -1
View File
@@ -1,6 +1,19 @@
export const SIGNUP_FIELDS = {
name: '',
email: '',
password: '',
confirmPassword: '',
};
export const RESET_FIELDS = {
password: '',
confirmPassword: '',
};
export const LOGIN_FIELDS = {
email: '',
password: '',
};
export const FORGOT_PASSWORD_FIELDS = {
email: '',
};
+24
View File
@@ -0,0 +1,24 @@
export const ROUTE = {
HOME: '/',
DASHBOARD: '/dashboard',
ONBOARDING: '/onboarding',
COURSES: '/courses',
LOGIN: '/login',
SIGN_UP: '/signup',
PROFILE: '/profile',
PEOPLE: '/people',
DISCUSSIONS: '/discussions',
ASK: '/ask',
FORGOT: '/forgot',
RESET: '/reset',
};
export const PUBLIC_ROUTES = [
ROUTE.HOME,
ROUTE.LOGIN,
ROUTE.SIGN_UP,
ROUTE.FORGOT,
ROUTE.RESET,
];
export const ROUTES_TO_HIDE_NAV = [...PUBLIC_ROUTES, ROUTE.ONBOARDING];
+66 -5
View File
@@ -1,10 +1,13 @@
import z from 'zod';
const validationSchema = z.object({
name: z.string().min(3, {
message: 'Must be 3 or more characters long',
const forgotValidationSchema = z.object({
email: z.string().email({
message: 'Invalid email address',
invalid_type_error: 'Must not be empty',
}),
});
const authValidationSchema = z.object({
email: z.string().email({
message: 'Invalid email address',
invalid_type_error: 'Must not be empty',
@@ -15,14 +18,44 @@ const validationSchema = z.object({
}),
});
const resetValidationSchema = z.object({
password: z.string().min(6, {
message: 'Must be 6 or more characters long',
invalid_type_error: 'Must not be empty',
}),
});
const onboardingValidationSchema = {
stepOne: z.object({
fullname: z.string().min(1, { message: 'Enter your fullname' }),
role: z.string({
required_error: 'Please select a goal',
invalid_type_error: 'Please select a goal',
}),
}),
stepTwo: z.object({
goal: z
.string({
required_error: 'Select an option',
invalid_type_error: 'Select an option',
})
.min(1),
source: z
.string({
required_error: 'Select an option',
invalid_type_error: 'Select an option',
})
.min(1),
}),
};
export const getConfirmPasswordError = ({ password, confirmPassword }) => {
return password > 6 && confirmPassword > 6 && password !== confirmPassword
? 'Does not match password'
: null;
};
export const signUpValidation = (fields = {}) => {
const { error } = validationSchema.safeParse(fields);
const processErrors = (error) => {
const errors = {};
if (Array.isArray(error?.issues)) {
@@ -38,6 +71,34 @@ export const signUpValidation = (fields = {}) => {
return errors;
};
export const authValidation = (fields = {}) => {
const { error } = authValidationSchema.safeParse(fields);
return processErrors(error);
};
export const resetValidation = (fields = {}) => {
const { error } = resetValidationSchema.safeParse(fields);
return processErrors(error);
};
export const forgotValidation = (fields = {}) => {
const { error } = forgotValidationSchema.safeParse(fields);
return processErrors(error);
};
export const onboardingValidation = (fields = {}, step) => {
const schema =
step === 1
? onboardingValidationSchema.stepOne
: onboardingValidationSchema.stepTwo;
const { error } = schema.safeParse(fields);
return processErrors(error);
};
export const getDisableSubmit = ({ password, confirmPassword }) => {
return !!(password && confirmPassword && password !== confirmPassword);
};
+6 -1
View File
@@ -2,7 +2,7 @@ import { writable } from 'svelte/store';
export const user = writable({
openAuthModal: false,
fetchingUser: false,
fetchingUser: null,
isLoggedIn: false,
currentSession: null,
expiresAt: 0,
@@ -10,8 +10,13 @@ export const user = writable({
export const profile = writable({
id: null,
fullname: '',
avatar_url:
'https://pbs.twimg.com/profile_images/1416443682157473795/dGtFbtht_normal.jpg',
username: 'Elon',
user_id: '',
email: '',
role: '',
goal: '',
source: '',
});
+1 -1
View File
@@ -11,7 +11,7 @@ export interface CurrentSessionType {
export interface UserType {
openAuthModal: boolean;
fetchingUser: boolean;
fetchingUser: boolean | null;
isLoggedIn: boolean;
currentSession: CurrentSessionType | null;
expiresAt: number;
+1 -1
View File
@@ -8,7 +8,7 @@ export interface CurrentSessionType {
export interface UserType {
openAuthModal: boolean;
fetchingUser: boolean;
fetchingUser: boolean | null;
isLoggedIn: boolean;
currentSession: CurrentSessionType | null;
expiresAt: number;