CLASSROOMIO-T-5: Educators Settings CRUD
Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com> Merge-request: CLASSROOMIO-MR-7 Merged-by: Digdippa Team <digdippa@gmail.com>
This commit is contained in:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
01a1f0f907
commit
5f14b36ef8
@@ -2,6 +2,7 @@
|
|||||||
export let name = '';
|
export let name = '';
|
||||||
export let src = '';
|
export let src = '';
|
||||||
export let className = '';
|
export let className = '';
|
||||||
|
export let shape = 'rounded-full';
|
||||||
export let width = 'w-10';
|
export let width = 'w-10';
|
||||||
export let height = 'h-10';
|
export let height = 'h-10';
|
||||||
</script>
|
</script>
|
||||||
@@ -9,5 +10,5 @@
|
|||||||
<img
|
<img
|
||||||
alt={name}
|
alt={name}
|
||||||
{src}
|
{src}
|
||||||
class="{className} {width} {height} rounded-full bg-light-blue-100"
|
class="{className} {width} {height} {shape} bg-light-blue-100"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
export let className = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
<div
|
<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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Box>
|
<Box>
|
||||||
<CoursesEmptyIcon />
|
<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">
|
<p class="w-1/3 text-center">
|
||||||
Share your knowledge with the world by creating engaging courses for
|
Share your knowledge with the world by creating engaging courses for
|
||||||
your students.
|
your students.
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import NotificationIcon from 'carbon-icons-svelte/lib/Notification20';
|
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 = '';
|
export let navClass = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -20,22 +17,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<NotificationIcon class="text-white mr-2" />
|
<NotificationIcon class="text-white mr-2" />
|
||||||
</li>
|
</li>
|
||||||
<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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { goto } from '@sapper/app';
|
import { goto } from '@sapper/app';
|
||||||
import Avatar from '../Avatar/index.svelte';
|
|
||||||
import { user, profile } from '../../utils/store/user';
|
import { user, profile } from '../../utils/store/user';
|
||||||
import PrimaryButton from '../PrimaryButton/index.svelte';
|
import PrimaryButton from '../PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '../PrimaryButton/constants';
|
import { VARIANTS } from '../PrimaryButton/constants';
|
||||||
@@ -76,20 +75,7 @@
|
|||||||
<span class="flex-grow" />
|
<span class="flex-grow" />
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
{#if $user.isLoggedIn}
|
{#if !disableLogin}
|
||||||
<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}
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Login"
|
label="Login"
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
Pagination,
|
Pagination,
|
||||||
DataTableSkeleton,
|
DataTableSkeleton,
|
||||||
} from 'carbon-components-svelte';
|
} from 'carbon-components-svelte';
|
||||||
import { orgAudience } from '../../utils/store/org';
|
import { orgAudience } from '../../../utils/store/org';
|
||||||
|
|
||||||
export let isLoading = false;
|
export let isLoading = false;
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Box from '../Box/index.svelte';
|
import Box from '../../Box/index.svelte';
|
||||||
import AudienceList from './AudienceList.svelte';
|
import AudienceList from './AudienceList.svelte';
|
||||||
import AudienceEmptyIcon from '../Icons/AudienceEmptyIcon.svelte';
|
import AudienceEmptyIcon from '../../Icons/AudienceEmptyIcon.svelte';
|
||||||
import { getOrgAudience } from '../../utils/services/org';
|
import { getOrgAudience } from '../../../utils/services/org';
|
||||||
import { orgAudience, currentOrg } from '../../utils/store/org';
|
import { orgAudience, currentOrg } from '../../../utils/store/org';
|
||||||
|
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
onMount(async () => {
|
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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { stores } from '@sapper/app';
|
import { stores } from '@sapper/app';
|
||||||
import UserAvatar from 'carbon-icons-svelte/lib/UserAvatar20';
|
|
||||||
import Help from 'carbon-icons-svelte/lib/Help20';
|
import Help from 'carbon-icons-svelte/lib/Help20';
|
||||||
|
|
||||||
import TextChip from '../../components/Chip/Text.svelte';
|
import TextChip from '../../components/Chip/Text.svelte';
|
||||||
@@ -9,7 +8,9 @@
|
|||||||
import CourseIcon from '../../components/Icons/CourseIcon.svelte';
|
import CourseIcon from '../../components/Icons/CourseIcon.svelte';
|
||||||
import SiteSettingsIcon from '../../components/Icons/SiteSettingsIcon.svelte';
|
import SiteSettingsIcon from '../../components/Icons/SiteSettingsIcon.svelte';
|
||||||
import AudienceIcon from '../../components/Icons/AudienceIcon.svelte';
|
import AudienceIcon from '../../components/Icons/AudienceIcon.svelte';
|
||||||
|
import Avatar from '../../components/Avatar/index.svelte';
|
||||||
import { currentOrg } from '../../utils/store/org';
|
import { currentOrg } from '../../utils/store/org';
|
||||||
|
import { profile } from '../../utils/store/user';
|
||||||
|
|
||||||
let orgSiteName = '';
|
let orgSiteName = '';
|
||||||
let activeClass = 'bg-gray-200';
|
let activeClass = 'bg-gray-200';
|
||||||
@@ -32,10 +33,20 @@
|
|||||||
<aside class="root bg-gray-100 h-full">
|
<aside class="root bg-gray-100 h-full">
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="border-b border-gray-200 pt-5 px-4">
|
<div class="border-b border-gray-200 pt-5 px-4">
|
||||||
<TextChip
|
{#if $currentOrg.avatar_url}
|
||||||
value={$currentOrg.shortName}
|
<Avatar
|
||||||
className="bg-blue-200 font-bold"
|
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 />
|
<OrgSelector />
|
||||||
|
|
||||||
@@ -101,7 +112,12 @@
|
|||||||
`/org/${orgSiteName}/settings`
|
`/org/${orgSiteName}/settings`
|
||||||
) && activeClass}"
|
) && activeClass}"
|
||||||
>
|
>
|
||||||
<UserAvatar />
|
<Avatar
|
||||||
|
src={$profile.avatar_url}
|
||||||
|
name={$profile.username}
|
||||||
|
width="w-7"
|
||||||
|
height="h-7"
|
||||||
|
/>
|
||||||
<p class="ml-2">Settings</p>
|
<p class="ml-2">Settings</p>
|
||||||
</li>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import TextChip from '../Chip/Text.svelte';
|
import TextChip from '../Chip/Text.svelte';
|
||||||
|
import ComingSoon from '../ComingSoon/index.svelte';
|
||||||
|
|
||||||
export let size;
|
export let size;
|
||||||
export let hasDivider = false;
|
export let hasDivider = false;
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<div class="coming-soon">
|
<div class="coming-soon">
|
||||||
<TextChip value="Coming soon" className="text-xs" />
|
<ComingSoon />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export const VARIANTS = {
|
|||||||
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
|
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
|
||||||
OUTLINED: 'OUTLINED',
|
OUTLINED: 'OUTLINED',
|
||||||
NONE: 'NONE',
|
NONE: 'NONE',
|
||||||
|
TEXT: 'TEXT',
|
||||||
|
TEXT_DANGER: 'TEXT_DANGER',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VARIANTS_CLASS = {
|
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_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
|
||||||
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
|
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
|
||||||
[VARIANTS.OUTLINED]:
|
[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]:
|
[VARIANTS.NONE]:
|
||||||
'border-none hover:border-black-300 hover:bg-gray-200 text-black',
|
'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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,19 @@
|
|||||||
import { snackbarStore, snackbarStoreInitialState } from './store';
|
import { snackbarStore, snackbarStoreInitialState } from './store';
|
||||||
|
|
||||||
let severityColor;
|
let severityColor;
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
if (typeof $snackbarStore.handleClose === 'number') {
|
if (typeof $snackbarStore.handleClose === 'number') {
|
||||||
$snackbarStore.handleClose();
|
$snackbarStore.handleClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbarStore.set(snackbarStoreInitialState);
|
snackbarStore.update((_s) => ({
|
||||||
|
..._s,
|
||||||
|
...snackbarStoreInitialState,
|
||||||
|
}));
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When open is true trigger autoHideDuration
|
// When open is true trigger autoHideDuration
|
||||||
@@ -26,8 +32,9 @@
|
|||||||
|
|
||||||
const { autoHideDuration } = $snackbarStore;
|
const { autoHideDuration } = $snackbarStore;
|
||||||
|
|
||||||
if (typeof autoHideDuration === 'number') {
|
if (typeof autoHideDuration === 'number' && !timeoutId) {
|
||||||
setTimeout(handleClose, autoHideDuration);
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(handleClose, autoHideDuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export let avatar;
|
import Camera from 'carbon-icons-svelte/lib/Camera20';
|
||||||
export let src;
|
|
||||||
export let widthHeight;
|
export let avatar = null;
|
||||||
|
export let src = null;
|
||||||
|
export let widthHeight = null;
|
||||||
|
export let shape = 'rounded-full';
|
||||||
|
|
||||||
let fileinput;
|
let fileinput;
|
||||||
|
|
||||||
@@ -16,43 +19,33 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="flex justify-center">
|
<section class="width-fit p-3 flex flex-col items-center">
|
||||||
<div
|
<div
|
||||||
class="avatar-container {widthHeight ||
|
class="avatar-container {widthHeight ||
|
||||||
'setwidthheight'} pointer border-2 border-gray-200 relative hover:bg-gray-400 hover:bg-opacity-75 cursor-pointer rounded-full"
|
'setwidthheight'} pointer border-2 border-gray-200 relative {shape}"
|
||||||
on:click={() => {
|
|
||||||
fileinput.click();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{#if src}
|
{#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}
|
{: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}
|
{:else}
|
||||||
<img
|
<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"
|
src="https://cdn4.iconfinder.com/data/icons/small-n-flat/24/user-alt-512.png"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/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>
|
</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
|
<input
|
||||||
style="display:none"
|
style="display:none"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -63,6 +56,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.width-fit {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-container.setwidthheight {
|
.avatar-container.setwidthheight {
|
||||||
height: 128px;
|
height: 128px;
|
||||||
width: 128px;
|
width: 128px;
|
||||||
@@ -71,9 +68,4 @@
|
|||||||
.upload-icon {
|
.upload-icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container:hover .upload-icon {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import PrimaryButton from '../../../components/PrimaryButton/index.svelte';
|
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;
|
let isLoading = false;
|
||||||
function exportAudience() {
|
function exportAudience() {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
import { validateEmail } from './validateEmail';
|
||||||
|
|
||||||
const forgotValidationSchema = z.object({
|
const forgotValidationSchema = z.object({
|
||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
@@ -100,3 +101,33 @@ export const onboardingValidation = (fields = {}, step) => {
|
|||||||
export const getDisableSubmit = ({ password, confirmPassword }) => {
|
export const getDisableSubmit = ({ password, confirmPassword }) => {
|
||||||
return !!(password && confirmPassword && 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,47 @@
|
|||||||
import { supabase } from '../../functions/supabase';
|
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) {
|
export async function getOrganizations(userId) {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -11,7 +53,8 @@ export async function getOrganizations(userId) {
|
|||||||
organization (
|
organization (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
siteName
|
siteName,
|
||||||
|
avatar_url
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -26,6 +69,7 @@ export async function getOrganizations(userId) {
|
|||||||
shortName:
|
shortName:
|
||||||
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
|
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
|
||||||
siteName: orgMember?.organization?.siteName,
|
siteName: orgMember?.organization?.siteName,
|
||||||
|
avatar_url: orgMember?.organization?.avatar_url,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ export const currentOrg = writable({
|
|||||||
name: '',
|
name: '',
|
||||||
shortName: '',
|
shortName: '',
|
||||||
siteName: '',
|
siteName: '',
|
||||||
|
avatar_url: '',
|
||||||
});
|
});
|
||||||
export const orgAudience = writable([]);
|
export const orgAudience = writable([]);
|
||||||
|
export const orgTeam = writable([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user