migrate web to svelte 5

This commit is contained in:
Rotimi Best
2025-12-25 05:37:21 +00:00
parent 50d6ee3111
commit 334c67f22a
63 changed files with 621 additions and 479 deletions
@@ -2,8 +2,14 @@
import { Badge } from '@cio/ui/base/badge';
import { formatDate } from '$lib/utils/format-date';
export let post;
export let isRecommended = false;
/**
* @typedef {Object} Props
* @property {any} post
* @property {boolean} [isRecommended]
*/
/** @type {Props} */
let { post, isRecommended = false } = $props();
</script>
{#if !isRecommended}
@@ -1,13 +1,29 @@
<script>
import { flexible, leftFist, rightFist, robotArm, thumbsUp } from '$lib/emojis';
export let id = '';
export let rightToLeft = false;
export let tagline = '';
export let title = '';
export let description = '';
export let video = '';
export let taglineIcon = '';
/**
* @typedef {Object} Props
* @property {string} [id]
* @property {boolean} [rightToLeft]
* @property {string} [tagline]
* @property {string} [title]
* @property {string} [description]
* @property {string} [video]
* @property {string} [taglineIcon]
* @property {import('svelte').Snippet} [more]
*/
/** @type {Props} */
let {
id = '',
rightToLeft = false,
tagline = '',
title = '',
description = '',
video = '',
taglineIcon = '',
more
} = $props();
</script>
<section {id} class="w-full py-32 border-b border-x-0 border-t-0 border-gray-200">
@@ -32,15 +48,15 @@
{/if}
<p class="font-medium text-base">{tagline}</p>
</div>
<h2 class="font-bold text-4xl mb-3 lg:mb-6">{title}</h2>
<h2 class="text-3xl mb-3 lg:mb-6">{title}</h2>
<p class="leading-8 text-lg text-gray-500">
{description}
</p>
{#if $$slots.more}
{#if more}
<br />
<p class="leading-8 text-lg text-gray-500">
<slot name="more" />
{@render more?.()}
</p>
{/if}
</div>
@@ -1,6 +1,10 @@
<script lang="ts">
export let src: string;
export let alt: string;
interface Props {
src: string;
alt: string;
}
let { src, alt }: Props = $props();
</script>
<figure>
@@ -4,7 +4,7 @@
import Youtube from '@lucide/svelte/icons/youtube';
</script>
<div class="border-t-[1px] px-[2%] lg:px-[14%]">
<div class="border-t px-[2%] lg:px-[14%]">
<div class="flex flex-col items-center justify-between py-[10%] pl-5 lg:flex-row lg:pl-0">
<div class="w-full min-w-[200px] lg:w-3/12">
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="classroomio logo" class="mb-5" />
+2 -2
View File
@@ -11,8 +11,8 @@
{ label: 'Customers', color: '#0233BD' },
{ label: 'Students', color: '#9747FF' }
];
let animate = false;
let titleIndex = 0;
let animate = $state(false);
let titleIndex = $state(0);
let timeout: NodeJS.Timeout;
onMount(() => {
-1
View File
@@ -19,4 +19,3 @@ export { default as Testimonial } from './testimonial.svelte';
export { default as Timeline } from './timeline.svelte';
export { default as ToolsHeader } from './tools-header.svelte';
export { default as UploadWidget } from './upload-widget.svelte';
+10 -3
View File
@@ -1,6 +1,13 @@
<script>
export let className = '';
export let open = false;
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {boolean} [open]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', open = false, children } = $props();
</script>
{#if open}
@@ -10,7 +17,7 @@
<div
class="fixed z-[3001] {className} w-full md:w-[30%] md:min-w-[500px] bg-white rounded-md mx-auto py-6 px-7"
>
<slot />
{@render children?.()}
</div>
</div>
{/if}
@@ -4,8 +4,8 @@
import { folder, globe, team, transcription } from '$lib/emojis';
import { tools } from '$lib/utils/constants/tools';
let selected = 0;
let interval: NodeJS.Timer;
let selected = $state(0);
let interval: NodeJS.Timer = $state();
const moreFeatures = [
{
@@ -22,13 +22,13 @@
'Extend invitations and manage your teaching institution seamlessly through ClassroomIO.',
image: '/team-management.png'
},
{
icon: transcription,
title: 'Auto Video Transcription',
description:
'AI automatically transcribes videos, making content searchable for seamless student access.',
image: '/video-transcription.png'
},
// {
// icon: transcription,
// title: 'Auto Video Transcription',
// description:
// 'AI automatically transcribes videos, making content searchable for seamless student access.',
// image: '/video-transcription.png'
// },
{
icon: folder,
title: 'Downloadable Lesson PDF',
@@ -52,7 +52,7 @@
<section id="morefeatures" class="py-[10%] px-3 lg:px-0 bg-gray-50">
<div class="mx-0 lg:mx-[12%]">
<div class="w-full lg:w-4/5 mx-4 lg:mx-0 mb-14">
<h2 class="mt-2 text-3xl font-bold font-display tracking-tight lg:text-4xl">
<h2 class="mt-2 text-3xl tracking-tight">
There is More...
</h2>
<p class="mt-4 text-lg text-gray-500 w-[94%] lg:w-4/5">
@@ -67,8 +67,8 @@
{#each moreFeatures as moreFeature, i}
<button
class="p-4 rounded-md {i === selected &&
'selected'} mb-3 text-start min-w-[75vw] lg:min-w-[unset]"
on:mouseenter={() => {
'selected'} mb-3 text-start min-w-[75vw] lg:min-w-[unset] cursor-pointer"
onclick={() => {
selected = i;
clearInterval(interval);
}}
@@ -82,11 +82,11 @@
alt=""
class="w-8"
/>
<p class="text-lg font-semibold">
<p class="text-lg">
{moreFeature.title}
</p>
</div>
<p>{moreFeature.description}</p>
<p class="text-sm">{moreFeature.description}</p>
</button>
{/each}
</div>
@@ -99,7 +99,7 @@
height="100%"
loading="lazy"
id="image"
in:fly={{ y: 100, duration: 1000 }}
in:fly={{ x: 100, duration: 800 }}
class="w-full flex flex-col items-center"
src={moreFeatures[selected].image}
alt={moreFeatures[selected].title}
@@ -14,13 +14,28 @@
import type { HTMLAttributes } from 'svelte/elements';
import * as NavigationMenu from '@cio/ui/base/navigation-menu';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import { cn } from '@cio/ui/tools';
export let stars: number;
let { stars }: { stars: number } = $props();
type ListItemProps = HTMLAttributes<HTMLAnchorElement> & {
title: string;
href: string;
subtitle: string;
key: string;
};
let showDrawer = false;
let showSolutions = false;
let activeLink = '';
let activeLink = $derived(page.url.pathname);
let activeHash = $derived(page.url.hash);
$effect(() => {
console.log('activeLink', activeLink);
console.log('activeHash', activeHash);
console.log('isSolutionsActive', isSolutionsActive);
});
function handleShowDrawer() {
showDrawer = !showDrawer;
@@ -76,16 +91,7 @@
}
];
type ListItemProps = HTMLAttributes<HTMLAnchorElement> & {
title: string;
href: string;
subtitle: string;
key: string;
};
$: activeLink = page.url.pathname;
$: activeHash = page.url.hash;
$: isSolutionsActive = solutions.some((s) => activeHash.includes(s.key));
let isSolutionsActive = $derived(!!solutions.some((s) => activeLink.includes(s.key)));
</script>
{#snippet list_item({ title, subtitle, href, key, class: className, ...restProps }: ListItemProps)}
@@ -140,11 +146,14 @@
</a>
<section class="hidden lg:block">
<NavigationMenu.Root class="rounded-lg bg-white p-1">
<NavigationMenu.Root class="p-1">
<NavigationMenu.List class="flex w-full items-center justify-center gap-1">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100"
class={cn(
"flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100",
isSolutionsActive && 'bg-gray-100!'
)}
>
<a href="/" class="no-underline"> Solutions </a>
</NavigationMenu.Trigger>
@@ -164,7 +173,7 @@
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100"
class={cn("flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100", activeLink.startsWith('/tools') && 'bg-gray-100!')}
>
<a href="/tools" class="no-underline"> Free Tools </a>
</NavigationMenu.Trigger>
@@ -187,11 +196,10 @@
{#snippet child()}
<a
href="/blog"
class="cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100 {activeLink.startsWith(
'/blog'
)
? 'bg-gray-100'
: ''}"
class={cn(
'cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100',
activeLink.startsWith('/blog') && 'bg-gray-100'
)}
>
Blog
</a>
@@ -204,11 +212,10 @@
{#snippet child()}
<a
href="/pricing"
class="cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100 {activeLink.startsWith(
'/pricing'
)
? 'bg-gray-100'
: ''}"
class={cn(
'cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100',
activeLink.startsWith('/pricing') && 'bg-gray-100'
)}
>
Pricing
</a>
@@ -250,7 +257,7 @@
</a>
</div>
<button type="button" aria-label="Hamburger Menu" class="block md:block lg:hidden" on:click={handleShowSolutions}>
<button type="button" aria-label="Hamburger Menu" class="block md:block lg:hidden" onclick={handleShowSolutions}>
<Menu size={24} />
</button>
@@ -262,7 +269,7 @@
>
<div class="mb-5 flex justify-between py-2">
<img loading="lazy" width="20" height="20" src="/logo-512.png" alt="classroomio logo" class="w-[15%]" />
<button class="mr-5" on:click={handleShowSolutions}>
<button class="mr-5" onclick={handleShowSolutions}>
<X size={24} />
</button>
</div>
@@ -270,9 +277,11 @@
<ul class="flex w-full flex-col items-center justify-between lg:flex-row">
<li class="w-full cursor-pointer text-sm font-semibold text-gray-800 md:text-lg">
<button
class="flex w-full items-center justify-between rounded-lg px-4 py-3 transition-all duration-200 hover:bg-gray-100"
on:click={handleShowDrawer}
class:active={isSolutionsActive}
class={cn(
"flex w-full items-center justify-between rounded-lg px-4 py-3 transition-all duration-200 hover:bg-gray-100",
isSolutionsActive && 'bg-gray-100'
)}
onclick={handleShowDrawer}
>
Our Superpowers <ChevronDown />
</button>
@@ -281,7 +290,7 @@
{#each solutions as solution}
<a
href="/{solution.key}"
on:click={() => {
onclick={() => {
handleShowSolutions();
}}
>
@@ -297,37 +306,43 @@
</li>
<a
class="w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={handleShowSolutions}
onclick={handleShowSolutions}
href="/tools"
>
<li>Free Tools</li>
</a>
<a
class="w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeLink.startsWith('/morefeatures') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
class:active={activeLink.startsWith('/blog')}
href="/blog"
>
<li>Blog</li>
</a>
<a
class="w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeLink.startsWith('/pricing') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
href="/pricing"
class:active={activeLink.startsWith('/pricing')}
>
<li>Pricing</li>
</a>
<a
class="w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeHash.includes('morefeatures') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
class:active={activeHash.includes('morefeatures')}
href="/#morefeatures"
>
<li>More features</li>
@@ -4,8 +4,14 @@
import SearchXIcon from '@lucide/svelte/icons/search-x';
import { Button } from '@cio/ui/base/button';
export let isOrg = false;
export let className = '';
/**
* @typedef {Object} Props
* @property {boolean} [isOrg]
* @property {string} [className]
*/
/** @type {Props} */
let { isOrg = false, className = '' } = $props();
function handleClick() {
if (!isOrg) {
@@ -1,7 +1,14 @@
<script>
export let className = '';
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', children } = $props();
</script>
<header class="hero-bg w-full border-b border-slate-100 pb-16 pt-32 lg:min-h-[20rem] {className}">
<slot />
{@render children?.()}
</header>
@@ -17,11 +17,11 @@
>
<div>
<h3
class="font-display mt-3 text-xl font-semibold tracking-tight text-white lg:mt-2 lg:text-4xl max-w-2xl"
class="font-display mt-3 text-xl tracking-tight text-white lg:mt-2 lg:text-4xl max-w-2xl"
>
{header}
</h3>
<p class="mt-6 text-xl text-gray-300 lg:text-lg">
<p class="mt-6 text-md text-gray-300">
{subText}
</p>
</div>
@@ -2,7 +2,11 @@
import { embedSenjaWidget } from '@cio/utils/senja';
import { onMount } from 'svelte';
export let id = '';
interface Props {
id?: string;
}
let { id = '' }: Props = $props();
onMount(() => {
embedSenjaWidget(id);
@@ -7,7 +7,7 @@
<div
class="relative overflow-hidden mt-10 flex space-y-3 flex-col text-center items-center border border-[#D9E0F5] justify-center w-full rounded-md min-h-[300px] p-4 bg-[#F5F8FE]"
>
<div class="absolute w-20 h-20 bg-[#0233BD] -top-14 -left-9 rotate-[50deg]" />
<div class="absolute w-20 h-20 bg-[#0233BD] -top-14 -left-9 rotate-[50deg]"></div>
<h1 class="font-bold text-3xl">Your message has been sent 🎉</h1>
<p class="w-[70%] font-normal text-sm">
We'll get back to you as soon as possible. In the meantime, Explore and enjoy features on our
@@ -1,25 +1,29 @@
<script lang="ts">
export let items: {
interface Props {
items?: {
title: string;
desc: string;
}[] = [];
}[];
}
let { items = [] }: Props = $props();
</script>
<div class="relative w-fit flex justify-center space-x-8 md:space-x-10">
<div
class="w-[3px] md:w-[2px] bg-blue-600 mt-10 before:bg-gradient-to-t before:from-white before:to-transparent before:w-[4px] before:h-[9rem] before:absolute before:bottom-0"
/>
class="w-[3px] md:w-[2px] bg-blue-600 mt-10 before:bg-linear-to-t before:from-white before:to-transparent before:w-[4px] before:h-36 before:absolute before:bottom-0"
></div>
<div class="space-y-8">
{#each items as card, index}
<div class="md:w-[35rem] w-full border-2 rounded-lg px-4 py-8 space-y-4 bg-white">
<div class="md:w-140 w-full border-2 rounded-lg px-4 py-8 space-y-4 bg-white">
<div class="flex items-center">
<div
class={`z-50 absolute -left-3 ${
index > 0 &&
'before:bg-gradient-to-t before:from-white before:to-transparent before:w-[4px] before:h-[9rem] before:absolute before:-top-36'
'before:bg-linear-to-t before:from-white before:to-transparent before:w-[4px] before:h-36 before:absolute before:-top-36'
} flex items-center justify-center w-7 h-7 bg-blue-100 rounded-full`}
>
<div class="w-4 h-4 bg-blue-800 rounded-full z-50" />
<div class="w-4 h-4 bg-blue-800 rounded-full z-50"></div>
</div>
<h1 class="text-2xl font-bold text-blue-800">{card.title}</h1>
</div>
@@ -1,7 +1,14 @@
<script>
export let className = '';
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', children } = $props();
</script>
<header class="tools-hero-bg py-24 text-center {className} border-b border-slate-100 bg-white">
<slot />
{@render children?.()}
</header>
@@ -3,12 +3,21 @@
import Trash2 from '@lucide/svelte/icons/trash-2';
import { ALLOWED_IMAGE_TYPES, validateImageUpload } from '@cio/utils/functions/fileValidation';
export let imageURL = '';
export let label = '';
export let isRequired = false;
export let labelClassName = '';
interface Props {
imageURL?: string;
label?: string;
isRequired?: boolean;
labelClassName?: string;
}
let {
imageURL = $bindable(''),
label = '',
isRequired = false,
labelClassName = ''
}: Props = $props();
let isUploading = false;
let fileInput: HTMLInputElement;
let fileInput: HTMLInputElement = $state();
const onFileSelected = () => {
const file = fileInput?.files?.[0];
@@ -52,8 +61,8 @@
};
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div>
{#if label}
<p class="flex w-full items-center justify-between text-left dark:text-white {labelClassName}">
@@ -65,7 +74,7 @@
</span>
{#if imageURL}
<span title="clear image" on:click={deleteImage}>
<span title="clear image" onclick={deleteImage}>
<Trash2 class="fill-red-500 " />
</span>
{/if}
@@ -73,14 +82,14 @@
{/if}
<div
class="flex min-h-[200px] w-full items-center justify-center rounded-md border p-4"
on:click={() => fileInput.click()}
onclick={() => fileInput.click()}
>
<input
type="file"
accept=".jpg,.jpeg,.png,.gif,.webp,image/jpeg,image/png,image/gif,image/webp"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
onchange={onFileSelected}
disabled={isUploading}
required={isRequired}
/>