feat: 🚀 😃 convert repo into a monorepo 🚀 😃 (#51)
* Merge main into turbo-monrepo * feat: rename apps * feat: remove supbase videos and serve all from project * feat: move supabase to root * feat: update README * docs: update link
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
|
||||
let siteName = '';
|
||||
let errors = '';
|
||||
let isLoading = false;
|
||||
|
||||
async function handleChangeDomain() {
|
||||
if (blockedSubdomain.includes(siteName || '')) {
|
||||
errors = 'Sitename already exists.';
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
|
||||
const { data: org, error } = await supabase
|
||||
.from('organization')
|
||||
.update({ siteName })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
console.log('Updating organisation', org);
|
||||
if (error) {
|
||||
console.log('Error: create organisation', error);
|
||||
errors = 'Sitename already exists.';
|
||||
} else {
|
||||
snackbar.success();
|
||||
$currentOrg.siteName = siteName;
|
||||
|
||||
goto(`/org/${$currentOrg.siteName}/settings/domains`);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function setSiteName(_siteName: string) {
|
||||
if (!siteName) {
|
||||
siteName = _siteName;
|
||||
}
|
||||
}
|
||||
|
||||
function resetErrors(_siteName: string) {
|
||||
if (errors) {
|
||||
errors = '';
|
||||
}
|
||||
}
|
||||
|
||||
$: resetErrors(siteName);
|
||||
$: setSiteName($currentOrg.siteName);
|
||||
</script>
|
||||
|
||||
<Grid class="border rounded border-gray-200 w-full mt-5">
|
||||
<Row class="py-7 border-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<p class="text-md text-gray-500 dark:text-white 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}
|
||||
/>
|
||||
<div class="flex items-center mb-6">
|
||||
<PrimaryButton
|
||||
label="Update Domain"
|
||||
className="py-4"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleChangeDomain}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
|
||||
<VisitOrgSiteButton />
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="py-7 border-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Custom domain</SectionTitle></Column>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<div class="flex items-center">
|
||||
<p class="dark:text-white mr-3">Add your own domain name</p>
|
||||
<ComingSoon />
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { Grid, Column, Row } from 'carbon-components-svelte';
|
||||
import CheckmarkFilled from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||
|
||||
let chatId: number | null;
|
||||
|
||||
async function addChatId() {
|
||||
if (!chatId || `${chatId}`.length < 5) return;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('profile')
|
||||
.update({
|
||||
telegram_chat_id: chatId
|
||||
})
|
||||
.match({ id: $profile.id });
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
snackbar.error('Integration Failed, please try again later');
|
||||
} else {
|
||||
$profile.telegram_chat_id = chatId;
|
||||
chatId = null;
|
||||
snackbar.success('Integration successful');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChatId() {
|
||||
const { error } = await supabase
|
||||
.from('profile')
|
||||
.update({ telegram_chat_id: null })
|
||||
.match({ id: $profile.id });
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Deletion Failed, please try again later');
|
||||
console.log(error);
|
||||
} else {
|
||||
$profile.telegram_chat_id = null;
|
||||
snackbar.success('Integration deleted');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Row class="flex lg:flex-row flex-col justify-center lg:justify-start py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4} class="flex items-center justify-center">
|
||||
<img src="/telegram-svg.svg" alt="" class="w-10 mr-2" />
|
||||
<SectionTitle>Telegram</SectionTitle></Column
|
||||
>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
|
||||
{#if $profile.telegram_chat_id !== null}
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div class="flex items-center">
|
||||
<CheckmarkFilled size={32} class="mr-2" style="fill: green;" />
|
||||
<SectionTitle>Integration Successful</SectionTitle>
|
||||
</div>
|
||||
<PrimaryButton className="mt-3" variant={VARIANTS.CONTAINED_DANGER} onClick={deleteChatId}
|
||||
>Disconnect</PrimaryButton
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<h3 class="font-normal text-lg">
|
||||
Connect your account to telegram to get notified of any change within the application.
|
||||
</h3>
|
||||
<h4 class="font-normal">Step 1: Authenticate your account via the ClassroomIO bot.</h4>
|
||||
<PrimaryButton
|
||||
className="mb-5"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
|
||||
><span class="mr-2">Open Bot</span> <ArrowUpRightIcon /></PrimaryButton
|
||||
>
|
||||
<h4 class="font-normal">Step 2: Enter the Chat ID given to you by the bot.</h4>
|
||||
<TextField
|
||||
bind:value={chatId}
|
||||
placeholder="360111"
|
||||
className="w-full lg:w-60 mb-5"
|
||||
type="number"
|
||||
isRequired
|
||||
/>
|
||||
<PrimaryButton onClick={addChatId}>Connect</PrimaryButton>
|
||||
</div>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -0,0 +1,503 @@
|
||||
<script lang="ts">
|
||||
import { Grid, Row, Column, RadioButtonGroup, RadioButton } from 'carbon-components-svelte';
|
||||
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Save from 'carbon-icons-svelte/lib/Save.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { Toggle } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { landingPageSettings } from './store';
|
||||
import type { OrgLandingPageJson } from './store';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
|
||||
let isSaving = false;
|
||||
let creatingNewQuestion = false;
|
||||
let widgetKey = '';
|
||||
const banner = [
|
||||
{ value: 'video', label: 'Video' },
|
||||
{ value: 'image', label: 'Image' }
|
||||
];
|
||||
const supabase = getSupabase();
|
||||
|
||||
let newQuestion = {
|
||||
title: '',
|
||||
content: ''
|
||||
};
|
||||
|
||||
function widgetControl(key: string) {
|
||||
widgetKey = key;
|
||||
$handleOpenWidget.open = true;
|
||||
}
|
||||
|
||||
function createNewFaq() {
|
||||
newQuestion = {
|
||||
title: '',
|
||||
content: ''
|
||||
};
|
||||
creatingNewQuestion = true;
|
||||
}
|
||||
|
||||
function saveNewFAQ() {
|
||||
if (newQuestion.title !== '' && newQuestion.content !== '') {
|
||||
$landingPageSettings.faq.questions = [
|
||||
...$landingPageSettings.faq.questions,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
title: newQuestion.title,
|
||||
content: newQuestion.content
|
||||
}
|
||||
];
|
||||
creatingNewQuestion = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewFAQ() {
|
||||
creatingNewQuestion = false;
|
||||
}
|
||||
|
||||
function deleteFaq(id: number) {
|
||||
let faqs = $landingPageSettings.faq.questions;
|
||||
const filteredFaq = faqs.filter((faq) => faq.id !== id);
|
||||
$landingPageSettings.faq.questions = filteredFaq;
|
||||
}
|
||||
|
||||
const checkPrefix = (inputValue: string) => {
|
||||
if (!inputValue) return;
|
||||
|
||||
if (inputValue.trim() !== '') {
|
||||
if (!inputValue.startsWith('https://')) {
|
||||
inputValue = 'https://' + inputValue;
|
||||
}
|
||||
}
|
||||
return inputValue;
|
||||
};
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
$landingPageSettings.footer.twitter = checkPrefix($landingPageSettings.footer.twitter) || '';
|
||||
$landingPageSettings.footer.linkedin = checkPrefix($landingPageSettings.footer.linkedin) || '';
|
||||
$landingPageSettings.footer.facebook = checkPrefix($landingPageSettings.footer.facebook) || '';
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ landingpage: $landingPageSettings })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
const message = error?.message || 'Please try again';
|
||||
snackbar.error(`Update failed: ${message}`);
|
||||
} else {
|
||||
$currentOrg.landingpage = $landingPageSettings;
|
||||
snackbar.success();
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
function setDefault(landingpage: OrgLandingPageJson) {
|
||||
if (landingpage && Object.keys(landingpage).length) {
|
||||
// Added new key, support backward compatibility
|
||||
if (!landingpage?.header?.banner) {
|
||||
landingpage.header.banner = $landingPageSettings.header.banner;
|
||||
}
|
||||
|
||||
$landingPageSettings = {
|
||||
...landingpage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$: setDefault($currentOrg?.landingpage as OrgLandingPageJson);
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5 relative">
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>Landing Page</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.header.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.header.title}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<TextField
|
||||
label="Title-Highlight"
|
||||
placeholder="Write your titlehighlight here"
|
||||
className="w-full mb-5"
|
||||
bind:value={$landingPageSettings.header.titleHighlight}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Subtitle"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your subtitle here"
|
||||
className="w-full mb-5"
|
||||
bind:value={$landingPageSettings.header.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<SectionTitle>Actions</SectionTitle>
|
||||
<TextField
|
||||
label="Label"
|
||||
placeholder="Write your label here"
|
||||
className="w-full mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.header.action.label}
|
||||
/>
|
||||
<div class="gap-2 mb-5">
|
||||
<TextField
|
||||
label="Link"
|
||||
placeholder="Write your link here"
|
||||
bind:value={$landingPageSettings.header.action.link}
|
||||
/>
|
||||
<Toggle bind:toggled={$landingPageSettings.header.action.redirect} size="sm">
|
||||
<span slot="labelA" style="color: gray">No redirect</span>
|
||||
<span slot="labelB" style="color: gray">Redirect</span>
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
<div />
|
||||
<RadioButtonGroup
|
||||
legendText="Banner Type"
|
||||
bind:selected={$landingPageSettings.header.banner.type}
|
||||
class="mt-10 mb-5"
|
||||
>
|
||||
{#each banner as item}
|
||||
<RadioButton value={item.value} labelText={item.label} />
|
||||
{/each}
|
||||
</RadioButtonGroup>
|
||||
{#if $landingPageSettings.header.banner.type === 'video'}
|
||||
<div class="gap-2 mt-3 mb-5">
|
||||
<TextField
|
||||
label="Link"
|
||||
placeholder="Write your video link here"
|
||||
bind:value={$landingPageSettings.header.banner.video}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label="Select Image"
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('banner')}
|
||||
/>
|
||||
{/if}
|
||||
{#if $landingPageSettings.header.banner.image && $landingPageSettings.header.banner.type === 'image'}
|
||||
<img
|
||||
alt="bannerImage"
|
||||
src={$landingPageSettings.header.banner.image}
|
||||
class="mt-2 rounded-md w-full"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Toggle bind:toggled={$landingPageSettings.header.banner.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide banner</span>
|
||||
<span slot="labelB" style="color: gray">Show banner</span>
|
||||
</Toggle>
|
||||
{#if $handleOpenWidget.open && widgetKey === 'banner'}
|
||||
<UploadWidget bind:imageURL={$landingPageSettings.header.banner.image} />
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>About</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.aboutUs.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.aboutUs.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Content"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your Content here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.aboutUs.content}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="font-bold">Upload an image</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label="Select Image"
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('about-us')}
|
||||
/>
|
||||
{#if $landingPageSettings.aboutUs.imageUrl}
|
||||
<img
|
||||
alt="About us"
|
||||
src={$landingPageSettings.aboutUs.imageUrl}
|
||||
class="mt-2 rounded-md w-full"
|
||||
/>
|
||||
{/if}
|
||||
{#if $handleOpenWidget.open && widgetKey === 'about-us'}
|
||||
<UploadWidget bind:imageURL={$landingPageSettings.aboutUs.imageUrl} />
|
||||
{/if}
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>Courses</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.courses.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.courses.title}
|
||||
/>
|
||||
<TextField
|
||||
label="Title-Highlight"
|
||||
placeholder="Write your title highlight here"
|
||||
bind:value={$landingPageSettings.courses.titleHighlight}
|
||||
/>
|
||||
<TextArea
|
||||
label="Subtitle"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your subtitle here"
|
||||
bind:value={$landingPageSettings.courses.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>Frequently Asked Questions</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.faq.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.faq.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
{#each $landingPageSettings.faq.questions as item (item.id)}
|
||||
<div class="mb-3">
|
||||
<TextField
|
||||
label="Question"
|
||||
placeholder="Write your question here"
|
||||
bind:value={item.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label="Answer"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your answer here"
|
||||
bind:value={item.content}
|
||||
className="mb-5"
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<IconButton onClick={() => deleteFaq(item.id)}>
|
||||
<TrashCan size={24} class="fill-red-700" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/each}
|
||||
{#if creatingNewQuestion}
|
||||
<TextField
|
||||
label="Question"
|
||||
placeholder="Write your question here"
|
||||
bind:value={newQuestion.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label="Answer"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your answer here"
|
||||
bind:value={newQuestion.content}
|
||||
className="mb-5"
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} />
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Cancel" onClick={cancelNewFAQ} />
|
||||
</div>
|
||||
{:else}
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Add New Faq" onClick={createNewFaq} />
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>Contact us</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.contact.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.contact.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextField
|
||||
label="Title-highlight"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your title highlight here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.contact.titleHighlight}
|
||||
/>
|
||||
<TextArea
|
||||
label="Subtitle"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your subtitle here"
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<TextField
|
||||
label="Phone number"
|
||||
placeholder="Write your phone number here"
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.phone}
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
placeholder="Write your email here"
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.email}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>Mailing List</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.mailinglist.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Write your title here"
|
||||
bind:value={$landingPageSettings.mailinglist.title}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label="Subtitle"
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your subtitle here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.mailinglist.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Button-label"
|
||||
placeholder="Write your label here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.mailinglist.buttonLabel}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>Footer</SectionTitle>
|
||||
<Toggle bind:toggled={$landingPageSettings.footer.show} size="sm">
|
||||
<span slot="labelA" style="color: gray">Hide section</span>
|
||||
<span slot="labelB" style="color: gray">Show section</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label="Facebook"
|
||||
placeholder="Write your Facebook link here"
|
||||
bind:value={$landingPageSettings.footer.facebook}
|
||||
className="mb-5"
|
||||
/>
|
||||
<!-- <TextField
|
||||
label="Instagram"
|
||||
placeholder="Write your Instagram link here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.instagram}
|
||||
/> -->
|
||||
<TextField
|
||||
label="Twitter"
|
||||
placeholder="Write your Twitter link here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.twitter}
|
||||
/>
|
||||
<TextField
|
||||
label="Linkedin"
|
||||
placeholder="Write your Linkedin link here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.linkedin}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<div class="sticky desktop float-right bottom-12 mr-2 z-[120]">
|
||||
<PrimaryButton
|
||||
label="Save Changes"
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div
|
||||
class="absolute
|
||||
mobile right-6 bottom-8 z-[120]"
|
||||
>
|
||||
<span>
|
||||
<IconButton onClick={handleSave} disabled={isSaving}>
|
||||
<Save size={40} class=" bg-blue-700 p-1 rounded-full" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media screen and (min-width: 769px) {
|
||||
.desktop {
|
||||
display: block;
|
||||
}
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
.mobile {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
|
||||
let avatar;
|
||||
let loading = false;
|
||||
const themes = {
|
||||
rose: 'theme-rose',
|
||||
green: 'theme-green',
|
||||
orange: 'theme-orange',
|
||||
violet: 'theme-violet',
|
||||
default: ''
|
||||
};
|
||||
|
||||
function handleChangeTheme(t = '') {
|
||||
return async () => {
|
||||
document.body.className = document.body.className
|
||||
.split(' ')
|
||||
.filter((c) => !c.includes('theme'))
|
||||
.join(' ')
|
||||
.concat(!!t ? ' ' : '', t);
|
||||
$currentOrg.theme = t;
|
||||
|
||||
const res = await supabase
|
||||
.from('organization')
|
||||
.update({ theme: t })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
console.log('Update theme', res);
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
updates.avatar_url = response.publicUrl;
|
||||
$currentOrg.avatar_url = response.publicUrl;
|
||||
}
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
let { error } = await supabase
|
||||
.from('organization')
|
||||
.update(updates)
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
currentOrg.update((_currentOrg) => ({
|
||||
..._currentOrg,
|
||||
...updates
|
||||
}));
|
||||
|
||||
snackbar.success('Update successful');
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
let message = error.message;
|
||||
if (message.includes('profile_username_key')) {
|
||||
message = 'username already exists';
|
||||
}
|
||||
|
||||
snackbar.error(`Update failed: ${message}`);
|
||||
loading = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function gotoSetting(pathname) {
|
||||
goto(`${$currentOrgPath}/settings${pathname}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>Organization Profile</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
|
||||
<TextField
|
||||
label="Organization Name"
|
||||
bind:value={$currentOrg.name}
|
||||
className="w-full lg: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 lg:mr-5 mt-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>Theme</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="dark:text-white lg:mt-0">Set your brand color</h4>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.default &&
|
||||
'border-[#1d4ee2]'} mr-3 flex items-center justify-center"
|
||||
on:click={handleChangeTheme(themes.default)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#1d4ee2] rounded-full m-1" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.rose &&
|
||||
'border-[#be1241]'} mr-3 flex items-center justify-center"
|
||||
on:click={handleChangeTheme(themes.rose)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#be1241] rounded-full m-1" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.green &&
|
||||
'border-[#0c891b]'} mr-3 flex items-center justify-center"
|
||||
on:click={handleChangeTheme(themes.green)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#0c891b] rounded-full m-1" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.orange &&
|
||||
'border-[#cc4902]'} mr-3 flex items-center justify-center"
|
||||
on:click={handleChangeTheme(themes.orange)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cc4902] rounded-full m-1" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.violet &&
|
||||
'border-[#cf00ce]'} mr-3 flex items-center justify-center"
|
||||
on:click={handleChangeTheme(themes.violet)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cf00ce] rounded-full m-1" />
|
||||
</button>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>Custom Domain</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="dark:text-white lg:mt-0">Customise your Domain</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
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-primary-700"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/domains')}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>Team</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="dark:text-white lg:mt-0">Set up your website</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
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-primary-700"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/teams')}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script>
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import LogoutButton from '$lib/components/Buttons/Logout/index.svelte';
|
||||
import generateUUID from '$lib/utils/functions/generateUUID';
|
||||
|
||||
let avatar = '';
|
||||
let loading = false;
|
||||
|
||||
async function handleUpdate() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const updates = {
|
||||
fullname: $profile.fullname,
|
||||
username: $profile.username,
|
||||
email: $profile.email
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
const filename = `user/${generateUUID()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
if (data) {
|
||||
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
updates.avatar_url = response.publicUrl;
|
||||
$profile.avatar_url = response.publicUrl;
|
||||
}
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profile').update(updates).match({ id: $profile.id });
|
||||
|
||||
profile.update((_profile) => ({
|
||||
..._profile,
|
||||
...updates
|
||||
}));
|
||||
snackbar.success('Update successful');
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
let message = error.message;
|
||||
if (message.includes('profile_username_key')) {
|
||||
message = 'username already exists';
|
||||
}
|
||||
snackbar.success(`Update failed: ${message}`);
|
||||
loading = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c ">
|
||||
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||
<SectionTitle>Profile Picture</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
|
||||
<UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex flex-col lg:flex-row py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>Personal Information</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
|
||||
<TextField label="Full Name" bind:value={$profile.fullname} className="w-full lg:w-60 mb-4" />
|
||||
<TextField label="Username" bind:value={$profile.username} className="w-full lg:w-60 mb-4" />
|
||||
<TextField label="Email" bind:value={$profile.email} className="w-full lg:w-60 mb-4" />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label="Update Profile"
|
||||
className="mr-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
<LogoutButton />
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -0,0 +1,192 @@
|
||||
<script>
|
||||
import { Grid, Row, Column, Select, SelectItem } from 'carbon-components-svelte';
|
||||
import { Moon } from 'svelte-loading-spinners';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { validateEmailInString } from '$lib/utils/functions/validator';
|
||||
import { ROLE_LABEL, ROLE } from '$lib/utils/constants/roles';
|
||||
import { currentOrg, orgTeam } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import {
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
|
||||
let emailsStr = '';
|
||||
let errorMessage = '';
|
||||
let role = ROLE.TUTOR;
|
||||
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
|
||||
})
|
||||
.select();
|
||||
console.log('data', data);
|
||||
|
||||
if (error) {
|
||||
apiError = error;
|
||||
|
||||
console.error('onSendInvite:', error);
|
||||
snackbar.error(`Failed to send invite, please try again`);
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
triggerSendEmail(NOTIFICATION_NAME.INVITE_TEACHER, {
|
||||
email: newMember.email,
|
||||
org: {
|
||||
id: $currentOrg.id,
|
||||
name: $currentOrg.name,
|
||||
siteName: $currentOrg.siteName
|
||||
}
|
||||
});
|
||||
|
||||
const isLast = index === emails.length - 1;
|
||||
if (isLast) {
|
||||
snackbar.success('Success');
|
||||
|
||||
emailsStr = '';
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onRemove(id) {
|
||||
console.log('onRemove called');
|
||||
isRemoving = true;
|
||||
const { data, error } = await supabase.from('organizationmember').delete().match({ id });
|
||||
|
||||
if (error) {
|
||||
console.error('onRemove:', error);
|
||||
snackbar.error('Failed to remove user, please try again');
|
||||
} else {
|
||||
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-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<p class="text-md text-gray-500 dark:text-white 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-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Members</SectionTitle></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 dark:text-white 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,98 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
|
||||
import Profile from './Profile.svelte';
|
||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||
// import Account from './Account.svelte';
|
||||
import OrgSettings from './OrgSettings.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import Integrations from './Integrations.svelte';
|
||||
|
||||
let selected = 0;
|
||||
let query = new URLSearchParams($page.url.search);
|
||||
let tabKey = query.get('tab') || '';
|
||||
|
||||
let tabs = [];
|
||||
|
||||
function getSelectedByTab(tabKey = '') {
|
||||
const tab = tabs.find((t) => t.tabKey === tabKey);
|
||||
|
||||
return tab ? tab.key : 0;
|
||||
}
|
||||
function changeRouteOnTabChange(key = 0) {
|
||||
const tab = tabs.find((t) => t.key === key);
|
||||
if (tab) {
|
||||
return goto(tab.href);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
selected = getSelectedByTab(tabKey);
|
||||
});
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
changeRouteOnTabChange(selected);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
tabs = [
|
||||
{
|
||||
key: 0,
|
||||
label: 'Profile',
|
||||
tabKey: '',
|
||||
href: $page.url.pathname,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: 'Organization',
|
||||
tabKey: 'org',
|
||||
href: `${$page.url.pathname}?tab=org`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
label: 'LandingPage',
|
||||
tabKey: 'landingpage',
|
||||
href: `${$page.url.pathname}?tab=landingpage`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
label: 'Integrations',
|
||||
tabKey: 'integrations',
|
||||
href: `${$page.url.pathname}?tab=integrations`,
|
||||
disabled: false
|
||||
}
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tabs autoWidth bind:selected>
|
||||
{#each tabs as tab}
|
||||
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} />
|
||||
{/each}
|
||||
<!-- <Tab label="Account" /> -->
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent class="w-full p-0">
|
||||
<Profile />
|
||||
</TabContent>
|
||||
<!-- <TabContent>
|
||||
<Account />
|
||||
</TabContent> -->
|
||||
<TabContent class="w-full p-0">
|
||||
<OrgSettings />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
<LandingpageSettings />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
<Integrations />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,146 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export interface OrgLandingPageJson {
|
||||
header: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
action: {
|
||||
label: string;
|
||||
link: string;
|
||||
redirect: boolean;
|
||||
};
|
||||
banner: {
|
||||
video: string;
|
||||
image: string;
|
||||
type: string;
|
||||
show: boolean;
|
||||
};
|
||||
show: boolean;
|
||||
};
|
||||
aboutUs: {
|
||||
title: string;
|
||||
content: string;
|
||||
imageUrl: string;
|
||||
show: boolean;
|
||||
};
|
||||
courses: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
show: boolean;
|
||||
};
|
||||
faq: {
|
||||
title: string;
|
||||
questions: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
}>;
|
||||
show: boolean;
|
||||
};
|
||||
contact: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
show: boolean;
|
||||
};
|
||||
mailinglist: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonLabel: string;
|
||||
show: boolean;
|
||||
};
|
||||
footer: {
|
||||
facebook: string;
|
||||
instagram: string;
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
show: boolean;
|
||||
};
|
||||
}
|
||||
export let landingPageSettings: Writable<OrgLandingPageJson> = writable({
|
||||
header: {
|
||||
title: 'Web Development Courses for',
|
||||
titleHighlight: 'Everyone',
|
||||
subtitle:
|
||||
'Practical project-based courses that are easy to understand and straight to the point',
|
||||
action: {
|
||||
label: 'Start learning Today',
|
||||
link: '#contact',
|
||||
redirect: false
|
||||
},
|
||||
banner: {
|
||||
video: 'https://www.youtube.com/watch?v=qqAYW7uxErI',
|
||||
image: '',
|
||||
type: 'video',
|
||||
show: true
|
||||
},
|
||||
show: true
|
||||
},
|
||||
aboutUs: {
|
||||
title: 'Our Story',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.',
|
||||
imageUrl:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/landingpage/riverside.jpeg',
|
||||
show: true
|
||||
},
|
||||
courses: {
|
||||
title: 'Explore our',
|
||||
titleHighlight: 'Courses',
|
||||
subtitle: 'Find courses you will love from best teachers all over the world🌎.',
|
||||
show: true
|
||||
},
|
||||
faq: {
|
||||
title: 'Frequently Asked Questions.',
|
||||
questions: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
}
|
||||
],
|
||||
show: true
|
||||
},
|
||||
contact: {
|
||||
title: 'Have you got any',
|
||||
titleHighlight: 'Question?',
|
||||
subtitle: 'Reach out to us with any question you might have.',
|
||||
address: 'Ashwoodfield House Farm, Kidderminster Rd, Stourbridge, Kingswinford DY6 0AA',
|
||||
phone: '+4407339904995',
|
||||
email: 'help@digdippa.com',
|
||||
show: true
|
||||
},
|
||||
mailinglist: {
|
||||
title: 'Join our mailing list',
|
||||
subtitle:
|
||||
'We are constantly releasing new courses and sharing them with our email list. Subscribe to get notified once we release a new course',
|
||||
buttonLabel: 'Subscribe',
|
||||
show: true
|
||||
},
|
||||
footer: {
|
||||
facebook: '#',
|
||||
instagram: '#',
|
||||
twitter: '#',
|
||||
linkedin: '#',
|
||||
show: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user