CLASSROOMIO-T-5: Educators Settings CRUD

Co-authored-by: Rotimi Best <[email protected]>

Merge-request: CLASSROOMIO-MR-7
Merged-by: Digdippa Team <[email protected]>
This commit is contained in:
Rotimi Best
2023-05-02 23:55:26 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 01a1f0f907
commit 5f14b36ef8
27 changed files with 832 additions and 89 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
export let name = '';
export let src = '';
export let className = '';
export let shape = 'rounded-full';
export let width = 'w-10';
export let height = 'h-10';
</script>
@@ -9,5 +10,5 @@
<img
alt={name}
{src}
class="{className} {width} {height} rounded-full bg-light-blue-100"
class="{className} {width} {height} {shape} bg-light-blue-100"
/>
+5 -1
View File
@@ -1,5 +1,9 @@
<script>
export let className = '';
</script>
<div
class="w-full h-80 flex items-center justify-center flex-col border rounded border-gray-200"
class="{className} w-full h-80 flex items-center justify-center flex-col border rounded border-gray-200"
>
<slot />
</div>
+15
View File
@@ -0,0 +1,15 @@
<script>
export let label = 'Coming soon';
</script>
<p
class="org-avatar text-xs bg-blue-200 text-blue-700 font-bold py-1 px-1 rounded-md text-center"
>
{label}
</p>
<style>
.org-avatar {
width: fit-content;
}
</style>
+1 -1
View File
@@ -33,7 +33,7 @@
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="text-2xl my-5 ">No Courses Created</h3>
<h3 class="text-2xl my-5">No Courses Created</h3>
<p class="w-1/3 text-center">
Share your knowledge with the world by creating engaging courses for
your students.
+1 -19
View File
@@ -1,9 +1,6 @@
<script>
import NotificationIcon from 'carbon-icons-svelte/lib/Notification20';
import Avatar from '../Avatar/index.svelte';
import { user, profile } from '../../utils/store/user';
export let segment;
export let navClass = '';
</script>
@@ -20,22 +17,7 @@
<li>
<NotificationIcon class="text-white mr-2" />
</li>
<li>
{#if $user.isLoggedIn}
<a
class="block"
aria-current={segment === 'profile' ? 'page' : undefined}
href={`/profile/${$profile.id}`}
>
<Avatar
src={$profile.avatar_url}
name={$profile.username}
width="w-7"
height="h-7"
/>
</a>
{/if}
</li>
<li />
</ul>
</nav>
+1 -15
View File
@@ -1,6 +1,5 @@
<script>
import { goto } from '@sapper/app';
import Avatar from '../Avatar/index.svelte';
import { user, profile } from '../../utils/store/user';
import PrimaryButton from '../PrimaryButton/index.svelte';
import { VARIANTS } from '../PrimaryButton/constants';
@@ -76,20 +75,7 @@
<span class="flex-grow" />
<li>
{#if $user.isLoggedIn}
<a
class="block"
aria-current={segment === 'profile' ? 'page' : undefined}
href={`/profile/${$profile.id}`}
>
<Avatar
src={$profile.avatar_url}
name={$profile.username}
width="w-7"
height="h-7"
/>
</a>
{:else if !disableLogin}
{#if !disableLogin}
<div class="flex">
<PrimaryButton
label="Login"
@@ -7,7 +7,7 @@
Pagination,
DataTableSkeleton,
} from 'carbon-components-svelte';
import { orgAudience } from '../../utils/store/org';
import { orgAudience } from '../../../utils/store/org';
export let isLoading = false;
@@ -1,10 +1,10 @@
<script>
import { onMount } from 'svelte';
import Box from '../Box/index.svelte';
import Box from '../../Box/index.svelte';
import AudienceList from './AudienceList.svelte';
import AudienceEmptyIcon from '../Icons/AudienceEmptyIcon.svelte';
import { getOrgAudience } from '../../utils/services/org';
import { orgAudience, currentOrg } from '../../utils/store/org';
import AudienceEmptyIcon from '../../Icons/AudienceEmptyIcon.svelte';
import { getOrgAudience } from '../../../utils/services/org';
import { orgAudience, currentOrg } from '../../../utils/store/org';
let isLoading = false;
onMount(async () => {
@@ -0,0 +1,98 @@
<script>
import { goto } from '@sapper/app';
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '../../Form/TextField.svelte';
import ComingSoon from '../../ComingSoon/index.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import { currentOrg } from '../../../utils/store/org';
import { supabase } from '../../../utils/functions/supabase';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
let siteName;
let errors = '';
let isLoading = false;
async function handleChangeDomain() {
isLoading = true;
const { data: org, error } = await supabase
.from('organization')
.update(
{ siteName },
{
returning: 'minimal', // Don't return the value after inserting
}
)
.match({ id: $currentOrg.id });
console.log('Updating organisation', org);
if (error) {
console.log('Error: create organisation', error);
errors = 'Sitename already exists.';
} else {
$snackbarStore.open = true;
$snackbarStore.message = 'Success';
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
$currentOrg.siteName = siteName;
goto(`/org/${$currentOrg.siteName}/settings/domains`);
}
isLoading = false;
}
function setSiteName(_siteName) {
if (!siteName) {
siteName = _siteName;
}
}
$: setSiteName($currentOrg.siteName);
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4} class="text-lg"><strong>Add</strong></Column>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 mb-5">
Add your team mates or collaborators to your organization. Start working
together
</p>
<div>
<!-- Org Site Name -->
<TextField
label="URL"
helperMessage={`https://${siteName || ''}.classroomio.com`}
bind:value={siteName}
type="text"
placeholder="e.g traversymedia"
className="mb-5 w-full"
labelClassName=""
errorMessage={errors}
/>
<PrimaryButton
label="Change Domain"
className="mb-6 py-4"
variant={VARIANTS.CONTAINED}
onClick={handleChangeDomain}
isDisabled={isLoading}
/>
</div>
</Column>
</Row>
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4} class="text-lg"
><strong>Custom domain</strong></Column
>
<Column sm={2} md={6} lg={8}>
<div class="flex items-center">
<p class="mr-3">Add your own domain name</p>
<ComingSoon />
</div>
</Column>
</Row>
</Grid>
@@ -0,0 +1,141 @@
<script>
import { goto } from '@sapper/app';
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '../../Form/TextField.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import UploadImage from '../../UploadImage/index.svelte';
import { supabase } from '../../../utils/functions/supabase';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
import { currentOrg } from '../../../utils/store/org';
let avatar;
let loading = false;
async function handleUpdate() {
try {
loading = true;
const updates = {
name: $currentOrg.name,
};
if (avatar) {
const filename = `user/${$currentOrg.name + Date.now()}.webp`;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
updates.avatar_url = publicURL;
$currentOrg.avatar_url = publicURL;
}
avatar = undefined;
}
let { error } = await supabase
.from('organization')
.update(updates, {
returning: 'minimal', // Don't return the value after inserting
})
.match({ id: $currentOrg.id });
currentOrg.update((_currentOrg) => ({
..._currentOrg,
...updates,
}));
$snackbarStore.open = true;
$snackbarStore.message = 'Update successful';
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
if (error) throw error;
} catch (error) {
let message = error.message;
if (message.includes('profile_username_key')) {
message = 'username already exists';
}
$snackbarStore.open = true;
$snackbarStore.message = `Update failed: ${message}`;
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
loading = false;
} finally {
loading = false;
}
}
function gotoSetting(pathname) {
goto(`/org/${$currentOrg.siteName}/settings${pathname}`);
}
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Organization Profile</strong></Column>
<Column sm={2} md={6} lg={8}>
<TextField
label="Organization Name"
bind:value={$currentOrg.name}
className="w-60 mb-5"
/>
<UploadImage
bind:avatar
src={$currentOrg.avatar_url}
shape="rounded-md"
widthHeight="w-24 h-24"
/>
<PrimaryButton
label="Update Organization"
className="px-6 py-3 mr-5 mt-5"
isLoading={loading}
isDisabled={loading}
onClick={handleUpdate}
/>
</Column>
</Row>
<!-- <Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Theme</strong></Column>
<Column sm={2} md={6} lg={8}>
<h4>Set your brand</h4>
</Column>
</Row> -->
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Custom Domain</strong></Column>
<Column sm={2} md={6} lg={8}>
<h4>Customise your Domain</h4>
<p class="text-sm text-gray-500">
Create a custom URL so your audience can get to your organization easily
</p>
<PrimaryButton
label="Edit domain"
className="my-7 py-5 px-10 text-blue-700"
variant={VARIANTS.OUTLINED}
onClick={() => gotoSetting('/domains')}
/>
</Column>
</Row>
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Team</strong></Column>
<Column sm={2} md={6} lg={8}>
<h4>Set up your website</h4>
<p class="text-sm text-gray-500">
Add team mates to your organization so you can both collaborate on
projects.
</p>
<PrimaryButton
label="Manage Team"
className="my-7 py-5 px-10 text-blue-700"
variant={VARIANTS.OUTLINED}
onClick={() => gotoSetting('/teams')}
/>
</Column>
</Row>
</Grid>
+129
View File
@@ -0,0 +1,129 @@
<script>
import { goto } from '@sapper/app';
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '../../Form/TextField.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import UploadImage from '../../UploadImage/index.svelte';
import { supabase } from '../../../utils/functions/supabase';
import { profile } from '../../../utils/store/user';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
let avatar;
let loading = false;
async function logout() {
const { error } = await supabase.auth.signOut();
console.log('error logout', error);
goto('/login');
}
async function handleUpdate() {
try {
loading = true;
const updates = {
fullname: $profile.fullname,
username: $profile.username,
};
if (avatar) {
const filename = `user/${$profile.username + Date.now()}.webp`;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
updates.avatar_url = publicURL;
$profile.avatar_url = publicURL;
}
avatar = undefined;
}
let { error } = await supabase
.from('profile')
.update(updates, {
returning: 'minimal', // Don't return the value after inserting
})
.match({ id: $profile.id });
profile.update((_profile) => ({
..._profile,
...updates,
}));
$snackbarStore.open = true;
$snackbarStore.message = 'Update successful';
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
if (error) throw error;
} catch (error) {
let message = error.message;
if (message.includes('profile_username_key')) {
message = 'username already exists';
}
$snackbarStore.open = true;
$snackbarStore.message = `Update failed: ${message}`;
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
loading = false;
} finally {
loading = false;
}
}
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Profile Picture</strong></Column>
<Column sm={2} md={6} lg={8}>
<UploadImage
bind:avatar
src={$profile.avatar_url}
widthHeight="w-24 h-24"
/>
</Column>
</Row>
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4}><strong>Personal Information</strong></Column>
<Column sm={2} md={6} lg={8}>
<TextField
label="Full Name"
bind:value={$profile.fullname}
className="w-60 mb-4"
/>
<TextField
label="Username"
bind:value={$profile.username}
className="w-60 mb-4"
/>
<TextField
label="Email"
bind:value={$profile.email}
className="w-60 mb-4"
/>
</Column>
</Row>
<Row class="m-5 w-full flex items-center justify-center">
<PrimaryButton
label="Update Profile"
className="px-6 py-3 mr-5"
isLoading={loading}
isDisabled={loading}
onClick={handleUpdate}
/>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Log out"
onClick={logout}
/>
</Row>
</Grid>
@@ -0,0 +1,205 @@
<script>
import {
Grid,
Row,
Column,
Select,
SelectItem,
} from 'carbon-components-svelte';
import { Moon } from 'svelte-loading-spinners';
import TextField from '../../Form/TextField.svelte';
import TextChip from '../../Chip/Text.svelte';
import ComingSoon from '../../ComingSoon/index.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import { validateEmailInString } from '../../../utils/functions/validator';
import { ROLE_LABEL, ROLE } from '../../../utils/constants/roles';
import { currentOrg, orgTeam } from '../../../utils/store/org';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
import { getOrgTeam } from '../../../utils/services/org';
import { profile } from '../../../utils/store/user';
import { supabase } from '../../../utils/functions/supabase';
let emailsStr = '';
let errorMessage = '';
let role = ROLE.ADMIN;
let isFetching = false;
let isLoading = false;
let isRemoving = false;
async function onSendInvite() {
const {
hasError,
error: _error,
emails,
} = validateEmailInString(emailsStr);
if (hasError) {
errorMessage = _error;
return;
}
isLoading = true;
let apiError = '';
emails.forEach(async (email, index) => {
if (apiError) return;
const { data, error } = await supabase.from('organizationmember').insert({
organization_id: $currentOrg.id,
email,
role_id: role,
verified: false,
});
console.log('data', data);
if (error) {
apiError = error;
console.error('onSendInvite:', error);
$snackbarStore.open = true;
$snackbarStore.message = `Failed to send invite, please try again`;
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
isLoading = false;
return;
}
const [newMember] = data || [];
if (newMember) {
orgTeam.update((team) => [
{
id: newMember.id,
email: newMember?.email,
verified: newMember?.verified,
role: ROLE_LABEL[newMember?.role_id] || '',
isAdmin: newMember?.role_id === ROLE.ADMIN,
},
...team,
]);
}
const isLast = index === emails.length - 1;
if (isLast) {
$snackbarStore.open = true;
$snackbarStore.message = 'Success';
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
emailsStr = '';
isLoading = false;
}
});
}
async function onRemove(id) {
console.error('onRemove called');
isRemoving = true;
const { data, error } = await supabase
.from('organizationmember')
.delete()
.match({ id });
if (error) {
console.error('onRemove:', error);
$snackbarStore.open = true;
$snackbarStore.message = `Failed to remove user, please try again`;
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
} else if (data) {
orgTeam.update((team) => team.filter((member) => member.id !== id));
}
isRemoving = false;
}
const fetchTeam = async (id) => {
if (!id) return;
isFetching = true;
await getOrgTeam(id);
isFetching = false;
};
const isTeamMemberAdmin = (members, profileId) => {
return members.some(
(member) => member.profileId === profileId && member.isAdmin
);
};
$: fetchTeam($currentOrg.id);
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4} class="text-lg"><strong>Add</strong></Column>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 mb-5">
Add your team mates or collaborators to your organization. Start working
together
</p>
<div class="">
<TextField
label="Invite"
placeholder="email comma separated"
bind:value={emailsStr}
className="mb-3"
{errorMessage}
/>
<Select labelText="Role" bind:selected={role} class="mb-5 w-40">
<SelectItem value={ROLE.ADMIN} text={ROLE_LABEL[ROLE.ADMIN]} />
<SelectItem value={ROLE.TUTOR} text={ROLE_LABEL[ROLE.TUTOR]} />
</Select>
<PrimaryButton
label="Send Invite"
onClick={onSendInvite}
{isLoading}
isDisabled={isLoading}
/>
</div>
</Column>
</Row>
<Row class="py-7 border border-t-0 border-l-0 border-r-0 border-gray-300">
<Column sm={2} md={2} lg={4} class="text-lg"
><strong>Members</strong></Column
>
<Column sm={2} md={6} lg={8}>
{#if isFetching}
<Moon />
{:else}
{#each $orgTeam as teamMember}
<div class="flex justify-between items-center mb-5">
<div class="flex">
<p class="text-sm text-gray-500 mr-3">
{teamMember.email}
</p>
<TextChip
value={teamMember.role}
className="text-xs mr-3"
size="sm"
/>
{#if !teamMember.verified}
<TextChip
value="Invite Sent"
className="text-xs bg-yellow-200 text-yellow-700"
size="sm"
/>
{:else if teamMember.profileId === $profile.id}
<ComingSoon label="You" />
{/if}
</div>
{#if teamMember.profileId !== $profile.id && isTeamMemberAdmin($orgTeam, $profile.id)}
<PrimaryButton
label="Remove"
variant={VARIANTS.TEXT_DANGER}
onClick={() => onRemove(teamMember.id)}
isLoading={isRemoving}
isDisabled={isRemoving}
/>
{/if}
</div>
{/each}
{/if}
</Column>
</Row>
</Grid>
+23
View File
@@ -0,0 +1,23 @@
<script>
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
import Profile from './Profile.svelte';
// import Account from './Account.svelte';
import OrgSettings from './OrgSettings.svelte';
</script>
<Tabs autoWidth>
<Tab label="Profile" />
<!-- <Tab label="Account" /> -->
<Tab label="Organization Settings" />
<svelte:fragment slot="content">
<TabContent>
<Profile />
</TabContent>
<!-- <TabContent>
<Account />
</TabContent> -->
<TabContent>
<OrgSettings />
</TabContent>
</svelte:fragment>
</Tabs>
+22 -6
View File
@@ -1,6 +1,5 @@
<script>
import { stores } from '@sapper/app';
import UserAvatar from 'carbon-icons-svelte/lib/UserAvatar20';
import Help from 'carbon-icons-svelte/lib/Help20';
import TextChip from '../../components/Chip/Text.svelte';
@@ -9,7 +8,9 @@
import CourseIcon from '../../components/Icons/CourseIcon.svelte';
import SiteSettingsIcon from '../../components/Icons/SiteSettingsIcon.svelte';
import AudienceIcon from '../../components/Icons/AudienceIcon.svelte';
import Avatar from '../../components/Avatar/index.svelte';
import { currentOrg } from '../../utils/store/org';
import { profile } from '../../utils/store/user';
let orgSiteName = '';
let activeClass = 'bg-gray-200';
@@ -32,10 +33,20 @@
<aside class="root bg-gray-100 h-full">
<div class="h-full flex flex-col">
<div class="border-b border-gray-200 pt-5 px-4">
<TextChip
value={$currentOrg.shortName}
className="bg-blue-200 font-bold"
/>
{#if $currentOrg.avatar_url}
<Avatar
src={$currentOrg.avatar_url}
name={$currentOrg.name}
shape="rounded-md"
width="w-7"
height="h-7"
/>
{:else}
<TextChip
value={$currentOrg.shortName}
className="bg-blue-200 font-bold"
/>
{/if}
<OrgSelector />
@@ -101,7 +112,12 @@
`/org/${orgSiteName}/settings`
) && activeClass}"
>
<UserAvatar />
<Avatar
src={$profile.avatar_url}
name={$profile.username}
width="w-7"
height="h-7"
/>
<p class="ml-2">Settings</p>
</li>
</a>
@@ -1,5 +1,6 @@
<script>
import TextChip from '../Chip/Text.svelte';
import ComingSoon from '../ComingSoon/index.svelte';
export let size;
export let hasDivider = false;
@@ -28,7 +29,7 @@
</button>
{#if disabled}
<div class="coming-soon">
<TextChip value="Coming soon" className="text-xs" />
<ComingSoon />
</div>
{/if}
</div>
+7 -1
View File
@@ -4,6 +4,8 @@ export const VARIANTS = {
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
OUTLINED: 'OUTLINED',
NONE: 'NONE',
TEXT: 'TEXT',
TEXT_DANGER: 'TEXT_DANGER',
};
export const VARIANTS_CLASS = {
@@ -11,7 +13,11 @@ export const VARIANTS_CLASS = {
[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 hover:border-black-300 hover:bg-gray-200 text-black',
'border border-blue-700 hover:border-black-300 hover:bg-blue-200 text-black',
[VARIANTS.NONE]:
'border-none hover:border-black-300 hover:bg-gray-200 text-black',
[VARIANTS.TEXT]:
'text-black hover:border border-t-0 border-l-0 border-r-0 border-black-300',
[VARIANTS.TEXT_DANGER]:
'text-red-500 hover:border border-t-0 border-l-0 border-r-0 border-red-300',
};
+10 -3
View File
@@ -9,13 +9,19 @@
import { snackbarStore, snackbarStoreInitialState } from './store';
let severityColor;
let timeoutId;
function handleClose() {
if (typeof $snackbarStore.handleClose === 'number') {
$snackbarStore.handleClose();
}
snackbarStore.set(snackbarStoreInitialState);
snackbarStore.update((_s) => ({
..._s,
...snackbarStoreInitialState,
}));
clearTimeout(timeoutId);
timeoutId = null;
}
// When open is true trigger autoHideDuration
@@ -26,8 +32,9 @@
const { autoHideDuration } = $snackbarStore;
if (typeof autoHideDuration === 'number') {
setTimeout(handleClose, autoHideDuration);
if (typeof autoHideDuration === 'number' && !timeoutId) {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleClose, autoHideDuration);
}
}
+24 -32
View File
@@ -1,7 +1,10 @@
<script>
export let avatar;
export let src;
export let widthHeight;
import Camera from 'carbon-icons-svelte/lib/Camera20';
export let avatar = null;
export let src = null;
export let widthHeight = null;
export let shape = 'rounded-full';
let fileinput;
@@ -16,43 +19,33 @@
};
</script>
<section class="flex justify-center">
<section class="width-fit p-3 flex flex-col items-center">
<div
class="avatar-container {widthHeight ||
'setwidthheight'} pointer border-2 border-gray-200 relative hover:bg-gray-400 hover:bg-opacity-75 cursor-pointer rounded-full"
on:click={() => {
fileinput.click();
}}
'setwidthheight'} pointer border-2 border-gray-200 relative {shape}"
>
{#if src}
<img class="w-full h-full rounded-full" {src} alt="Avatar" />
<img class="w-full h-full {shape}" {src} alt="Avatar" />
{:else if avatar}
<img class="w-full h-full rounded-full" src={avatar} alt="Avatar" />
<img class="w-full h-full {shape}" src={avatar} alt="Avatar" />
{:else}
<img
class="w-full h-full rounded-full"
class="w-full h-full {shape}"
src="https://cdn4.iconfinder.com/data/icons/small-n-flat/24/user-alt-512.png"
alt=""
/>
{/if}
<div
class="upload-icon w-full h-full flex items-center justify-center top-0"
>
<svg
class="text-white"
width="24"
height="24"
viewBox="0 0 24 24"
role="presentation"
><g fill="currentColor" fill-rule="evenodd"
><path
d="M2 6.994C2 5.893 2.898 5 3.99 5h16.02C21.108 5 22 5.895 22 6.994v12.012A1.997 1.997 0 0120.01 21H3.99A1.994 1.994 0 012 19.006V6.994zM12 17a4 4 0 100-8 4 4 0 000 8zm5-8c0 .556.448 1 1 1 .556 0 1-.448 1-1 0-.556-.448-1-1-1-.556 0-1 .448-1 1zM8 4c0-.552.453-1 .997-1h6.006c.55 0 .997.444.997 1v1H8V4z"
/><circle cx="12" cy="13" r="2" /></g
></svg
>
</div>
</div>
<p
class="width-fit text-blue-700 flex items-center cursor-pointer text-sm mt-3"
on:click={() => {
fileinput.click();
}}
>
<Camera /> <span class="ml-2">Upload Image</span>
</p>
<input
style="display:none"
type="file"
@@ -63,6 +56,10 @@
</section>
<style>
.width-fit {
width: fit-content;
}
.avatar-container.setwidthheight {
height: 128px;
width: 128px;
@@ -71,9 +68,4 @@
.upload-icon {
display: none;
}
.avatar-container:hover .upload-icon {
display: flex;
position: absolute;
}
</style>
+1 -1
View File
@@ -1,6 +1,6 @@
<script>
import PrimaryButton from '../../../components/PrimaryButton/index.svelte';
import Audience from '../../../components/Audience/index.svelte';
import Audience from '../../../components/Org/Audience/index.svelte';
let isLoading = false;
function exportAudience() {
-1
View File
@@ -1 +0,0 @@
settings
@@ -0,0 +1,22 @@
<script>
import Domains from '../../../../components/Org/Settings/Domains.svelte';
import { currentOrg } from '../../../../utils/store/org';
</script>
<svelte:head>
<title>Custom domain - ClassroomIO</title>
</svelte:head>
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<a
class="text-gray-500 text-md"
href={`/org/${$currentOrg.siteName}/settings`}>Back to Settings</a
>
<div class="flex items-center justify-between mb-10">
<h1 class="text-3xl font-bold">Custom domain</h1>
</div>
<Domains />
</div>
</section>
@@ -0,0 +1,17 @@
<script>
import OrgSettings from '../../../../components/Org/Settings/index.svelte';
</script>
<svelte:head>
<title>Settings - ClassroomIO</title>
</svelte:head>
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<div class="flex items-center justify-between mb-10">
<h1 class="text-3xl font-bold">Settings</h1>
</div>
<OrgSettings />
</div>
</section>
@@ -0,0 +1,22 @@
<script>
import TeamMembers from '../../../../components/Org/Settings/TeamMembers.svelte';
import { currentOrg } from '../../../../utils/store/org';
</script>
<svelte:head>
<title>Teams - ClassroomIO</title>
</svelte:head>
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<a
class="text-gray-500 text-md"
href={`/org/${$currentOrg.siteName}/settings`}>Back to Settings</a
>
<div class="flex items-center justify-between mb-10">
<h1 class="text-3xl font-bold">Team members</h1>
</div>
<TeamMembers />
</div>
</section>
+31
View File
@@ -1,4 +1,5 @@
import z from 'zod';
import { validateEmail } from './validateEmail';
const forgotValidationSchema = z.object({
email: z.string().email({
@@ -100,3 +101,33 @@ export const onboardingValidation = (fields = {}, step) => {
export const getDisableSubmit = ({ password, confirmPassword }) => {
return !!(password && confirmPassword && password !== confirmPassword);
};
export const validateEmailInString = (emailsStr) => {
let error = '';
let hasError = false;
if (!emailsStr) {
return {
hasError: true,
error: 'Enter an email',
emails: [],
};
}
const emails = emailsStr.split(',').map((_email) => {
const email = _email.trim();
if (!validateEmail(email)) {
hasError = true;
error += !error.length ? `Not valid email: ${email}` : `, ${email}`;
}
return email;
});
return {
hasError,
error,
emails,
};
};
+46 -2
View File
@@ -1,5 +1,47 @@
import { supabase } from '../../functions/supabase';
import { orgs, currentOrg, orgAudience } from '../../store/org';
import { orgs, currentOrg, orgAudience, orgTeam } from '../../store/org';
import { ROLE, ROLE_LABEL } from '../../constants/roles';
export async function getOrgTeam(orgId) {
const { data, error } = await supabase
.from('organizationmember')
.select(
`
id,
email,
verified,
role_id,
profile(
id,
fullname,
email
)
`
)
.eq('organization_id', orgId)
.order('id', { ascending: false });
const team = [];
if (data?.length) {
data.forEach((teamMember) => {
team.push({
id: teamMember.id,
email: teamMember?.profile?.email || teamMember?.email,
verified: teamMember?.verified,
profileId: teamMember?.profile?.id,
role: ROLE_LABEL[teamMember?.role_id] || '',
isAdmin: teamMember?.role_id === ROLE.ADMIN,
});
});
orgTeam.set(team);
}
return {
team,
error,
};
}
export async function getOrganizations(userId) {
const { data, error } = await supabase
@@ -11,7 +53,8 @@ export async function getOrganizations(userId) {
organization (
id,
name,
siteName
siteName,
avatar_url
)
`
)
@@ -26,6 +69,7 @@ export async function getOrganizations(userId) {
shortName:
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
siteName: orgMember?.organization?.siteName,
avatar_url: orgMember?.organization?.avatar_url,
});
});
+2
View File
@@ -6,5 +6,7 @@ export const currentOrg = writable({
name: '',
shortName: '',
siteName: '',
avatar_url: '',
});
export const orgAudience = writable([]);
export const orgTeam = writable([]);