Auth - Login/Signup/Forgot/Rest
This commit is contained in:
Generated
+5
@@ -5290,6 +5290,11 @@
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.19.1",
|
||||
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/zod/-/zod-3.19.1.tgz",
|
||||
"integrity": "sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@
|
||||
"polka": "next",
|
||||
"sapper-environment": "^1.0.1",
|
||||
"sirv": "^1.0.0",
|
||||
"svelte-easy-crop": "^1.0.4"
|
||||
"svelte-easy-crop": "^1.0.4",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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="you@domain.com"
|
||||
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
@@ -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> 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="you@domain.com"
|
||||
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="you@domain.com"
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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> 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="you@domain.com"
|
||||
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="you@domain.com"
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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: '',
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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: '',
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface CurrentSessionType {
|
||||
|
||||
export interface UserType {
|
||||
openAuthModal: boolean;
|
||||
fetchingUser: boolean;
|
||||
fetchingUser: boolean | null;
|
||||
isLoggedIn: boolean;
|
||||
currentSession: CurrentSessionType | null;
|
||||
expiresAt: number;
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface CurrentSessionType {
|
||||
|
||||
export interface UserType {
|
||||
openAuthModal: boolean;
|
||||
fetchingUser: boolean;
|
||||
fetchingUser: boolean | null;
|
||||
isLoggedIn: boolean;
|
||||
currentSession: CurrentSessionType | null;
|
||||
expiresAt: number;
|
||||
|
||||
Reference in New Issue
Block a user