feat: custom org brand color (#474)
* feat: add package * feat: add a new color picker * feat: integrate with supabase * feat: add translation * run translation script * feat: hover state * quick fix * fix: requested PR fixes * update lock * ux improvements * remove log * cleanup and improve code --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
1ee02cdf3a
commit
947181a3c5
@@ -205,9 +205,9 @@
|
||||
|
||||
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5">
|
||||
<Row class="py-7 border-bottom-c">
|
||||
<Column sm={2} md={2} lg={4} class="text-lg"
|
||||
><SectionTitle>{$t('components.settings.domains.add')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={2} md={2} lg={4} class="text-lg">
|
||||
<SectionTitle>{$t('components.settings.domains.add')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={2} md={6} lg={8}>
|
||||
<p class="text-md text-gray-500 dark:text-white mb-5">
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.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 { isFreePlan } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
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';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { updateOrgNameValidation } from '$lib/utils/functions/validator';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { setTheme, setCustomTheme, injectCustomTheme } from '$lib/utils/functions/theme';
|
||||
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let avatar;
|
||||
|
||||
@@ -24,8 +27,9 @@
|
||||
let errors: Error = {
|
||||
orgName: ''
|
||||
};
|
||||
|
||||
let loading = false;
|
||||
let hex = '';
|
||||
|
||||
const themes = {
|
||||
rose: 'theme-rose',
|
||||
green: 'theme-green',
|
||||
@@ -43,6 +47,8 @@
|
||||
.concat(!!t ? ' ' : '', t);
|
||||
$currentOrg.theme = t;
|
||||
|
||||
hex = '';
|
||||
|
||||
setTheme(t);
|
||||
|
||||
const res = await supabase
|
||||
@@ -54,6 +60,24 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function handleCustomTheme() {
|
||||
if (!hex) return;
|
||||
|
||||
injectCustomTheme(hex);
|
||||
setCustomTheme('theme-custom');
|
||||
|
||||
$currentOrg.theme = hex;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ theme: hex })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Failed to update theme: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
errors = updateOrgNameValidation($currentOrg.name) as Error;
|
||||
|
||||
@@ -65,7 +89,7 @@
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const updates = {
|
||||
const updates: Record<string, string> = {
|
||||
name: $currentOrg.name
|
||||
};
|
||||
|
||||
@@ -100,7 +124,7 @@
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
let message = error.message;
|
||||
let message = error as string;
|
||||
if (message.includes('profile_username_key')) {
|
||||
message = $t('snackbar.lms.error.username_exists');
|
||||
}
|
||||
@@ -112,17 +136,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
function gotoSetting(pathname) {
|
||||
function gotoSettings(pathname) {
|
||||
goto(`${$currentOrgPath}/settings${pathname}`);
|
||||
}
|
||||
|
||||
function setHex(theme: string) {
|
||||
if (hex || theme.includes('theme-')) return;
|
||||
hex = theme;
|
||||
}
|
||||
|
||||
$: setHex($currentOrg.theme);
|
||||
$: isCustomTheme = hex && !hex.includes('theme-');
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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>{$t('settings.organization.organization_profile.heading')}</SectionTitle
|
||||
></Column
|
||||
>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>
|
||||
{$t('settings.organization.organization_profile.heading')}
|
||||
</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={$t('settings.organization.organization_profile.organization_name')}
|
||||
@@ -145,7 +178,7 @@
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c relative">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.organization.organization_profile.theme.heading')}</SectionTitle
|
||||
></Column
|
||||
@@ -155,55 +188,84 @@
|
||||
{$t('settings.organization.organization_profile.theme.sub_heading')}
|
||||
</h4>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-center gap-5">
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.default &&
|
||||
'border-[#1d4ee2]'} mr-3 flex items-center justify-center"
|
||||
'border-[#1d4ee2]'} flex items-center justify-center h-fit"
|
||||
on:click={handleChangeTheme(themes.default)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#1d4ee2] rounded-full m-1" />
|
||||
<div class="w-6 h-6 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"
|
||||
'border-[#be1241]'} flex items-center justify-center h-fit"
|
||||
on:click={handleChangeTheme(themes.rose)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#be1241] rounded-full m-1" />
|
||||
<div class="w-6 h-6 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"
|
||||
'border-[#0c891b]'} flex items-center justify-center h-fit"
|
||||
on:click={handleChangeTheme(themes.green)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#0c891b] rounded-full m-1" />
|
||||
<div class="w-6 h-6 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"
|
||||
'border-[#cc4902]'} flex items-center justify-center h-fit"
|
||||
on:click={handleChangeTheme(themes.orange)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cc4902] rounded-full m-1" />
|
||||
<div class="w-6 h-6 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"
|
||||
'border-[#cf00ce]'} flex items-center justify-center h-fit"
|
||||
on:click={handleChangeTheme(themes.violet)}
|
||||
>
|
||||
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cf00ce] rounded-full m-1" />
|
||||
<div class="w-6 h-6 md:w-6 md:h-6 bg-[#cf00ce] rounded-full m-1" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="w-fit h-auto border-2 {isCustomTheme &&
|
||||
'border-primary-700'} rounded-full relative group"
|
||||
>
|
||||
<!-- plus icon positioned over the color picker -->
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-200"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-{isCustomTheme ? 'white' : 'black'} z-10 opacity-100"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<ColorPicker
|
||||
position="responsive"
|
||||
label=""
|
||||
bind:hex
|
||||
on:input={handleCustomTheme}
|
||||
nullable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle
|
||||
>{$t('settings.organization.organization_profile.customize_lms.heading')}</SectionTitle
|
||||
></Column
|
||||
>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>
|
||||
{$t('settings.organization.organization_profile.customize_lms.heading')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="dark:text-white lg:mt-0">
|
||||
{$t('settings.organization.organization_profile.customize_lms.sub_heading')}
|
||||
@@ -214,7 +276,7 @@
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/customize-lms')}
|
||||
onClick={() => gotoSettings('/customize-lms')}
|
||||
>
|
||||
{$t('settings.organization.organization_profile.customize_lms.button')}
|
||||
</PrimaryButton>
|
||||
@@ -236,7 +298,7 @@
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/domains')}
|
||||
onClick={() => gotoSettings('/domains')}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
@@ -260,7 +322,7 @@
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/teams')}
|
||||
onClick={() => gotoSettings('/teams')}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
|
||||
Reference in New Issue
Block a user