feat: migrate course UI from supabase (#615)

* chore: fix storybook build

* chore: implement theming

* community list not showing properly

* refactor: migrate Chip component to @cio/ui/custom/chip

- Updated imports for the Chip component across multiple files to use the new location in the @cio/ui package.
- Replaced the previous Chip implementation with a new version that utilizes a Badge for better styling and functionality.
- Adjusted related components to ensure consistent usage of the updated Chip component.

* fix: align course sidebar design to match organization design (#611)

* fix: align course sidebar design to match organization design

* feat: add hideSearch prop to AppHeader and update sidebar layout

---------

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

* refactor: replace PrimaryButton with Button component across multiple files

- Updated various components to use the new Button component from @cio/ui/base/button instead of the deprecated PrimaryButton.
- Ensured consistent button usage throughout the application for improved styling and functionality.
- Removed the PrimaryButton component and its associated constants from the codebase.

* refactor: standardize button and input usage across components

- Replaced instances of deprecated components such as TextField, TextArea, and IconButton with their updated counterparts (InputField, TextareaField, and updated IconButton).
- Ensured consistent event handling by changing `onClick` to `onclick` for better alignment with Svelte's event binding conventions.
- Removed unused components and streamlined imports to enhance code maintainability and readability.

* chore: themeing, course card, use $features

* refactor: centralize UI components and update modal implementations

- Refactored various components to centralize UI elements, improving discoverability and maintainability.
- Replaced deprecated modal components with the new Dialog component for consistent modal behavior across the application.
- Removed unused components and streamlined imports to enhance code clarity and reduce complexity.

* feat: add new documentation and refactor org and lms UI components

- Introduced new documentation files for Auth Flow, Breadcrumbs Refactoring, Page and Route Creation, and Route Page Setup to enhance developer onboarding and reference.
- Refactored various UI components, including the Course Sidebar and Lesson Modal, to improve layout and functionality.
- Removed deprecated components and streamlined imports for better maintainability.
- Updated translations across multiple languages to ensure consistency in user experience.

* refactor: update UI components and improve styling consistency

- Refactored various components to enhance styling consistency, including updates to button and text color classes.
- Removed deprecated components and streamlined imports for better maintainability.
- Replaced instances of PageBody with a more flexible Page component structure across multiple routes.
- Updated translations and UI elements to align with the new design system.
- Deleted unused components to reduce code complexity and improve clarity.

* update output.css

---------

Co-authored-by: Abdulmuizz Hamzat <[email protected]>
This commit is contained in:
Rotimi Best
2025-12-22 02:36:48 +01:00
committed by GitHub
co-authored by Abdulmuizz Hamzat
parent e6a3cf9332
commit 9fb6c7cbaa
401 changed files with 10489 additions and 11679 deletions
@@ -0,0 +1,70 @@
<script lang="ts">
import { preventDefault } from '$lib/utils/functions/svelte';
import * as Dialog from '@cio/ui/base/dialog';
import { Input } from '@cio/ui/base/input';
import { DomainInput } from '@cio/ui/custom/domain-input';
import { Button } from '@cio/ui/base/button';
import * as Field from '@cio/ui/base/field';
import { generateSitename } from '$lib/utils/functions/org';
import { newOrgModal } from '../new-org-modal-store';
import { t } from '$lib/utils/functions/translations';
import { orgApi } from '$features/org/api/org.svelte';
let orgName = $state('');
let siteName = $derived(generateSitename(orgName));
function resetForm() {
orgName = '';
siteName = '';
orgApi.errors = {};
}
async function createNewOrg() {
await orgApi.create({ name: orgName, siteName });
if (orgApi.success) {
$newOrgModal.open = false;
resetForm();
}
}
</script>
<Dialog.Root
bind:open={$newOrgModal.open}
onOpenChange={(isOpen) => {
if (!isOpen) $newOrgModal.open = false;
}}
>
<Dialog.Content class="w-96">
<Dialog.Header>
<Dialog.Title>{$t('add_org.create_org')}</Dialog.Title>
</Dialog.Header>
<form onsubmit={preventDefault(createNewOrg)} class="px-2">
<Field.Group>
<Field.Field>
<Field.Label>{$t('add_org.name')}</Field.Label>
<Input bind:value={orgName} placeholder="e.g Pepsi Co" autofocus />
{#if orgApi.errors.name || orgApi.errors.orgName}
<Field.Error>{orgApi.errors.name || orgApi.errors.orgName}</Field.Error>
{/if}
</Field.Field>
<Field.Field>
<Field.Label>{$t('add_org.org_sitename')}</Field.Label>
<DomainInput bind:value={siteName} placeholder="myschool" prefix="https://" suffix=".classroomio.com" />
{#if orgApi.errors.siteName || orgApi.errors.general}
<Field.Error>{orgApi.errors.siteName || orgApi.errors.general}</Field.Error>
{/if}
</Field.Field>
<Field.Field orientation="horizontal">
<Button type="submit" loading={orgApi.isLoading} class="ml-auto">
{$t('add_org.create')}
</Button>
</Field.Field>
</Field.Group>
</form>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,30 @@
<script lang="ts">
import { Skeleton } from '@cio/ui/base/skeleton';
import { t } from '$lib/utils/functions/translations';
let { src, className, alt }: { src: string | undefined; className?: string; alt?: string } = $props();
function loadImage(src: string | undefined): Promise<void> {
if (!src) return Promise.reject();
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = reject;
img.src = src;
});
}
</script>
{#await loadImage(src)}
<div class="flex max-h-[215px] w-full items-center justify-center rounded bg-gray-100 dark:bg-neutral-800">
<Skeleton class={className} />
</div>
{:then}
<img {src} {alt} class={className} />
{:catch}
<div class="flex h-[200px] w-full items-center justify-center rounded bg-gray-100 dark:bg-neutral-800">
<p class="text-sm text-red-600">{$t('courses.course_card.error_message')}</p>
</div>
{/await}
@@ -0,0 +1,576 @@
<script lang="ts">
import { untrack } from 'svelte';
import { goto } from '$app/navigation';
import { fade } from 'svelte/transition';
import MailIcon from '@lucide/svelte/icons/mail';
import PhoneIcon from '@lucide/svelte/icons/phone';
import RocketIcon from '@lucide/svelte/icons/rocket';
import MapPinIcon from '@lucide/svelte/icons/map-pin';
import TwitterIcon from '@lucide/svelte/icons/twitter';
import FacebookIcon from '@lucide/svelte/icons/facebook';
import LinkedinIcon from '@lucide/svelte/icons/linkedin';
import { preventDefault } from '$lib/utils/functions/svelte';
import Navigation from '$lib/components/Navigation/index.svelte';
import { PoweredBy } from '$features/ui';
import { CourseCardList, CourseCardLoader } from '$features/course/components';
import { landingPageSettings } from '$features/org/components/settings/landing-page-store';
import { CoursesEmptyIcon } from '$features/ui/icons';
import PageLoader from './page-loader.svelte';
import * as Accordion from '@cio/ui/base/accordion';
import * as Button from '@cio/ui/base/button';
import * as Field from '@cio/ui/base/field';
import { Input } from '@cio/ui/base/input';
import { Textarea } from '@cio/ui/base/textarea';
import { Empty } from '@cio/ui/custom/empty';
import { t } from '$lib/utils/functions/translations';
import type { AccountOrg } from '$features/app/types';
import { orgApi } from '$features/org/api/org.svelte';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import { courseMetaDeta, courses } from '$features/course/utils/store';
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
interface Props {
orgSiteName?: string;
org: AccountOrg;
}
let { orgSiteName = '', org }: Props = $props();
let email: string | undefined = $state('');
let isAdding = $state(false);
let success = $state(false);
let successContactSaved = $state(false);
let viewAll = $state(false);
let isContactSubmiting = $state(false);
let player = $state();
let contact = $state({
name: '',
email: '',
phone: '',
message: ''
});
let contactError: Record<string, string> = $state({});
async function handleSubmit() {
if (!email || !validateEmail(email) || !org) return;
isAdding = true;
// TODO: Implement email list submission
success = true;
setTimeout(() => {
isAdding = false;
success = false;
email = undefined;
}, 5000);
}
async function handleContactSubmit() {
isContactSubmiting = true;
contactError = orgLandingpageValidation(contact);
if (Object.keys(contactError).length) {
isContactSubmiting = false;
return;
}
// Save to db
// TODO: Implement contact submission
isContactSubmiting = false;
successContactSaved = true;
contact = {
name: '',
email: '',
phone: '',
message: ''
};
}
function isYouTubeLink(link: string) {
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim());
}
function loadData(siteName) {
if (!siteName) return;
untrack(async () => {
try {
$courseMetaDeta.isLoading = true;
await orgApi.getCourseBySiteName(siteName);
if (orgApi.error) {
console.error('Error fetching courses', orgApi.error);
courses.set([]);
} else {
courses.set(orgApi.orgSiteCourses);
}
$courseMetaDeta.isLoading = false;
} catch (error) {
console.log('error', error);
$courseMetaDeta.isLoading = false;
}
});
}
function initPlyr(_player: any, _video: string | undefined) {
if (!player) return;
// @ts-ignore
const plyr = new Plyr('#player', { width: '400px', height: '300px', borderRadius: '10px' });
// @ts-ignore
window.player = plyr;
}
// Use default data in store if user hasn't added their own content to landing page
function setDefault(landingpage) {
if (landingpage && Object.keys(landingpage).length) {
if (!landingpage?.header?.banner) {
landingpage.header.banner = $landingPageSettings.header.banner;
}
untrack(() => {
$landingPageSettings = {
...landingpage
};
});
}
}
function getBgImage(settings) {
const defaultImg = '/images/org-landingpage-banner.jpeg';
const { show, image } = settings.header.background || {
show: true,
image: defaultImg
};
if (!show) {
return undefined;
}
return `url('${image || defaultImg}')`;
}
$effect(() => {
loadData(orgSiteName);
});
$effect(() => {
initPlyr(player, $landingPageSettings.header?.banner?.video);
});
$effect(() => {
setDefault(org?.landingpage);
});
</script>
<svelte:head>
<title>
{!org?.name ? '' : `${org.name}'s `}{$t('course.navItem.landing_page.landing_page')}
</title>
</svelte:head>
<PoweredBy />
{#if !org?.landingpage}
<PageLoader />
{:else}
<main>
<Navigation
logo={org.avatarUrl}
orgName={org.name}
disableSignup={true}
isOrgSite={true}
customLinks={$landingPageSettings.customLinks}
/>
<!-- Header Section -->
{#if $landingPageSettings.header.show}
<header
id="header"
class={`relative mb-10 h-screen w-full md:h-[90vh] ${
$landingPageSettings.header.background?.show ? 'bg-cover bg-center' : 'border-b border-gray-300'
}`}
style="background-image: {getBgImage($landingPageSettings)}"
>
<div class="absolute top-0 z-10 h-screen w-full bg-white opacity-80 md:h-[90vh]"></div>
{#if $landingPageSettings.header.banner.show}
<div class="flex items-center justify-center py-2 md:h-full">
<div
class="relative z-20 flex w-full flex-col-reverse items-center justify-center md:w-11/12 md:flex-row md:justify-between lg:w-5/6"
>
<!-- Org Description -->
<div class="mr-3 flex w-11/12 flex-col items-center py-10 md:w-2/5 md:items-start">
<p class="ui:text-primary text-2xl font-semibold capitalize">
{org.name}
</p>
<h1 class="my-4 text-center text-4xl md:text-start md:text-5xl lg:text-6xl">
{$landingPageSettings.header.title} <br /><span class="ui:text-primary"
>{$landingPageSettings.header.titleHighlight}</span
>
</h1>
<p class="text-md mb-3 text-center text-xl md:mb-5 md:text-start">
{$landingPageSettings.header.subtitle}
</p>
<Button.Root
class="mt-2 w-fit px-10 md:mt-5"
onclick={() => {
$landingPageSettings.header.action.redirect && goto($landingPageSettings.header.action.link);
}}
>
{$landingPageSettings.header.action.label}
</Button.Root>
</div>
<div class="flex w-5/6 md:w-1/2 md:max-w-[650px]">
{#if isYouTubeLink($landingPageSettings.header?.banner?.video) && $landingPageSettings.header.banner.type === 'video'}
<div bind:this={player} id="player" style="width:100%; border-radius:12px">
<iframe
title={$t('course.navItem.landing_page.header_video')}
src={$landingPageSettings.header?.banner?.video}
allowfullscreen
allowtransparency
allow="autoplay"
></iframe>
</div>
{:else}
<!-- <video class="w-full rounded-xl" controls loop autoplay>
<source src={$landingPageSettings.header?.banner?.video} type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" />
Captions are optional
<track kind="captions" />
</video> -->
<img class="rounded-md" src={$landingPageSettings.header?.banner?.image} alt="landing page banner" />
{/if}
</div>
</div>
</div>
{:else}
<div class="relative z-20 flex h-full w-full items-center justify-center md:flex-row">
<div class="mx-auto flex w-11/12 max-w-[600px] flex-col items-center py-10">
<p class=" ui:text-primary text-2xl font-semibold capitalize">{org.name}</p>
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
{$landingPageSettings.header.title} <br /><span class="ui:text-primary"
>{$landingPageSettings.header.titleHighlight}</span
>
</h1>
<p class="text-md mb-6 text-center text-xl">
{$landingPageSettings.header.subtitle}
</p>
<Button.Root
class="mt-5 w-fit px-10"
onclick={() => {
$landingPageSettings.header.action.redirect && goto($landingPageSettings.header.action.link);
}}
>
{$landingPageSettings.header.action.label}
</Button.Root>
</div>
</div>
{/if}
</header>
{/if}
<!-- Our Story section -->
{#if $landingPageSettings.aboutUs.show}
<section id="aboutus" class="m-h-[400px] mx-auto my-10 w-full max-w-6xl">
<div class="mx-4 flex flex-col items-center justify-evenly lg:flex-row">
<div class="mb-5 mr-5 max-w-[600px] lg:mb-0 lg:w-2/5">
<h2 class="text-4xl md:text-5xl lg:text-6xl">{$landingPageSettings.aboutUs.title}</h2>
<p class="mb-2">
{$landingPageSettings.aboutUs.content}
</p>
</div>
<div class="image">
<img src={$landingPageSettings.aboutUs.imageUrl} alt="Our Story" class=" max-h-[450px] rounded-2xl" />
</div>
</div>
</section>
{/if}
<!-- Courses Section -->
{#if $landingPageSettings.courses.show}
<section id="courses" transition:fade class="mx-auto my-10 w-full max-w-6xl">
<div class="w-full">
<div class="mx-auto w-11/12 max-w-[500px] py-10">
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
{$landingPageSettings.courses.title}
<span class="text-primary">{$landingPageSettings.courses.titleHighlight}</span>
</h1>
<p class="text-md text-center">
{$landingPageSettings.courses.subtitle}
</p>
</div>
</div>
{#if $courseMetaDeta.isLoading}
<div class="cards-container mx-2 my-4">
<CourseCardLoader />
<CourseCardLoader />
<CourseCardLoader />
</div>
{:else if $courses.length > 0}
<CourseCardList courses={$courses.slice(0, viewAll ? $courses.length : 3)} isOnLandingPage={true} />
{:else}
<Empty
icon={CoursesEmptyIcon}
title={$t('course.navItem.landing_page.no_course_published')}
description={$t('course.navItem.landing_page.coming_your_way')}
variant="page"
/>
{/if}
{#if $courses.length > 3}
<div class="mt-3 flex w-full justify-center">
<Button.Root variant="outline" onclick={() => (viewAll = !viewAll)} class="w-fit px-10 py-5">
{viewAll ? $t('course.navItem.landing_page.view_less') : $t('course.navItem.landing_page.view_all')}
</Button.Root>
</div>
{/if}
</section>
{/if}
<!-- FAQ Section -->
{#if $landingPageSettings.faq.show}
<section id="faq" transition:fade class="mx-auto my-10 w-full max-w-[700px]">
<div class="py-10">
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
{$landingPageSettings.faq.title}
</h1>
</div>
<div class="mx-2">
<Accordion.Root type="single" class="w-full">
{#each $landingPageSettings.faq.questions as faq, index}
<Accordion.Item value="item-{index}">
<Accordion.Trigger>
{faq.title}
</Accordion.Trigger>
<Accordion.Content>
{faq.content}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
</div>
</section>
{/if}
<!-- Contact Section-->
{#if $landingPageSettings.contact.show}
<section id="contact" transition:fade class="ui:bg-primary-foreground my-10 w-full">
<div class="mx-auto w-full max-w-6xl">
<div class="mx-auto w-11/12 max-w-[500px] py-10">
<h1 class="my-4 text-center text-4xl md:text-5xl lg:text-6xl">
{$landingPageSettings.contact.title}
<span class="ui:text-primary">{$landingPageSettings.contact.titleHighlight}</span>
</h1>
<p class="text-md text-center">{$landingPageSettings.contact.subtitle}</p>
</div>
<div class="mx-auto max-w-[700px] pb-10">
<!-- Contact Details -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div
class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg border py-2 text-center transition-all duration-500"
>
<MapPinIcon size={16} class="filled" />
<p class="mt-3 max-w-[200px] text-xs md:text-sm">
{$landingPageSettings.contact.address}
</p>
</div>
<div
class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg border py-2 text-center transition-all duration-500"
>
<PhoneIcon size={16} />
<p class="mt-3 text-xs md:text-sm">{$landingPageSettings.contact.phone}</p>
</div>
<div
class="mx-2 flex cursor-pointer flex-col items-center justify-center break-all rounded-lg border py-2 text-center transition-all duration-500"
>
<MailIcon size={16} />
<p class="mt-3 text-xs md:text-sm">{$landingPageSettings.contact.email}</p>
</div>
</div>
<!-- Contact Form -->
<div class="mt-8 rounded-lg bg-white p-7">
{#if successContactSaved}
<div class="flex w-full items-center justify-center">
{$t('course.navItem.landing_page.thank_you')}
</div>
{:else}
<form onsubmit={preventDefault(handleContactSubmit)}>
<div class="flex w-full flex-col justify-between md:flex-row">
<div class="mr-5 w-full md:w-2/4">
<Field.Field class="mb-5">
<Field.Label class="font-bold">
{$t('course.navItem.landing_page.name')}
</Field.Label>
<Input bind:value={contact.name} placeholder="Elon Musk" aria-invalid={!!contactError.name} />
{#if contactError.name}
<Field.Error>{contactError.name}</Field.Error>
{/if}
</Field.Field>
<Field.Field class="mb-5">
<Field.Label class="text-xs font-normal">
{$t('course.navItem.landing_page.email')}
</Field.Label>
<Input
bind:value={contact.email}
type="email"
placeholder="[email protected]"
aria-invalid={!!contactError.email}
/>
{#if contactError.email}
<Field.Error>{contactError.email}</Field.Error>
{/if}
</Field.Field>
<Field.Field class="mb-5">
<Field.Label class="text-xs font-normal">
{$t('course.navItem.landing_page.phone')}
</Field.Label>
<Input
bind:value={contact.phone}
placeholder="+1194802480"
aria-invalid={!!contactError.phone}
/>
{#if contactError.phone}
<Field.Error>{contactError.phone}</Field.Error>
{/if}
</Field.Field>
</div>
<div class="w-full md:w-2/4">
<Field.Field>
<Field.Label>
{$t('course.navItem.landing_page.message')}
</Field.Label>
<Textarea
bind:value={contact.message}
rows={9}
placeholder={$t('course.navItem.landing_page.your_message')}
aria-invalid={!!contactError.message}
/>
{#if contactError.message}
<Field.Error>{contactError.message}</Field.Error>
{/if}
</Field.Field>
</div>
</div>
<Button.Root class="mx-auto mt-5 w-full md:mt-0" type="submit" loading={isContactSubmiting}>
<span class="text-md mr-2">{$t('course.navItem.landing_page.submit')}</span>
<RocketIcon size={16} class="custom" />
</Button.Root>
</form>
{/if}
</div>
</div>
</div>
</section>
{/if}
<!-- Waitlist Section -->
{#if $landingPageSettings.mailinglist.show}
<section id="waitlist" transition:fade class="mx-auto my-10 w-[95%] max-w-6xl">
<div class="ui:bg-primary gap-4 rounded-lg px-4 py-14 md:px-10 lg:flex-row lg:items-center">
<div class="mx-auto flex max-w-[700px] flex-col lg:flex-row lg:items-center lg:justify-between">
<div class="w-3/5">
<h1 class="mb-5 mt-0 text-4xl text-white">
{$landingPageSettings.mailinglist.title}
</h1>
<p class="text-md text-white">
{$landingPageSettings.mailinglist.subtitle}
</p>
</div>
<form onsubmit={preventDefault(handleSubmit)}>
<div class="flex max-w-60 flex-col items-center">
{#if success}
<p class="text-white">{$t('course.navItem.landing_page.successful_sub')}</p>
{:else}
<Input
bind:value={email}
type="email"
placeholder={$t('course.navItem.landing_page.enter')}
required={true}
disabled={isAdding}
/>
<Button.Root class="my-1 mt-2 w-full" variant="outline" type="submit" loading={isAdding}>
{$landingPageSettings.mailinglist.buttonLabel}
</Button.Root>
{/if}
</div>
</form>
</div>
</div>
</section>
{/if}
<!-- Footer Section -->
{#if $landingPageSettings.footer.show}
<footer
id="footer"
class="my-10 flex w-full flex-col items-center justify-center border-b-0 border-l-0 border-r-0 border-t border-gray-300 px-5 py-10 md:py-3"
>
<ul class="flex w-11/12 flex-col items-center sm:flex-row">
<div class="logo">
<a href="/" title={`Go to ${org.name} Home`} id="logo" data-hveid="8" class="flex items-center">
<img
src={org.avatarUrl || '/logo-192.png'}
alt={`${org.name} logo`}
class="mx-auto inline-block max-h-10 w-10 rounded"
data-atf="1"
/>
<h3 class="ml-3 text-xl text-black">{org.name}</h3>
</a>
</div>
<span class="flex-grow"></span>
<div class="mt-5 flex gap-2 sm:mt-0">
{#if $landingPageSettings.footer.facebook}
<li>
<a
href={$landingPageSettings.footer.facebook}
target="_blank"
title={$t('settings.landing_page.footer.facebook')}
id="logo-fb"
data-hveid="8"
>
<FacebookIcon size={16} />
</a>
</li>
{/if}
{#if $landingPageSettings.footer.twitter}
<li>
<a
href={$landingPageSettings.footer.twitter}
target="_blank"
title={$t('settings.landing_page.footer.twitter')}
id="logo-tw"
data-hveid="8"
>
<TwitterIcon size={16} />
</a>
</li>
{/if}
{#if $landingPageSettings.footer.linkedin}
<li>
<a
href={$landingPageSettings.footer.linkedin}
target="_blank"
title={$t('settings.landing_page.footer.linkedin')}
id="logo-ln"
data-hveid="8"
>
<LinkedinIcon size={16} />
</a>
</li>
{/if}
</div>
</ul>
</footer>
{/if}
</main>
{/if}
@@ -0,0 +1,28 @@
<script>
import { Input } from '@cio/ui/base/input';
import { t } from '$lib/utils/functions/translations';
import Search from '@lucide/svelte/icons/search';
let searchValue = $state('');
</script>
<section class="p- flex min-h-[337px] w-full items-center justify-center bg-[#040f2d]">
<div class="flex w-full flex-col-reverse items-center justify-center md:flex-row">
<!-- Course Description -->
<div class="course-descr w-11/12 py-10">
<h1 class="my-4 text-center text-5xl text-white dark:text-white">
{$t('course.navItem.landing_page.explore')}
</h1>
<p class="text-md mb-6 text-center text-white dark:text-white">
{$t('course.navItem.landing_page.find')}🌎.
</p>
<div class="relative mx-auto max-w-xl">
<Input type="text" placeholder={$t('course.navItem.landing_page.find_course')} bind:value={searchValue} />
<Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
</div>
</div>
</div>
</section>
@@ -0,0 +1,27 @@
<script>
import { Skeleton } from '@cio/ui/base/skeleton';
import { CourseCardLoader } from '$features/course/components';
</script>
<section class="h-screen w-screen p-5">
<!-- navbar -->
<div class="flex w-full items-center justify-between overflow-hidden rounded-md">
<Skeleton class="mr-40 h-4 w-[80%] rounded-[10px]" />
<div class="flex w-[20%] items-center justify-between">
<Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
<Skeleton class="mr-1 h-4 w-[2%] rounded-[10px]" />
<Skeleton class="h-4 w-[2%] rounded-[10px]" />
</div>
</div>
<!-- hero -->
<div class="my-8 flex w-full flex-row flex-wrap items-start justify-center gap-5 overflow-hidden rounded-md">
<Skeleton class="h-80 w-full rounded-md" />
</div>
<!-- cards -->
<div class="my-4 flex flex-wrap items-center justify-center gap-5 overflow-hidden rounded-md">
<CourseCardLoader />
<CourseCardLoader />
<CourseCardLoader />
</div>
</section>
@@ -0,0 +1,6 @@
import { writable } from 'svelte/store';
export const newOrgModal = writable({
open: false
});
@@ -0,0 +1,71 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
import * as Dialog from '@cio/ui/base/dialog';
import { deleteModal, quizesStore } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
let { onDelete = () => {} } = $props();
function closeModal() {
deleteModal.update((m) => ({
...m,
open: false,
id: null,
isQuestion: false
}));
}
async function deleteQuiz() {
const { data, error } = await supabase.from('quiz').delete().match({ id: $deleteModal.id });
console.log('data', data);
console.log('error', error);
if (error) {
return;
}
$quizesStore = $quizesStore.filter((q) => q.id !== $deleteModal.id);
closeModal();
}
</script>
<Dialog.Root
bind:open={$deleteModal.open}
onOpenChange={(isOpen) => {
if (!isOpen) closeModal();
}}
>
<Dialog.Content class="w-2/5 max-w-[388px]">
<Dialog.Header>
<Dialog.Title>
{$deleteModal.isQuestion ? $t('components.quiz.question_delete') : $t('components.quiz.quiz_delete')}
</Dialog.Title>
</Dialog.Header>
<div class="my-10 flex w-full flex-col items-center">
<h1 class="text-center text-lg dark:text-white">
{#if $deleteModal.isQuestion}
{$t('components.quiz.delete_question')}
{:else}
{$t('components.quiz.delete_quiz')}
{/if}
{$t('components.quiz.lose_content')}
</h1>
<div class="mt-5">
<Button
onclick={() => {
if ($deleteModal.isQuestion) {
onDelete();
$deleteModal.open = false;
} else {
deleteQuiz();
}
}}
>
{$t('components.quiz.delete')}
</Button>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,21 @@
<script lang="ts">
import { Skeleton } from '@cio/ui/base/skeleton';
</script>
<div
class="root relative mb-4 mr-4 flex w-full rounded-md border border-gray-100 p-3 transition ease-in-out dark:border-neutral-600 dark:bg-neutral-800"
>
<Skeleton class="h-full w-[20%]" />
<div class="mx-5 flex w-full flex-col justify-evenly">
<!-- Title -->
<Skeleton class="h-4 w-2/4" />
<!-- Description -->
<Skeleton class="h-16 w-2/4" />
</div>
</div>
<style>
.root {
height: 180px;
}
</style>
@@ -0,0 +1,141 @@
<script>
import { preventDefault } from '$lib/utils/functions/svelte';
import { goto } from '$app/navigation';
import { Button } from '@cio/ui/base/button';
import * as Dialog from '@cio/ui/base/dialog';
import { InputField } from '@cio/ui/custom/input-field';
import { snackbar } from '$features/ui/snackbar/store';
import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, createQuizModal, currentOrgPath, quizesStore } from '$lib/utils/store/org';
import { createQuizValidation } from '$lib/utils/functions/validator';
import { t } from '$lib/utils/functions/translations';
let open = $state(false);
let errors = $state({
title: ''
});
let isLoading = $state(false);
function handleClose() {
$createQuizModal.id = null;
$createQuizModal.title = '';
$createQuizModal.open = false;
$createQuizModal.openEdit = false;
}
function errorNotification() {
snackbar.error();
}
async function createQuiz() {
errors = createQuizValidation($createQuizModal);
console.log('createQuiz|updateQuiz errors', errors);
if (Object.keys(errors).length) {
return;
}
isLoading = true;
if ($createQuizModal.openEdit) {
const { error } = await supabase
.from('quiz')
.update({ title: $createQuizModal.title, updated_at: new Date() })
.match({ id: $createQuizModal.id });
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = $quizesStore.map((s) => {
if (s.id === $createQuizModal.id) {
s.title = $createQuizModal.title;
}
return s;
});
handleClose();
}
} else {
const { data, error } = await supabase
.from('quiz')
.insert({
title: $createQuizModal.title,
organization_id: $currentOrg.id,
questions: [
{
id: 1,
label: '',
type: 'multichoice',
options: [
{
id: 'circle',
label: '',
isCorrect: false
},
{
id: 'triangle',
label: '',
isCorrect: false
}
]
}
]
})
.select();
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = [...$quizesStore, data[0]];
handleClose();
goto(`${$currentOrgPath}/quiz/${data[0].id}`);
}
}
}
$effect(() => {
open = $createQuizModal.openEdit || $createQuizModal.open;
});
</script>
<svelte:head>
<title>{$t('components.quiz.title')} - ClassroomIO</title>
</svelte:head>
<Dialog.Root
bind:open
onOpenChange={(isOpen) => {
if (!isOpen) handleClose();
}}
>
<Dialog.Content class="w-4/5 md:w-2/5">
<Dialog.Header>
<Dialog.Title>
{$createQuizModal.openEdit ? $t('components.quiz.update') : $t('components.quiz.create')} {$t(
'components.quiz.a_quiz'
)}
</Dialog.Title>
</Dialog.Header>
<form onsubmit={preventDefault(createQuiz)}>
<InputField
label={$t('components.quiz.quiz_title')}
bind:value={$createQuizModal.title}
autofocus={true}
placeholder={$t('components.quiz.placehoolder')}
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<div class="mt-5 flex items-center justify-end">
<Button type="submit" disabled={isLoading} loading={isLoading}>
{$createQuizModal.openEdit ? $t('components.quiz.save') : $t('components.quiz.continue')}
</Button>
</div>
</form>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,59 @@
<script>
import { onMount } from 'svelte';
import { Spinner } from '@cio/ui/base/spinner';
import PlayContainer from './container.svelte';
import PlayHeader from './header/index.svelte';
import { quizStore, playQuizStore } from '$lib/utils/store/org';
import { STEPS } from '$lib/utils/constants/quiz';
import { genQuizPin } from '$lib/utils/functions/org';
import { Button } from '@cio/ui/base/button';
let isGettingPin = $state(true);
function getPin() {
setTimeout(() => {
$quizStore.pin = genQuizPin();
isGettingPin = false;
}, 3000);
}
function goToPlayersStep() {
$playQuizStore.step = STEPS.WAIT_FOR_PLAYERS;
}
onMount(() => {
getPin();
});
</script>
<PlayContainer>
{#snippet header()}
<div>
<PlayHeader startCount={true} showCountDown={true} />
</div>
{/snippet}
{#snippet body()}
<div class="w-full rounded-md bg-white px-5 py-7 dark:bg-neutral-800">
<div class="mb-3">
<p>1. Visit</p>
<h3>play.classroomio.com</h3>
</div>
<div class="">
<p>2. Enter Pin</p>
{#if isGettingPin}
<Spinner class="size-10! text-blue-700!" />
{:else}
<h3>{$quizStore.pin}</h3>
{/if}
</div>
</div>
{/snippet}
{#snippet footer()}
<div class="flex items-center justify-center">
<p class="mr-3">Let's go</p>
<Button variant="outline" onclick={goToPlayersStep}>View Players</Button>
</div>
{/snippet}
</PlayContainer>
@@ -0,0 +1,53 @@
<script lang="ts">
interface Props {
header?: import('svelte').Snippet;
body?: import('svelte').Snippet;
footer?: import('svelte').Snippet;
}
let { header, body, footer }: Props = $props();
</script>
<div class="root m-auto flex h-full flex-col justify-between">
{#if header}
<!-- HEADER -->
<div class="header">
{@render header?.()}
</div>
{/if}
{#if body}
<!-- BODY -->
<div class="body mb-3">
{@render body?.()}
</div>
{/if}
{#if footer}
<!-- FOOTER -->
<div class="footer m-auto bg-white px-2 py-6 dark:bg-neutral-800">
{@render footer?.()}
</div>
{/if}
</div>
<style>
.root {
max-width: 680px;
width: 80%;
height: 95vh;
}
.header {
height: 15%;
}
.body {
min-height: 60%;
height: fit-content;
}
.footer {
max-width: 400px;
}
</style>
@@ -0,0 +1,11 @@
<script lang="ts">
import CircleContent from './circle-content.svelte';
interface Props {
answers?: number;
}
let { answers = 0 }: Props = $props();
</script>
<CircleContent value={answers} label="Answer(s)" />
@@ -0,0 +1,49 @@
<script lang="ts">
import { slide } from 'svelte/transition';
interface Props {
value?: boolean;
label?: string;
className?: string;
}
let { value = false, label = '', className = '' }: Props = $props();
function fadeSlide(node, options) {
const slideTrans = slide(node, options);
return {
duration: options.duration,
css: (t) => `
${slideTrans.css(t)}
opacity: ${t};
`
};
}
</script>
<div class="circle flex flex-col items-center justify-center {className}">
{#key value}
<h3 class="ui:text-primary" transition:fadeSlide={{ duration: 500 }}>
{value}
</h3>
{/key}
<p class="ui:text-primary text-md">{label}</p>
</div>
<style>
.circle {
width: 100px;
height: 100px;
line-height: 100px;
border-radius: 50%;
font-size: 50px;
text-align: center;
background: #fff;
}
.circle h3 {
font-size: 40px;
margin: 0;
padding: 0;
line-height: 1;
}
</style>
@@ -0,0 +1,22 @@
<script lang="ts">
import { quizStore } from '$lib/utils/store/org';
import Answer from './answer.svelte';
import Timer from './timer.svelte';
interface Props {
startCount?: boolean;
showCountDown?: boolean;
}
let { startCount = false, showCountDown = false }: Props = $props();
</script>
<div class="flex w-full items-center justify-between">
<h3 class="text-white">{$quizStore.title}</h3>
{#if showCountDown}
<div class="flex">
<Timer {startCount} timelimit={$quizStore.timelimit} />
<Answer />
</div>
{/if}
</div>
@@ -0,0 +1,45 @@
<script lang="ts">
import { onMount } from 'svelte';
import CircleContent from './circle-content.svelte';
interface Props {
startCount?: boolean;
timelimit?: string;
}
let { startCount = false, timelimit = '' }: Props = $props();
let countDownNo = $state(0);
let started = false;
function triggerCountDown(start) {
if (!start || started) return;
const getNum = (k) => parseInt(timelimit.replace(k, ''));
if (timelimit.includes('s')) {
countDownNo = getNum('s');
} else if (timelimit.includes('m')) {
countDownNo = getNum('m') * 60;
}
window.refreshIntervalId = setInterval(() => {
if (--countDownNo <= 0) {
clearInterval(window.refreshIntervalId);
}
}, 1000);
started = true;
}
onMount(() => {
if (startCount) {
triggerCountDown(startCount);
}
});
$effect(() => {
triggerCountDown(startCount);
});
</script>
<CircleContent value={countDownNo} label="Second(s)" className="mr-3" />
@@ -0,0 +1,47 @@
<script>
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { Spinner } from '@cio/ui/base/spinner';
import { themeImages, STEPS } from '$lib/utils/constants/quiz';
import { quizStore, playQuizStore } from '$lib/utils/store/org';
import ConnectToPlay from './connect-to-play.svelte';
import Players from './players.svelte';
import Question from './question.svelte';
import Scoreboard from './scoreboard.svelte';
import Podium from './podium.svelte';
let bgImg = $state();
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 h-screen w-screen bg-white"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="h-full w-full p-5"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
{#if $playQuizStore.step === STEPS.CONNECT_TO_PLAY}
<ConnectToPlay />
{:else if $playQuizStore.step === STEPS.WAIT_FOR_PLAYERS}
<Players />
{:else if $playQuizStore.step === STEPS.SHOW_NEXT_QUESTION}
<Question />
{:else if $playQuizStore.step === STEPS.SCOREBOARD}
<Scoreboard />
{:else if $playQuizStore.step === STEPS.PODIUM}
<Podium />
{:else}
<div class="m-auto w-full">
<Spinner class="size-8! text-blue-700!" />
</div>
{/if}
</div>
</div>
{/if}
@@ -0,0 +1 @@
players
@@ -0,0 +1 @@
Podium
@@ -0,0 +1,102 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import QuizQuestion from '../quiz-question.svelte';
import PlayContainer from './container.svelte';
import PlayHeader from './header/index.svelte';
import { quizStore } from '$lib/utils/store/org';
import { themeImages } from '$lib/utils/constants/quiz';
import { Button } from '@cio/ui/base/button';
import { IconButton } from '@cio/ui/custom/icon-button';
let { exitPreview = () => {} } = $props();
let bgImg = $state();
let totalQ = $quizStore.questions.length;
let curQId = $state(0);
let currentQuestion = $state($quizStore.questions[0] || {});
function handlePrev() {
const prevQId = curQId - 1;
console.log('prevQId', prevQId);
const prev = $quizStore.questions[prevQId];
console.log('prev', prev);
if (prev) {
currentQuestion = prev;
curQId = prevQId;
}
}
function handleNext() {
const nextQId = curQId + 1;
console.log('nextqid', nextQId);
const next = $quizStore.questions[nextQId];
console.log('next', next);
if (next) {
currentQuestion = next;
curQId = nextQId;
}
}
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 h-screen w-screen bg-white"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="h-full w-full p-5"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
<PlayContainer>
{#snippet header()}
<div>
<PlayHeader startCount={true} showCountDown={true} />
</div>
{/snippet}
{#snippet body()}
<div class="quiz-body">
{#if currentQuestion}
<QuizQuestion {currentQuestion} isPreview={true} />
{:else}
<h2>No question added</h2>
{/if}
</div>
{/snippet}
{#snippet footer()}
<div class="flex flex-col justify-center">
{#if currentQuestion}
<Button variant="ghost" onclick={exitPreview} class="w-fit">Exit Preview</Button>
<div class="flex items-center justify-center">
<IconButton onclick={handlePrev}>
<ChevronLeftIcon size={16} />
</IconButton>
<p class="mx-3">{curQId + 1} / {totalQ}</p>
<IconButton onclick={handleNext}>
<ChevronRightIcon size={16} />
</IconButton>
</div>
{/if}
</div>
{/snippet}
</PlayContainer>
</div>
</div>
{/if}
<style>
.quiz-body {
width: 500px;
margin: auto;
}
</style>
@@ -0,0 +1 @@
Question
@@ -0,0 +1 @@
Scoreboard
@@ -0,0 +1,95 @@
<script lang="ts">
import pluralize from 'pluralize';
// import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import { currentOrgPath } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import { themeImages } from '$lib/utils/constants/quiz';
import { calDateDiff } from '$lib/utils/functions/date';
// import { onRename, onDelete } from '$lib/utils/services/org/quiz';
import { Button } from '@cio/ui/base/button';
import { Image } from '$features/ui';
let { quiz, totalQuestions } = $props();
/*function getOptions() {
return [
// {
// label: 'Edit',
// onClick: onEdit(quiz),
// },
// {
// label: 'Share',
// onClick: onShare(quiz),
// },
// {
// label: 'Report',
// onClick: onReport(quiz),
// },
{
label: $t('components.quiz.rename'),
onClick() {
onRename(quiz);
}
},
// {
// label: 'Make a copy',
// onClick() {
// onMakeCopy(quiz);
// },
// },
{
label: $t('components.quiz.delete'),
onClick() {
onDelete(quiz);
}
}
];
}*/
function startQuiz() {}
</script>
{#if quiz}
<div
class="root relative mb-5 flex w-full flex-col rounded-lg border bg-gray-100 p-3 transition ease-in-out hover:shadow-2xl lg:flex-row dark:bg-black"
>
<Image
src={themeImages[quiz.theme]?.card || themeImages.standard.card}
alt="quiz-card"
className="max-w-[300px] min-w-[200px]"
/>
<div class="flex w-full flex-col justify-between p-2 md:p-5">
<div class="flex justify-between">
<h4 class="title text-md capitalize dark:text-white">
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
</h4>
<!-- <Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
<div class="rounded-full bg-gray-200 p-1 dark:bg-gray-600">
<EllipsisIcon size={16} />
</div>
</Dropdown> -->
</div>
<div class="flex flex-col justify-between md:flex-row">
<div>
<p class="mb-2">{pluralize('question', totalQuestions, true)}</p>
<p class="mb-2 md:mb-0">{$t('components.quiz.updated')} {calDateDiff(quiz.updated_at)}</p>
</div>
<Button variant="outline" onclick={startQuiz}>
{$t('components.quiz.start')}
</Button>
</div>
</div>
</div>
{/if}
<style>
.root {
min-height: 180px;
}
</style>
@@ -0,0 +1,66 @@
<script lang="ts">
import { CircleCheckIcon } from '$features/ui/icons';
import { optionImage } from '$lib/utils/constants/quiz';
import { InputField } from '@cio/ui/custom/input-field';
import { IconButton } from '@cio/ui/custom/icon-button';
import { t } from '$lib/utils/functions/translations';
interface Props {
currentQuestion?: any;
currentError?: any;
optionHasError?: any;
isPreview?: boolean;
}
let {
currentQuestion = $bindable({
label: ''
}),
currentError = {},
optionHasError = () => false,
isPreview = false
}: Props = $props();
</script>
{#if isPreview}
<h1 class="mb-5 text-white">{currentQuestion.label}</h1>
{:else}
<InputField
placeholder={$t('components.quiz.start_typing')}
bind:value={currentQuestion.label}
className="w-full my-4"
errorMessage={currentError.isLabelEmpty && $t('components.quiz.required_field')}
isRequired={true}
bgColor="bg-white"
/>
{/if}
<!-- Options -->
<div class="flex w-full flex-wrap justify-between">
{#each currentQuestion.options || [] as option}
<div class="relative mb-5 w-2/5 rounded bg-white p-5 dark:bg-neutral-800">
<img src={optionImage[option.id]} alt={option.id} />
{#if isPreview}
<p class="mt-5">{option.label}</p>
{:else}
<InputField
bind:value={option.label}
placeholder={$t('components.quiz.type_answer')}
bgColor="bg-white"
className="mt-3"
isDisabled={currentQuestion.type === 'boolean'}
errorMessage={optionHasError(option.id, currentError.options) && $t('components.quiz.label')}
/>
<div class="absolute right-2 top-2 flex justify-end">
<IconButton
onclick={() => (option.isCorrect = !option.isCorrect)}
class={option.isCorrect ? 'success' : ''}
>
<CircleCheckIcon size={16} filled={option.isCorrect} />
</IconButton>
</div>
{/if}
</div>
{/each}
</div>
@@ -0,0 +1,54 @@
<script>
import QuizLoader from './loader.svelte';
import QuizCard from './quiz-card.svelte';
import { Empty } from '@cio/ui/custom/empty';
import HelpCircleIcon from '@lucide/svelte/icons/help-circle';
import { snackbar } from '$features/ui/snackbar/store';
import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, quizesStore } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
let isLoading = $state(false);
async function fetchQuizes(id) {
if (!id) return;
isLoading = true;
const { data, error } = await supabase
.from('quiz')
.select(`*`)
.eq('organization_id', id)
.order('updated_at', { ascending: false });
if (error) {
snackbar.error();
return;
}
console.log('data', data);
quizesStore.set(data);
isLoading = false;
}
$effect(() => {
fetchQuizes($currentOrg.id);
});
</script>
<div class="m-auto my-4 flex flex-wrap items-center justify-center lg:justify-start">
{#if isLoading}
<QuizLoader />
<QuizLoader />
<QuizLoader />
{:else}
{#each $quizesStore as quiz}
<QuizCard {quiz} totalQuestions={quiz.questions?.length || 0} />
{:else}
<Empty
title={$t('components.quiz.no_quizz')}
description={$t('components.quiz.interactive')}
icon={HelpCircleIcon}
variant="page"
/>
{/each}
{/if}
</div>
@@ -0,0 +1,45 @@
<script lang="ts">
import { Label } from '@cio/ui/base/label';
import * as Select from '@cio/ui/base/select';
import type { TLocale } from '@cio/db/types';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { t } from '$lib/utils/functions/translations';
interface Props {
className?: string;
value?: TLocale;
hasLangChanged?: boolean;
change?: () => void;
}
let { className = '', value = $bindable('en'), hasLangChanged = $bindable(false), change }: Props = $props();
function handleSelect(selectedValue: string) {
value = selectedValue as TLocale;
hasLangChanged = true;
if (change) {
change();
}
}
const triggerContent = $derived(
LANGUAGES.find((lang) => lang.id === value)?.text ?? $t('settings.account.select_language')
);
</script>
<div class={className}>
<Label>{$t('settings.account.language')}</Label>
<Select.Root type="single" bind:value onValueChange={handleSelect}>
<Select.Trigger>
{triggerContent}
</Select.Trigger>
<Select.Content>
{#each LANGUAGES as lang}
<Select.Item value={lang.id} label={lang.text}>
{lang.text}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
@@ -0,0 +1,117 @@
import type { OrgLandingPageJson } from '$lib/utils/types/org';
import type { Writable } from 'svelte/store';
import { writable } from 'svelte/store';
export const 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
},
background: {
image: '',
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://pgrest.classroomio.com/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: '[email protected]',
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
},
customLinks: {
show: true,
links: [
{
id: 1,
label: 'About Us',
url: '#aboutus',
openInNewTab: false
},
{
id: 2,
label: 'Courses',
url: '#courses',
openInNewTab: false
},
{
id: 3,
label: 'FAQ',
url: '#faq',
openInNewTab: false
},
{
id: 4,
label: 'Contact Us',
url: '#contact',
openInNewTab: false
}
]
},
footer: {
facebook: '#',
instagram: '#',
twitter: '#',
linkedin: '#',
show: true
}
});
@@ -0,0 +1,39 @@
<script lang="ts">
import { Label } from '@cio/ui/base/label';
import * as Select from '@cio/ui/base/select';
import type { TLocale } from '@cio/db/types';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { t, initialized } from '$lib/utils/functions/translations';
interface Props {
className?: string;
value?: TLocale;
hasLangChanged?: boolean;
change?: () => void;
}
let { className = '', value = $bindable('en'), hasLangChanged = $bindable(false), change }: Props = $props();
function handleSelect(selectedValue: string) {
value = selectedValue as TLocale;
hasLangChanged = true;
change?.();
}
</script>
{#if $initialized}
<div class={className}>
<Label class="mb-2 block">{$t('content.toggle_label')}</Label>
<Select.Root type="single" bind:value onValueChange={handleSelect}>
<Select.Trigger class="w-full">
<p>{value ? LANGUAGES.find((lang) => lang.id === value)?.text : 'Pick a Language'}</p>
</Select.Trigger>
<Select.Content>
{#each LANGUAGES as language}
<Select.Item value={language.id}>{language.text}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}