CLASSROOMIO-T-1: Login/Signup UI page

This commit is contained in:
Rotimi Best
2022-11-28 11:23:46 +01:00
parent fa38cca0ba
commit e14af6b1ad
12 changed files with 397 additions and 157 deletions
+25 -3
View File
@@ -1946,9 +1946,9 @@
"integrity": "sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ=="
},
"carbon-icons-svelte": {
"version": "10.28.0",
"resolved": "https://registry.npmjs.org/carbon-icons-svelte/-/carbon-icons-svelte-10.28.0.tgz",
"integrity": "sha512-C3K+U2PRy63WHFZme1HQNju89rhVIQmUkfgBrZzcezdutkhXbSPOY+o/MI0B2mDcQ7G9gfhEpqsXET8gBTWfJQ==",
"version": "10.45.1",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/carbon-icons-svelte/-/carbon-icons-svelte-10.45.1.tgz",
"integrity": "sha512-Uz3byPkSBzjpvLmjt/ZWYJkQG1omfqKh1vzF1FeFYOYqbGOCwc4sHbyF8oet2CseT2cECn1BbEyY4yWvlWYd/w==",
"dev": true
},
"caseless": {
@@ -3846,6 +3846,15 @@
"pinkie": "^2.0.0"
}
},
"polka": {
"version": "1.0.0-next.22",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/polka/-/polka-1.0.0-next.22.tgz",
"integrity": "sha512-a7tsZy5gFbJr0aUltZS97xCkbPglXuD67AMvTyZX7BTDBH384FWf0ZQF6rPvdutSxnO1vUlXM2zSLf5tCKk5RA==",
"requires": {
"@polka/url": "^1.0.0-next.21",
"trouter": "^3.1.0"
}
},
"postcss": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz",
@@ -4166,6 +4175,11 @@
"@babel/runtime": "^7.8.4"
}
},
"regexparam": {
"version": "1.3.0",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/regexparam/-/regexparam-1.3.0.tgz",
"integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g=="
},
"regexpu-core": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz",
@@ -4929,6 +4943,14 @@
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
},
"trouter": {
"version": "3.2.0",
"resolved": "https://flipplib.jfrog.io/artifactory/api/npm/flipp-npm/trouter/-/trouter-3.2.0.tgz",
"integrity": "sha512-rLLXbhTObLy2MBVjLC+jTnoIKw99n0GuJs9ov10J870vDw5qhTurPzsDrudNtBf5w/CZ9ctZy2p2IMmhGcel2w==",
"requires": {
"regexparam": "^1.3.0"
}
},
"true-case-path": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
+68
View File
@@ -0,0 +1,68 @@
<script>
import PrimaryButton from '../PrimaryButton/index.svelte';
import { VARIANTS } from '../PrimaryButton/constants';
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
export let supabase;
export let handleSubmit;
export let isLogin = true;
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
});
}
</script>
<div class="w-full min-h-screen flex items-center justify-center mt-4">
<div class="container border border-gray bg-white">
<div class="flex items-center flex-col p-2 lg:px-8 lg:py-4">
<div class="flex items-center w-full justify-center">
<img
src="/logo-192.png"
alt="ClassroomIO logo"
height="50"
width="50"
data-atf="1"
/>
<h4 class="text-xl">ClassroomIO</h4>
</div>
<form
on:submit|preventDefault={handleSubmit}
class="flex items-center flex-col w-10/12"
>
<slot />
</form>
<div class="w-10/12 mb-3">
<p class="text-sm mb-5">or sign up with:</p>
<PrimaryButton
variant={VARIANTS.OUTLINED}
onClick={signInWithGoogle}
className="py-3 sm:w-full w-full"
>
<GoogleIconColored />
<span class="ml-2">Sign up with Google</span>
</PrimaryButton>
</div>
</div>
<div class="w-full p-6 border-t border-grey text-center">
{#if isLogin}
Not registered yet? <a
class="hover:underline text-blue-700"
href="/signup">Sign up</a
>
{:else}
Already have an account? <a
class="hover:underline text-blue-700"
href="/login">Log In</a
>
{/if}
</div>
</div>
</div>
<style>
.container {
width: 450px;
}
</style>
+35 -4
View File
@@ -1,5 +1,8 @@
<script>
import { onMount } from 'svelte';
import IconButton from '../IconButton/index.svelte';
import PasswordEye from '../Icons/PasswordEye.svelte';
export let label = '';
export let placeholder = '';
export let value = '';
@@ -7,6 +10,7 @@
export let className = '';
export let inputClassName = '';
export let type = 'text';
export let isPassword = false;
export let autoFocus = false;
export let isRequired = false;
export let isDisabled = false;
@@ -16,6 +20,7 @@
export let onChange = () => {}; // This is to know if element is 'dirty'
let ref;
let fieldNode;
function typeAction(node) {
node.type = type;
@@ -23,18 +28,29 @@
if (isRequired) {
node.required = '';
}
fieldNode = node;
}
function handlePasswordEye() {
type = type === 'password' ? 'text' : 'password';
typeAction(fieldNode);
}
onMount(() => {
if (autoFocus) {
ref.focus();
}
if (type === 'password') {
isPassword = true;
}
});
</script>
<label class="block {className}">
<label class="block relative {className}">
{#if label}
<label for="text-field" class="m-0">
<label for="text-field" class="m-0 font-light">
{label}
{#if isRequired}
@@ -44,9 +60,9 @@
{/if}
<input
use:typeAction
class="form-input p-2 mt-1 block w-full bg-white border {inputClassName} {errorMessage
class="form-input p-3 mt-1 block w-full border-none bg-gray-100 {inputClassName} {errorMessage
? 'border-red-500'
: 'border-gray-300'}"
: ''}"
on:keydown={onKeyDown}
{placeholder}
bind:value
@@ -56,6 +72,13 @@
autocomplete={autoComplete ? 'on' : 'off'}
on:blur={onChange}
/>
{#if isPassword}
<span class="password-eye">
<IconButton value="write-code" onClick={handlePasswordEye}>
<PasswordEye isClosed={type === 'password' ? true : false} />
</IconButton>
</span>
{/if}
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{:else if helperMessage}
@@ -64,3 +87,11 @@
</p>
{/if}
</label>
<style>
.password-eye {
position: absolute;
top: 33px;
right: 6px;
}
</style>
+2
View File
@@ -6,6 +6,7 @@
export let buttonClassName = '';
export let contained = false;
export let value = '';
export let type = 'button';
export let size = 'large';
export let color = '';
export let toolTipProps = {
@@ -28,6 +29,7 @@
class="root {color} {selected && 'active'} {size} {contained &&
'contained'} {buttonClassName}"
{disabled}
{type}
on:click={handleClick}
>
<slot />
@@ -0,0 +1,32 @@
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 8.76233C3.75 7.96961 3.96841 7.22701 4.34784 6.59117V3.89404H1.65072C0.58025 5.28429 0 6.97864 0 8.76233C0 10.546 0.58025 12.2404 1.65072 13.6306H4.34784V10.9335C3.96841 10.2976 3.75 9.55504 3.75 8.76233Z"
fill="#FBBD00"
/>
<path
d="M8 13.0124L6.125 14.8874L8 16.7624C9.78372 16.7624 11.478 16.1822 12.8683 15.1117V12.4175H10.174C9.53262 12.7983 8.78691 13.0124 8 13.0124Z"
fill="#0F9D58"
/>
<path
d="M4.34739 10.9336L1.65027 13.6307C1.86221 13.906 2.09299 14.1696 2.34271 14.4193C3.85371 15.9303 5.86267 16.7624 7.99955 16.7624V13.0124C6.4488 13.0124 5.08964 12.1774 4.34739 10.9336Z"
fill="#31AA52"
/>
<path
d="M16 8.76258C16 8.27586 15.9559 7.78824 15.869 7.3133L15.7987 6.92896H8V10.679H11.7954C11.4268 11.4121 10.8601 12.0103 10.174 12.4176L12.8682 15.1119C13.1435 14.9 13.4071 14.6692 13.6568 14.4195C15.1678 12.9084 16 10.8995 16 8.76258Z"
fill="#3C79E6"
/>
<path
d="M11.0052 5.75723L11.3367 6.08867L13.9883 3.43705L13.6569 3.10561C12.1459 1.59461 10.1369 0.762451 8 0.762451L6.125 2.63745L8 4.51245C9.13519 4.51245 10.2025 4.95451 11.0052 5.75723Z"
fill="#CF2D48"
/>
<path
d="M7.99958 4.51245V0.762451C5.86271 0.762451 3.85374 1.59461 2.34271 3.10558C2.09299 3.3553 1.86221 3.61889 1.65027 3.89417L4.34739 6.5913C5.08967 5.34745 6.44883 4.51245 7.99958 4.51245Z"
fill="#EB4132"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+43
View File
@@ -0,0 +1,43 @@
<script>
export let isClosed = true;
</script>
{#if isClosed}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.6 11.3L3.3 10.6C2.6 9.8 1.9 9 1.5 8C2.5 5.5 5.3 3.5 8 3.5C8.7 3.5 9.4 3.6 10 3.9L10.8 3.1C9.9 2.7 9 2.5 8 2.5C4.7 2.6 1.7 4.7 0.5 7.8V8.1C1 9.3 1.7 10.4 2.6 11.3ZM12.6 4.1C13.9 5 14.9 6.3 15.5 7.8V8.2C14.3 11.3 11.3 13.4 8 13.5C6.7 13.4 5.4 13.1 4.3 12.4L1.7 15L1 14.3L14.3 1L15 1.7L12.6 4.1ZM7.8 6.1C6.9 6.1 6.1 6.9 6 7.9L5.1 8.8C5 8.3 5 7.7 5.1 7.2C5.5 5.6 7.2 4.7 8.7 5.2L7.8 6.1ZM10 8C10 9.1 9.1 10 8 10C7.7 10 7.3 9.9 7 9.7L9.7 7C9.9 7.3 10 7.6 10 8ZM5 11.7C5.9 12.2 7 12.5 8 12.5C10.6 12.5 13.4 10.5 14.5 8C13.9 6.7 13 5.6 11.9 4.8L10.5 6.2C11.2 7.2 11.2 8.6 10.5 9.6C9.5 11 7.7 11.3 6.3 10.4L5 11.7Z"
fill="#ADADAD"
/>
</svg>
{:else}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.666748 8.00008C0.666748 8.00008 3.33341 2.66675 8.00008 2.66675C12.6667 2.66675 15.3334 8.00008 15.3334 8.00008C15.3334 8.00008 12.6667 13.3334 8.00008 13.3334C3.33341 13.3334 0.666748 8.00008 0.666748 8.00008Z"
stroke="#ADADAD"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z"
stroke="#ADADAD"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/if}
+11 -1
View File
@@ -3,6 +3,7 @@
import Avatar from '../Avatar/index.svelte';
import { user, profile } from '../../utils/store/user';
import PrimaryButton from '../PrimaryButton/index.svelte';
import { VARIANTS } from '../PrimaryButton/constants';
export let segment;
export let disableLogin;
@@ -88,11 +89,20 @@
/>
</a>
{:else if !disableLogin}
<div class="flex">
<PrimaryButton
label="Login"
className="py-2 px-4"
variant={VARIANTS.NONE}
className="py-2 px-6 mx-2 app-background-color"
onClick={() => goto('/login')}
/>
<PrimaryButton
label="Sign Up"
variant={VARIANTS.CONTAINED}
className="py-2 px-6 mx-2"
onClick={() => goto('/signup')}
/>
</div>
{/if}
</li>
<!-- <li class="hidden">
+7 -6
View File
@@ -3,14 +3,15 @@ export const VARIANTS = {
CONTAINED_DANGER: 'CONTAINED_DANGER',
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
OUTLINED: 'OUTLINED',
NONE: 'NONE',
};
export const VARIANTS_CLASS = {
[VARIANTS.CONTAINED]: 'bg-blue-700 hover:bg-blue-900 text-white font-bold',
[VARIANTS.CONTAINED_SUCCESS]:
'bg-green-700 hover:bg-green-900 text-white font-bold',
[VARIANTS.CONTAINED_DANGER]:
'bg-red-700 hover:bg-red-900 text-white font-bold',
[VARIANTS.CONTAINED]: 'border-none bg-blue-700 hover:bg-blue-900 text-white',
[VARIANTS.CONTAINED_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
[VARIANTS.OUTLINED]:
'border border-gray-700 hover:border-black-300 hover:bg-gray-200 text-black',
'border border-gray hover:border-black-300 hover:bg-gray-200 text-black',
[VARIANTS.NONE]:
'border-none hover:border-black-300 hover:bg-gray-200 text-black',
};
+2 -2
View File
@@ -6,7 +6,7 @@
export let className = '';
export let onClick = () => {};
export let name = '';
export let type = 'text';
export let type = 'button';
export let variant = VARIANTS.CONTAINED;
export let disablePadding = false;
export let isDisabled = false;
@@ -21,7 +21,7 @@
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
variant
]} {!disablePadding &&
'py-1 px-5'} rounded {className} py-2 w-full justify-center sm:w-auto {isLoading &&
'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"
on:click={onClick}
{name}
+5
View File
@@ -197,5 +197,10 @@
:global(:root) {
--main-primary-color: rgba(29, 78, 216, 1);
--border-color: #eaecef;
--app-background-color: #fafbfc;
}
:global(.app-background-color) {
background-color: var(--app-background-color);
}
</style>
+28 -36
View File
@@ -10,11 +10,13 @@
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
import AuthUI from '../components/AuthUI/index.svelte';
export let config;
let supabase = getSupabase(config);
let email;
let password;
let loading = false;
let success = false;
let errors = {};
@@ -23,6 +25,7 @@
if (!email) {
errors = {
email: 'Email is required',
password: 'Password is required',
};
return;
}
@@ -50,26 +53,11 @@
</script>
<svelte:head>
<title>Join ClassroomIO</title>
<title>Login - ClassroomIO</title>
</svelte:head>
<div class="w-full h-screen flex items-center justify-center ">
<div class="container border border-gray-300 rounded-md p-2 lg:p-12">
<form
on:submit|preventDefault={handleSubmit}
class="flex items-center flex-col w-full"
>
<img
src="/logo-192.png"
alt="ClassroomIO logo"
height="70"
width="70"
data-atf="1"
/>
<h4 class="text-lg m-0 mt-3">Login</h4>
<p class="text-sm">into ClassroomIO</p>
<div class="my-4 w-full">
<AuthUI {supabase} isLogin={true} {handleSubmit}>
<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" />
@@ -81,24 +69,36 @@
<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.
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="Email address"
label="Your email"
bind:value={email}
type="email"
autoFocus={true}
placeholder="you@domain.com"
inputClassName="rounded-md w-full px-2 py-4"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.email}
/>
<p class="py-3 text-md">
A magic link will be sent to your email, please open the link
</p>
<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>
{/if}
</div>
@@ -106,19 +106,11 @@
<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...' : 'Next'}
label={loading ? 'Creating...' : 'Log In'}
type="submit"
className="py-2 px-4"
className="py-3 sm:w-full w-full"
isDisabled={loading}
/>
</div>
{/if}
</form>
</div>
</div>
<style>
.container {
width: 450px;
}
</style>
</AuthUI>
+104 -70
View File
@@ -5,39 +5,49 @@
</script>
<script>
import { Chasing } from 'svelte-loading-spinners';
import TextField from '../components/Form/TextField.svelte';
import Select from '../components/Form/Select.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
import AuthUI from '../components/AuthUI/index.svelte';
export let config;
let supabase = getSupabase(config);
let email;
let userType = 0;
let name;
let password;
let passwordAgain;
let loading = false;
let success = false;
let errors = {};
const userTypes = [
{
text: '--Select an option--',
value: 0,
},
{
text: 'Student',
value: 1,
},
{
text: 'Organization/Teacher',
value: 2,
},
];
async function handleSubmit() {
if (!email) {
errors = {
email: 'Email is required',
password: 'Password is required',
};
return;
}
try {
loading = true;
const { error } = await supabase.auth.signIn({ email });
const { data, error } = await supabase.auth.signIn(
{ email },
{
redirectTo: window.location.origin + '/login',
}
);
if (error) throw error;
alert('Check your email for the login link!');
success = true;
email = {};
} catch (error) {
alert(error.error_description || error.message);
errors = {
email: error.error_description || error.message,
};
} finally {
loading = false;
}
@@ -45,60 +55,84 @@
</script>
<svelte:head>
<title>Join ClassroomIO</title>
<title>Sign Up - ClassroomIO</title>
</svelte:head>
<div class="w-full h-screen flex items-center justify-center ">
<div class="container border border-gray-300 rounded-md p-12">
<form
on:submit|preventDefault={handleSubmit}
class="flex items-center flex-col w-full"
>
<img
src="/logo-192.png"
alt="ClassroomIO logo"
height="70"
width="70"
data-atf="1"
/>
<h4 class="text-lg m-0 mt-3">Signup</h4>
<p class="text-sm">for ClassroomIO</p>
<!-- <div class="my-4 w-full">
<Select
label="Who are you?"
bind:value={userType}
options={userTypes}
valueKey="value"
isRequired={true}
/>
</div> -->
<div class="my-4 w-full">
<TextField
bind:value={email}
autofocus={true}
label="Email"
type="email"
placeholder="Enter your email"
inputClassName="rounded-md w-full px-2 py-4"
isRequired={true}
/>
<p class="py-3 text-sm">
A magic link will be sent to your email, please open the link
<AuthUI {supabase} isLogin={false} {handleSubmit}>
<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>
<div class="my-4 w-full flex justify-between items-center">
<a href="/login" class="text-blue-700 text-sm">I have an account</a>
<PrimaryButton label="Next" type="submit" className="py-2 px-4" />
{:else}
<p class="text-lg font-semibold mb-6">Create a free account</p>
<TextField
label="Full Name"
bind:value={name}
type="text"
autoFocus={true}
placeholder="e.g Joke Silva"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.name}
/>
<TextField
label="Your Email"
bind:value={email}
type="email"
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}
/>
<TextField
label="Confirm Password"
bind:value={passwordAgain}
type="password"
placeholder="************"
className="mb-6"
inputClassName="w-full"
isDisabled={loading}
errorMessage={errors.passwordAgain}
/>
<div class="w-full text-right">
<a class="text-md text-blue-700" href="/forgot"> Forgot password? </a>
</div>
</form>
{/if}
</div>
</div>
<style>
.container {
width: 450px;
}
</style>
{#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}
</AuthUI>