feat: website release v2 (#616)

* feat: upgrade to svelte 5 & tailwind v4 + add navigation menu to ui package

* feat: website navigation with nav menu from shadcn

* chore: refactor navigation menu storybook

* fix: responsiveness bug

* chore: remove tailwind config file

* chore:
- remove carbon icons from repo
- add Geist font family

* refactor website

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Adeyemi Adetunji
2025-12-20 12:44:59 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 690cf147ff
commit e6a3cf9332
108 changed files with 2425 additions and 2169 deletions
-22
View File
@@ -1,22 +0,0 @@
<script>
import Renew from 'carbon-icons-svelte/lib/Renew.svelte';
export let label = '';
export let className = '';
export let isLoading = false;
export let isDisabled = false;
export let onClick = () => {};
</script>
<div class="w-full flex items-center justify-center pt-5">
<button
class="bg-[#0233BD] text-white font-medium text-sm py-4 {className}"
disabled={isDisabled}
on:click={onClick}
>
{#if isLoading}
<Renew size={24} class="animate-spin" />
{:else}
{label}
{/if}
</button>
</div>
-7
View File
@@ -1,7 +0,0 @@
<script lang="ts">
export let label: string;
</script>
<p class="whitespace-nowrap bg-gray-200 px-2 py-1 rounded-full text-xs font-normal text-black">
{label}
</p>
@@ -1,70 +0,0 @@
<script>
import Button from '$lib/Button/Button.svelte';
import TextArea from '$lib/Input/TextArea.svelte';
import TextField from '$lib/Input/TextField.svelte';
import { isFormValid } from '$lib/utils/isFormValid';
import { submitForm } from '$lib/utils/submitForm';
import Sucess from '$lib/Contact/Sucess.svelte';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
const handleClick = async () => {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[FEEDBACK] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
};
</script>
<div>
{#if sent}
<Sucess />
{:else}
<div class="w-full my-10 space-y-5">
<div class="flex flex-col md:flex-row gap-10">
<TextField label="Your Name" isRequired className="w-full" bind:value={name} />
<TextField label="Your Email" isRequired className="w-full" bind:value={email} />
</div>
<TextField label="Subject" isRequired bind:value={title} />
<TextArea label="Message" isRequired bind:value={description} />
{#if !!errorMessage}
<p class="text-left text-sm text-red-500">{errorMessage}</p>
{/if}
<Button
label="Submit"
className="px-32"
onClick={handleClick}
isDisabled={isLoading}
{isLoading}
/>
</div>
{/if}
</div>
@@ -1,70 +0,0 @@
<script>
import Button from '$lib/Button/Button.svelte';
import TextArea from '$lib/Input/TextArea.svelte';
import TextField from '$lib/Input/TextField.svelte';
import { isFormValid } from '$lib/utils/isFormValid';
import { submitForm } from '$lib/utils/submitForm';
import Sucess from './Sucess.svelte';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
async function handleClick() {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[HELP] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
}
</script>
<div>
{#if sent}
<Sucess />
{:else}
<div class="w-full my-10 space-y-5">
<div class="flex flex-col md:flex-row gap-10">
<TextField label="Your Name" isRequired className="w-full" bind:value={name} />
<TextField label="Your Email" isRequired className="w-full" bind:value={email} />
</div>
<TextField label="Subject" isRequired bind:value={title} />
<TextArea label="Message" isRequired bind:value={description} />
{#if !!errorMessage}
<p class="text-left text-sm text-red-500">{errorMessage}</p>
{/if}
<Button
label="Submit"
className="px-32"
onClick={handleClick}
isDisabled={isLoading}
{isLoading}
/>
</div>
{/if}
</div>
-213
View File
@@ -1,213 +0,0 @@
<script>
import LogoYoutube from 'carbon-icons-svelte/lib/LogoYoutube.svelte';
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
</script>
<div class="border-t-[1px] 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"
/>
<p class="lg:text-base mb-5 mr-2 text-sm leading-7 tracking-wide text-gray-600 lg:leading-7">
The Open Source Learning Management System for Companies
<br />
ClassroomIO is the easiest way to grow your teaching business.
</p>
<!-- <a
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
target="_blank"
><img
loading="lazy"
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=425236&theme=light"
alt="ClassroomIO - The&#0032;Open&#0032;Source&#0032;Platform&#0032;for&#0032;Tech&#0032;Bootcamps | Product Hunt"
style="width: 250px; height: 54px;"
width="250"
height="54"
/></a
> -->
<div class="mt-5 flex items-center gap-3 lg:mt-10 lg:gap-7">
<a href="https://www.facebook.com/classroomiohq" target="_blank" aria-label="Facebook Link">
<LogoFacebook
class="scale-90 cursor-pointer fill-gray-400 transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
size={20}
/>
</a>
<a href="https://twitter.com/classroomio" target="_blank" aria-label="Twitter Link">
<img
width="20"
height="20"
loading="lazy"
src="/twitter.svg"
alt=""
class="scale-90 cursor-pointer transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
/>
</a>
<a href="https://www.youtube.com/@ClassroomIO" target="_blank" aria-label="Youtube Link">
<LogoYoutube
class="scale-90 cursor-pointer fill-gray-400 transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
size={20}
/>
</a>
</div>
</div>
<nav
class="mt-16 flex w-full min-w-[400px] flex-col items-start justify-end gap-12 space-y-6 lg:mt-0 lg:w-3/4 lg:flex-row lg:space-y-0"
>
<div class="max-w-[120px]">
<p class="lg:text-base mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
Product
</p>
<ul class="space-y-2 lg:space-y-4">
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/#coursemanagement"> Course Management </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/#customization"> Customizations </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/#collaboration"> Forum </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/#ai"> AI Lesson Planner </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/#student-dashboard"> Student Dashboard </a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="lg:text-base mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
Solutions
</p>
<ul class="space-y-2 lg:space-y-4">
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/employee-training">Employee Training</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/bootcamps">Bootcamps</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/customer-education">Customer Education</a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="lg:text-base mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
Resources
</p>
<ul class="space-y-2 lg:space-y-4">
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/contact">Contact us</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/teach">Teach with us</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
Getting Started
</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/oss-friends"> OSS Friends </a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="lg:text-base mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
Company
</p>
<ul class="space-y-2 lg:space-y-4">
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/roadmap"> Roadmap </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="mailto:[email protected]" target="_blank"> Support </a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
target="_blank"
>
Privacy policy
</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
Getting Started
</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
target="_blank"
>
Terms of use
</a>
</li>
</ul>
</div>
</nav>
</div>
<footer
class="lg:text-base border-t-[1px] py-[3%] text-center text-xs font-light leading-5 text-gray-500 lg:text-left"
>
<p>All rights reserved.</p>
<p class="flex items-center">
ClassroomIO © {new Date().getFullYear()} by
<a href="https://twitter.com/rotimi_best" target="_blank" class="mx-1 text-blue-700 underline"
>Rotimi Best</a
>
from
<img loading="lazy" alt="Nigerian Flag" src="/ng.png" class="ml-2 h-3 w-3" />
</p>
</footer>
</div>
-42
View File
@@ -1,42 +0,0 @@
<script>
import { faqs } from './faqs';
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
</script>
<div class="mb-[15%] mt-[5%]">
<header class="mb-[5%] px-[8%] text-center">
<h1 class="mt-2 text-4xl font-medium tracking-tight text-gray-900 lg:text-4xl">
Questions & Answers
</h1>
<p
class="mt-4 text-lg font-normal tracking-normal text-gray-600 lg:px-[20%] lg:text-xl lg:tracking-wide"
>
Can’t find the answer you’re looking for?
<a class="font-semibold text-blue-700" href="mailto:[email protected]" target="_blank"
>Shoot us an email</a
> and we’ll get back to you ASAP.
</p>
</header>
<div class="mt-[20%] grid grid-cols-1 gap-10 lg:m-0 lg:grid-cols-3 lg:gap-20 lg:px-[13%]">
{#each faqs as faq}
<div class="max-w-[100%] px-6 lg:p-0">
<h1 class="text:md lg:text-base font-semibold leading-8 text-gray-800">
{faq.question}
</h1>
<p
class="text:md lg:text-base mt-2 font-light leading-7 tracking-wide text-gray-600 lg:leading-9"
>
{faq.answer}
</p>
</div>
{/each}
</div>
<PageSignupCTA
header="Your Custom Academy – Up & Running in Minutes!"
subText="Try before you buy. No credit card required."
btnLabel="Book a demo"
link="classroomio/demo"
/>
</div>
@@ -1,28 +0,0 @@
<script>
import SenjaEmbed from '$lib/Senja/Embed.svelte';
</script>
<section id="testimonial" class="flex flex-col items-center py-[5%] px-3 lg:px-0">
<h1
class="mx-auto text-3xl md:text-4xl lg:text-6xl font-normal leading-[1.5] flex flex-col items-center gap-4"
>
Hear from Happy ClassroomIO Users
</h1>
<p
class="mx-auto text-center my-5 lg:mt-6 px-4 lg:p-0 text-base tracking-wide leading-6 text-gray-400 lg:w-2/4"
>
See what excites them about building and managing bootcamps with ClassroomIO.
</p>
<a
class="md:text-base w-11/12 rounded-md bg-blue-700 px-6 py-3 text-sm font-medium text-white text-center transition-all delay-100 hover:bg-blue-800 hover:shadow-xl hover:scale-95 md:w-fit lg:px-6 lg:py-5 lg:font-semibold"
href="/signup"
rel="noopener noreferrer nofollow"
>
Sign Up for Free
</a>
<div class="relative my-5 md:my-10 w-[85%] lg:w-[70%]">
<SenjaEmbed id="3e847d7a-13d3-483e-8a71-5b35273dcc6c" />
</div>
</section>
@@ -1,17 +0,0 @@
<script>
export let color = 'currentColor';
</script>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="dark"
>
<path
d="M18 22.5H6C5.60232 22.4995 5.22105 22.3414 4.93984 22.0602C4.65864 21.7789 4.50046 21.3977 4.5 21V3C4.5004 2.6023 4.65856 2.221 4.93978 1.93978C5.221 1.65856 5.6023 1.5004 6 1.5H18C18.3977 1.50046 18.779 1.65864 19.0602 1.93984C19.3414 2.22105 19.4995 2.60232 19.5 3V15.4636L15.75 13.5886L12 15.4636V3H6V21H18V18H19.5V21C19.4995 21.3977 19.3413 21.7789 19.0601 22.0601C18.7789 22.3413 18.3977 22.4995 18 22.5ZM15.75 11.9114L18 13.0364V3H13.5V13.0364L15.75 11.9114Z"
fill={color}
/>
</svg>
@@ -1,56 +0,0 @@
<script lang="ts">
export let label = '';
export let disabled = false;
export let placeholder = '';
export let value = '';
export let rows = 3;
export let className = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
export let onChange = () => {}; // This is to know if element is 'dirty'
export let ref = null;
</script>
<label for="text-field" class="block w-full relative {className}">
{#if label}
<p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
<span>
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</span>
</p>
{/if}
<textarea
{disabled}
{rows}
{placeholder}
bind:value
bind:this={ref}
required={isRequired}
class="form-textarea p-2 border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
{bgColor} {!!errorMessage ? 'border-red-500' : 'border-gray-300'}"
on:change={onChange}
/>
{#if !!errorMessage}
<p class="text-sm text-red-500">
{errorMessage}
</p>
{:else if helperMessage}
<p class="text-sm text-gray-500 dark:text-white">
{helperMessage}
</p>
{/if}
<slot name="iconbutton" />
</label>
<style>
.form-textarea {
box-shadow: none !important;
}
</style>
@@ -1,81 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
export let label = '';
export let placeholder = '';
export let value: string | number | null = null;
export let name = '';
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white';
export let autoFocus = false;
export let isRequired = false;
export let isDisabled = false;
export let errorMessage = '';
export let helperMessage = '';
export let autoComplete = true;
let ref: HTMLInputElement | undefined = undefined;
let focusClass = '';
onMount(() => {
if (autoFocus && ref) {
ref.focus();
}
});
</script>
<label class="block relative {className}">
{#if label}
<p class=" dark:text-white text-left m-0 {labelClassName}">
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</p>
{/if}
<input
class="form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
'hover:cursor-not-allowed opacity-50'} dark:text-black p-2 mt-1 block w-full {bgColor} {errorMessage
? 'border-red-600'
: ''}"
{placeholder}
bind:value
bind:this={ref}
required={isRequired}
disabled={isDisabled}
autocomplete={autoComplete ? 'on' : 'off'}
on:blur={(e) => {
if (focusClass.includes('border-primary-600')) {
focusClass = focusClass.replace('border-primary-600', '');
}
}}
on:focus={() => (focusClass += ' border-primary-600')}
{name}
/>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{:else if helperMessage}
<p class="dark:text-white text-sm">
{helperMessage}
</p>
{/if}
</label>
<style>
.form-input {
box-shadow: none !important;
}
.password-eye {
position: absolute;
top: 30px;
right: 6px;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
@@ -1,334 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import CourseIcon from '$lib/Icons/CourseIcon.svelte';
import ChevronDown from 'carbon-icons-svelte/lib/ChevronDown.svelte';
import CloseLarge from 'carbon-icons-svelte/lib/CloseLarge.svelte';
import ForumIcon from 'carbon-icons-svelte/lib/Forum.svelte';
import MapCenter from 'carbon-icons-svelte/lib/MapCenter.svelte';
import TextAlignJustify from 'carbon-icons-svelte/lib/TextAlignJustify.svelte';
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
export let stars: number;
let showDrawer = false;
let showSolutions = false;
let activeLink = '';
let closeTimeout: NodeJS.Timeout;
function closeSolutions() {
closeTimeout = setTimeout(() => {
showSolutions = false;
}, 100);
}
function resetCloseTimeout() {
clearTimeout(closeTimeout);
}
function handleShowDrawer() {
showDrawer = !showDrawer;
}
function handleShowSolutions() {
showSolutions = !showSolutions;
}
const solutions = [
{
key: 'employee-training',
title: 'Employee Training',
subtitle: 'Keep your team in sync.'
},
{
key: 'bootcamps',
title: 'Bootcamps',
subtitle: 'Drive student satisfaction.'
},
{
key: 'customer-education',
title: 'Customer Education',
subtitle: 'Teach customers your product.'
}
];
$: activeLink = page.url.pathname;
$: activeHash = page.url.hash;
$: isSolutionsActive = solutions.some((s) => activeHash.includes(s.key));
</script>
<div
class="flex w-full justify-between items-center py-6 border-b-[1px] md:px-12 px-5 fixed top-0 z-[3000] filter backdrop-blur-xl shadow-sm bg-white"
>
<a href="/" class="w-[10%]">
<div class="flex items-center w-full">
<img
loading="lazy"
width="28"
height="28"
src="/logo-512.png"
alt="classroomio logo"
class="w-6 md:w-10"
/>
<h1 class="text-xs md:text-lg font-medium ml-2">ClassroomIO</h1>
</div>
</a>
<nav class="w-[50%] hidden md:hidden lg:block">
<ul class="flex justify-center items-center w-full gap-5">
<li class="text-gray-800 font-semibold text-sm cursor-pointer relative">
<button
on:focus={() => (showSolutions = true)}
on:mouseenter={() => (showSolutions = true)}
on:mouseleave={closeSolutions}
class="flex items-center hover:bg-gray-100 {showSolutions &&
'bg-gray-100'} px-4 py-2 rounded-md transition-all duration-200"
on:click={() => (showSolutions = !showSolutions)}
class:active={isSolutionsActive}
>
<span class="mr-2">Solutions</span>
<ChevronDown size={16} />
</button>
{#if showSolutions}
<button
class="absolute w-[20rem] top-10 -left-10 border rounded-lg px-2 py-4 shadow-slate-700 z-[3001] bg-white flex flex-col gap-3"
on:mouseenter={resetCloseTimeout}
on:mouseleave={() => (showSolutions = false)}
>
{#each solutions as solution}
<a
class="flex justify-between items-center w-full rounded-lg hover:bg-gray-100 hover:cursor-pointer p-2 transition-colors duration-200"
href="/{solution.key}"
on:click={() => (showSolutions = false)}
>
{#if solution.key === 'employee-training'}
<CourseIcon />
{:else if solution.key === 'bootcamps'}
<MapCenter size={24} />
{:else if solution.key === 'customer-education'}
<ForumIcon size={24} />
{/if}
<div class="w-[84%] text-start">
<h3 class="font-semibold text-sm text-gray-700">
{solution.title}
</h3>
<p class="font-normal text-sm text-gray-600">
{solution.subtitle}
</p>
</div>
</a>
{/each}
</button>
{/if}
</li>
<a
href="/tools"
class="text-gray-800 font-semibold text-sm cursor-pointer"
class:active={activeLink.startsWith('/tools')}
>
<li class="hover:bg-gray-100 px-4 py-2 rounded-md transition-all duration-200">
Free Tools
</li>
</a>
<a
href="/blog"
class="text-gray-800 font-semibold text-sm cursor-pointer"
class:active={activeLink.startsWith('/blog')}
>
<li class="hover:bg-gray-100 px-4 py-2 rounded-md transition-all duration-200">Blog</li>
</a>
<a
href="/pricing"
class="text-gray-800 font-semibold text-sm cursor-pointer"
class:active={activeLink.startsWith('/pricing')}
>
<li class="hover:bg-gray-100 px-4 py-2 rounded-md transition-all duration-200">Pricing</li>
</a>
</ul>
</nav>
<div class="justify-between items-center flex-row hidden md:hidden lg:flex gap-3">
<a
href="/discord"
target="_blank"
class="flex items-center hover:opacity-80 transition-opacity duration-200"
>
<img
loading="lazy"
alt="discord logo"
src="/discord-blue.png"
class="w-6 h-5 cursor-pointer"
/>
</a>
<div class="flex items-center">
<a
href="/github"
target="_blank"
class="flex items-center gap-1.5 p-2 rounded-md transition-all duration-200 ease-in-out hover:bg-gray-100 group"
>
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
class="w-5 h-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span
class="text-sm text-gray-600 font-medium leading-none transition-colors duration-200 group-hover:text-black"
>
{stars}
</span>
</a>
</div>
<a
class="font-medium text-sm after:content-['→'] after:ml-2 hover:opacity-80 transition-opacity duration-200"
href="https://app.classroomio.com"
>
Dashboard
</a>
</div>
<button
type="button"
aria-label="Hamburger Menu"
class="block md:block lg:hidden"
on:click={handleShowSolutions}
>
<TextAlignJustify size={24} />
</button>
{#if showSolutions}
<div
in:fly={{ x: 20, duration: 700 }}
out:fly={{ x: 20, duration: 400 }}
class="w-[60%] md:w-[40%] h-[100vh] border-2 sm:block lg:hidden px-3 pt-3 pb-2 md:p-7 absolute right-0 top-0 bg-white"
>
<div class="flex justify-between py-2 mb-5">
<img
loading="lazy"
width="20"
height="20"
src="/logo-512.png"
alt="classroomio logo"
class="w-[15%]"
/>
<button class="mr-5" on:click={handleShowSolutions}>
<CloseLarge size={24} />
</button>
</div>
<nav>
<ul class="flex items-center flex-col lg:flex-row justify-between w-full">
<li class="text-gray-800 font-semibold text-sm md:text-lg cursor-pointer w-full">
<button
class="w-full flex items-center justify-between hover:bg-gray-100 py-3 px-4 rounded-lg transition-all duration-200"
on:click={handleShowDrawer}
class:active={isSolutionsActive}
>
Our Superpowers <ChevronDown />
</button>
{#if showDrawer}
<div in:fly={{ y: -20, duration: 700 }} out:fly={{ y: 20, duration: 400 }}>
{#each solutions as solution}
<a
href="/{solution.key}"
on:click={() => {
handleShowSolutions();
}}
>
<p
class="font-normal text-xs text-gray-700 hover:bg-gray-100 rounded-lg py-2.5 pl-5 transition-colors duration-200"
>
{solution.title}
</p>
</a>
{/each}
</div>
{/if}
</li>
<a
class="text-gray-800 font-semibold text-sm md:text-lg cursor-pointer hover:bg-gray-100 py-3 px-4 rounded-xl w-full transition-all duration-200"
on:click={handleShowSolutions}
href="/tools"
>
<li>Free Tools</li>
</a>
<a
class="text-gray-800 font-semibold text-sm md:text-lg cursor-pointer hover:bg-gray-100 py-3 px-4 rounded-md w-full transition-all duration-200"
on:click={() => {
handleShowSolutions();
}}
class:active={activeLink.startsWith('/blog')}
href="/blog"
>
<li>Blog</li>
</a>
<a
class="text-gray-800 font-semibold text-sm md:text-lg cursor-pointer hover:bg-gray-100 py-3 px-4 rounded-xl w-full transition-all duration-200"
on:click={() => {
handleShowSolutions();
}}
href="/pricing"
class:active={activeLink.startsWith('/pricing')}
>
<li>Pricing</li>
</a>
<a
class="text-gray-800 font-semibold text-sm md:text-lg cursor-pointer hover:bg-gray-100 py-3 px-4 rounded-md w-full transition-all duration-200"
on:click={() => {
handleShowSolutions();
}}
class:active={activeHash.includes('morefeatures')}
href="/#morefeatures"
>
<li>More features</li>
</a>
</ul>
</nav>
<div class="flex items-start flex-col gap-y-2 mt-5 border-t-[1px] pt-5">
<a
href="/discord"
target="_blank"
class="flex items-center rounded-md w-full text-left py-4 px-4 hover:bg-gray-100 text-sm md:text-lg transition-all duration-200"
>
<img
loading="lazy"
alt="discord logo"
src="/discord-blue.png"
class="w-6 h-5 mr-2 cursor-pointer"
/>
<span>Discord</span>
</a>
<a
href="/github"
target="_blank"
class="flex items-center rounded-md w-full text-left py-4 px-4 hover:bg-gray-100 text-sm md:text-lg group transition-all duration-200"
>
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
class="w-5 h-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span class="ml-3 transition-colors duration-200 group-hover:text-black">Github</span>
<span
class="text-sm text-gray-600 font-medium ml-1 transition-colors duration-200 group-hover:text-black"
>({stars})</span
>
</a>
<a
class="font-semibold after:content-['→'] rounded-md after:ml-2 w-full text-left py-4 px-4 hover:bg-gray-100 text-sm md:text-lg transition-all duration-200"
href="https://app.classroomio.com"
>
Dashboard
</a>
</div>
</div>
{/if}
</div>
<style>
.active {
background-color: #f3f4f6;
border-radius: 6px;
}
</style>
@@ -1,38 +0,0 @@
<script>
import { goto } from '$app/navigation';
export let isOrg = false;
export let className = '';
function handleClick() {
if (!isOrg) {
return goto('/');
}
return goto('https://classroomio.com');
}
</script>
<div class="{className} min-h-[80vh] flex items-center justify-center flex-col w-screen">
<img src="https://brand.cdn.clsrio.com/404.png" alt="classroomio_error_image" />
<div class="flex flex-col items-center justify-center dark:text-white gap-3 w-4/5 lg:w-2/5">
<p class="text-4xl dark:text-white font-semibold mb-5">
{#if isOrg}
Organization doesn't exist!
{:else}
Page not found
{/if}
</p>
<p class=" text-gray-700 text-lg text-center dark:text-white mb-5">
The page you are looking for doesn't exist or has been moved. Please go back to the homepage.
</p>
<button
class="text-base rounded-md bg-black transition-all delay-100 hover:shadow-xl hover:scale-95 px-6 py-4 font-semibold text-white lg:px-7 lg:py-3 lg:text-lg"
on:click={handleClick}
>
Go Home
</button>
</div>
</div>
@@ -1,9 +0,0 @@
<script>
export let className = '';
</script>
<header
class="hero-bg lg:min-h-[20rem] mt-[10%] md:mt-16 pt-24 pb-16 w-full border-b border-slate-100 {className}"
>
<slot />
</header>
@@ -1,52 +0,0 @@
<script>
export let header = '';
export let subText = '';
export let btnLabel = '';
export let link = '';
export let demo = true;
</script>
<div
class="floating grad relative mx-auto my-[5%] flex h-[61vh] w-[100%] flex-col items-center justify-center px-[6%] py-[12%] text-center shadow-2xl lg:h-[54vh] lg:w-[70%] lg:rounded-3xl max-w-4xl"
>
<div>
<h1
class="font-display mt-3 text-3xl font-bold tracking-tight text-white lg:mt-2 lg:text-4xl max-w-2xl"
>
{header}
</h1>
<p class="mt-6 text-xl text-gray-300 lg:text-lg">
{subText}
</p>
</div>
{#if demo}
<button
class="text-base mt-10 rounded-md bg-white px-6 py-4 font-semibold text-gray-900 lg:px-7 lg:py-3 lg:text-lg"
data-cal-config="'layout':'month_view'"
data-cal-link="classroomio/demo"
>
{btnLabel}
</button>
{:else}
<a
class="text-base mt-10 rounded-md bg-white px-6 py-4 font-semibold text-gray-900 lg:px-7 lg:py-3 lg:text-lg"
href={link}
target="_blank"
rel="noopener noreferrer nofollow"
>
{btnLabel}
</a>
{/if}
</div>
<style>
.grad {
/* background: rgb(2, 51, 189);
background: radial-gradient(circle at 50% 100%, rgba(2, 51, 189, 1) 1%, rgb(4, 2, 56) 100%); */
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
background-image: url('https://brand.cdn.clsrio.com/cta-bg.png');
}
</style>
@@ -1,6 +1,6 @@
<script>
import Chip from '$lib/Chip/Chip.svelte';
import { formatDate } from '$lib/utils/formatDate';
import { Badge } from '@cio/ui/base/badge';
import { formatDate } from '$lib/utils/format-date';
export let post;
export let isRecommended = false;
@@ -15,7 +15,7 @@
<div class="flex flex-col">
<div class="flex flex-wrap gap-2">
{#each post.tags as tag}
<Chip label={tag} />
<Badge variant="outline">{tag}</Badge>
{/each}
</div>
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import Section from './CoreFeatureSection.svelte';
import Section from './core-feature-section.svelte';
import { PUBLIC_ENABLE_STATS } from '$env/static/public';
</script>
@@ -0,0 +1,40 @@
<script>
import { faqs } from '$lib/utils/faqs';
import { PageSignupCTA } from '$lib/components';
import * as Accordion from '@cio/ui/base/accordion';
</script>
<div class="mb-20 mt-10 flex flex-col items-center justify-center">
<header class="text-center">
<h1 class="mt-2 text-xl md:text-4xl">
Questions & Answers
</h1>
<p
class="mt-4 text-sm md:text-lg font-normal tracking-normal ui:text-muted-foreground lg:px-[20%] lg:text-xl lg:tracking-wide"
>
Can't find the answer you're looking for?
<a class="font-semibold text-blue-700" href="mailto:[email protected]" target="_blank"
>Shoot us an email</a> and we'll get back to you ASAP.
</p>
</header>
<div class="mt-[20%] lg:m-0 max-w-3xl w-full mx-auto px-2">
<Accordion.Root type="single" class="w-full">
{#each faqs as faq, index}
<Accordion.Item value="faq-{index}">
<Accordion.Trigger>{faq.question}</Accordion.Trigger>
<Accordion.Content>
{faq.answer}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
</div>
<PageSignupCTA
header="Your Custom Academy – Up & Running in Minutes!"
subText="Try before you buy. No credit card required."
btnLabel="Book a demo"
link="classroomio/demo"
/>
</div>
@@ -0,0 +1,144 @@
<script>
import { Button } from '@cio/ui/base/button';
import Facebook from '@lucide/svelte/icons/facebook';
import Youtube from '@lucide/svelte/icons/youtube';
</script>
<div class="border-t-[1px] 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" />
<p class="mb-5 mr-2 text-sm leading-7 tracking-wide text-gray-600 lg:text-base lg:leading-7">
The Open Source Learning Management System for Companies
<br />
ClassroomIO is the easiest way to grow your teaching business.
</p>
<!-- <a
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
target="_blank"
><img
loading="lazy"
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=425236&theme=light"
alt="ClassroomIO - The&#0032;Open&#0032;Source&#0032;Platform&#0032;for&#0032;Tech&#0032;Bootcamps | Product Hunt"
style="width: 250px; height: 54px;"
width="250"
height="54"
/></a
> -->
<div class="mt-5 flex items-center gap-3 lg:mt-10 lg:gap-7">
<Button href="https://www.facebook.com/classroomiohq" target="_blank" variant="ghost" aria-label="Facebook Link">
<Facebook />
</Button>
<Button href="https://twitter.com/classroomio" target="_blank" variant="ghost" aria-label="Twitter Link">
<img
width="20"
height="20"
loading="lazy"
src="/twitter.svg"
alt=""
/>
</Button>
<Button href="https://www.youtube.com/@ClassroomIO" target="_blank" variant="ghost" aria-label="Youtube Link">
<Youtube />
</Button>
</div>
</div>
<nav
class="mt-16 flex w-full flex-col items-start justify-end gap-12 space-y-6 lg:mt-0 lg:w-3/4 lg:flex-row lg:space-y-0"
>
<div class="max-w-[120px]">
<p class="mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10 lg:text-base">Product</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#coursemanagement"> Course Management </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#customization"> Customizations </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#collaboration"> Forum </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#ai"> AI Lesson Planner </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#student-dashboard"> Student Dashboard </a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10 lg:text-base">Solutions</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/employee-training">Employee Training</a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/bootcamps">Bootcamps</a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/customer-education">Customer Education</a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10 lg:text-base">Resources</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/contact">Contact us</a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/teach">Teach with us</a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/oss-friends"> OSS Friends </a>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10 lg:text-base">Company</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/roadmap"> Roadmap </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="mailto:[email protected]" target="_blank"> Support </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594" target="_blank">
Privacy policy
</a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594" target="_blank">
Terms of use
</a>
</li>
</ul>
</div>
</nav>
</div>
<footer
class="border-t py-3 text-center text-xs font-light leading-5 text-gray-500 lg:text-left lg:text-base"
>
<p>All rights reserved.</p>
<p class="flex items-center justify-center">
ClassroomIO © {new Date().getFullYear()} by
<a href="https://twitter.com/rotimi_best" target="_blank" class="mx-1 text-blue-700 underline">Rotimi Best</a>
from
<img loading="lazy" alt="Nigerian Flag" src="/ng.png" class="ml-2 h-3 w-3" />
</p>
</footer>
</div>
@@ -3,6 +3,7 @@
import { fly } from 'svelte/transition';
import { sineInOut } from 'svelte/easing';
import { PUBLIC_ENABLE_USERS_COMPANIES } from '$env/static/public';
import { Button } from '@cio/ui/base/button';
const names = [
{ label: 'Audience', color: '#3ADFEC' },
@@ -30,32 +31,32 @@
});
</script>
<div class="hero-bg relative border-b-2 text-center bg-[#F5F8FE] h-full mt-[5%] w-full">
<div class="pt-24 w-full">
<div class="mb-10 ml-[5%] lg:ml-0 flex w-full items-center justify-start lg:justify-center">
<div class="hero-bg pt-18 relative h-full w-full border-b-2 bg-[#F5F8FE] text-center">
<div class="w-full pt-24">
<div class="mb-10 ml-[5%] flex w-full items-center justify-start lg:ml-0 lg:justify-center">
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="/github"
style="color: rgb(75, 85, 99);"
class="rounded-full border-2 border-[#C2D2FF] px-4 py-1 text-sm font-medium bg-[#DCE5FF] text-[#4B5563] hover:underline"
>We are <span class="text-blue-700">100%</span> open source</a
class="rounded-full border-2 border-[#C2D2FF] bg-[#DCE5FF] px-4 py-1 text-sm font-medium text-[#4B5563] hover:underline"
>We are <span class="ui:text-primary">100%</span> open source</a
>
</div>
<div class="justify-between w-[90%] absolute top-[10%] left-[5%] hidden lg:flex">
<div class="absolute left-[5%] top-[10%] hidden w-[90%] justify-between lg:flex">
{#if animate}
<div
class="relative transition-all hover:scale-110 rotate-12"
class="relative rotate-12 transition-all hover:scale-110"
transition:fly={{ y: 100, delay: 0, easing: sineInOut }}
>
<a
href="/#collaboration"
class="px-6 py-2 border-[1.5px] border-[#3ADFEC] rounded-[4px] bg-white text-[#3ADFEC] text-lg font-bold"
class="rounded-[4px] border-[1.5px] border-[#3ADFEC] bg-white px-6 py-2 text-lg font-bold text-[#3ADFEC]"
>Collaboration</a
>
<svg
class="absolute -right-1 -bottom-2.5"
class="absolute -bottom-2.5 -right-1"
width="11"
height="11"
viewBox="0 0 11 11"
@@ -67,17 +68,17 @@
</div>
<div
class="relative transition-all hover:scale-110 -rotate-12"
class="relative -rotate-12 transition-all hover:scale-110"
transition:fly={{ x: 100, delay: 50, easing: sineInOut }}
>
<a
href="/github"
target="_blank"
class="px-6 py-2 border-[1.5px] border-[#BF0696] rounded-[4px] bg-white text-[#BF0696] text-lg font-bold"
class="rounded-[4px] border-[1.5px] border-[#BF0696] bg-white px-6 py-2 text-lg font-bold text-[#BF0696]"
>Open Source</a
>
<svg
class="absolute -right-1 -bottom-2.5"
class="absolute -bottom-2.5 -right-1"
width="11"
height="11"
viewBox="0 0 11 11"
@@ -91,21 +92,19 @@
</div>
<div class="w-full">
<div
class="font-bold md:leading-[1.5] text-left lg:text-center ml-6 text-slate-900 text-4xl lg:text-7xl"
>
<div class="ml-6 text-left text-2xl font-semibold text-slate-900 lg:text-center lg:text-6xl">
<span>Educate Your</span>
<div class="flex-wrap flex items-center justify-center lg:gap-3 mb-2">
<div class="mb-2 flex flex-wrap items-center justify-center lg:gap-3">
<span
class="w-full lg:w-[48%] h-[40px] md:h-[50px] lg:h-[75px] mb-0 mt-1 md:mt-0 lg:-mb-4 relative inline-block overflow-hidden lg:text-center"
class="relative mb-0 mt-1 inline-block h-[40px] w-full overflow-hidden md:mt-0 md:h-[50px] lg:-mb-4 lg:h-[75px] lg:w-[48%] lg:text-center"
>
{#key titleIndex}
<span
class="absolute left-0 right-[unset] lg:right-0 lg:left-[unset] w-full"
class="absolute left-0 right-[unset] w-full lg:left-[unset] lg:right-0"
style="color: {names[titleIndex].color}"
transition:fly={{ y: 100, delay: 0, easing: sineInOut }}
>
<span class="w-full text-center m-auto">
<span class="m-auto w-full text-center">
{names[titleIndex].label}
</span>
</span>
@@ -115,27 +114,27 @@
<span>from one Platform</span>
</div>
<p class="text-base mx-auto mt-10 px-4 text-slate-700 md:text-lg lg:mt-6 lg:w-[42%] lg:p-0">
<p class="mx-auto mt-10 px-4 ui:text-muted-foreground md:text-lg lg:mt-6 lg:w-[42%] lg:p-0">
ClassroomIO is an open source learning management system for companies. We give you a suite
of tools to help you achieve your training, onboarding and certification goals.
</p>
</div>
<div class="justify-between w-[90%] absolute top-[30%] left-[5%] hidden lg:flex">
<div class="absolute left-[5%] top-[30%] hidden w-[90%] justify-between lg:flex">
{#if animate}
<div
class="relative transition-all hover:scale-110 -rotate-12"
class="relative -rotate-12 transition-all hover:scale-110"
transition:fly={{ x: -100, delay: 100, easing: sineInOut }}
>
<a
href="/#customization"
class="px-6 py-2 border-[1.5px] border-[#0233BD] rounded-[4px] bg-white text-[#0233BD] text-lg font-bold"
class="rounded-[4px] border-[1.5px] border-[#0233BD] bg-white px-6 py-2 text-lg font-bold text-[#0233BD]"
>
Customizable LMS
</a>
<svg
class="absolute -right-1 -bottom-2.5"
class="absolute -bottom-2.5 -right-1"
width="11"
height="11"
viewBox="0 0 11 11"
@@ -146,17 +145,17 @@
</svg>
</div>
<div
class="relative transition-all hover:scale-110 rotate-12"
class="relative rotate-12 transition-all hover:scale-110"
transition:fly={{ y: 100, delay: 150, easing: sineInOut }}
>
<a
href="/#ai"
class="px-6 py-2 border-[1.5px] border-[#9747FF] rounded-[4px] bg-white text-[#9747FF] text-lg font-bold"
class="rounded-[4px] border-[1.5px] border-[#9747FF] bg-white px-6 py-2 text-lg font-bold text-[#9747FF]"
>AI Support</a
>
<svg
class="absolute -right-1 -bottom-2.5"
class="absolute -bottom-2.5 -right-1"
width="11"
height="11"
viewBox="0 0 11 11"
@@ -170,20 +169,12 @@
</div>
<div class="my-10 flex flex-col items-center justify-center gap-4 md:flex-row">
<a
class="md:text-base w-11/12 rounded-md border-gray-200 border-2 px-6 py-3 text-sm font-medium text-center transition-all delay-100 hover:bg-gray-200 hover:shadow-xl hover:scale-95 md:w-fit lg:px-6 lg:py-4 lg:font-semibold"
href="/demo"
rel="noopener noreferrer nofollow"
>
<Button variant="outline" href="/demo" target="_blank" rel="noopener noreferrer nofollow">
Book a demo
</a>
<a
class="md:text-base w-11/12 rounded-md bg-blue-700 px-6 py-3 text-sm font-medium text-white text-center transition-all delay-100 hover:bg-blue-800 hover:shadow-xl hover:scale-95 md:w-fit lg:px-6 lg:py-4 lg:font-semibold"
href="/signup"
rel="noopener noreferrer nofollow"
>
</Button>
<Button href="/signup" target="_blank" rel="noopener noreferrer nofollow">
Sign Up for Free
</a>
</Button>
<!-- <a
class="font-medium lg:font-semibold text-sm md:text-base w-11/12 md:w-fit border px-6 py-3 lg:px-10 lg:py-5 rounded-md"
href="/github"
+22
View File
@@ -0,0 +1,22 @@
// UI Components - Named exports for easy importing
export { default as BlogListItem } from './blog-list-item.svelte';
export { default as CoreFeatureSection } from './core-feature-section.svelte';
export { default as CoreFeatures } from './core-features.svelte';
export { default as CustomImg } from './custom-img.svelte';
export { default as Faq } from './faq.svelte';
export { default as Footer } from './footer.svelte';
export { default as Hero } from './hero.svelte';
export { default as Modal } from './modal.svelte';
export { default as MoreFeatures } from './more-features.svelte';
export { default as Navigation } from './navigation.svelte';
export { default as NotFound } from './not-found.svelte';
export { default as PageHeader } from './page-header.svelte';
export { default as PageSignupCTA } from './page-signup-cta.svelte';
export { default as SenjaEmbed } from './senja-embed.svelte';
export { default as StudentDashboard } from './student-dashboard.svelte';
export { default as Success } from './success.svelte';
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';
@@ -0,0 +1,379 @@
<script lang="ts">
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import Gamepad from '@lucide/svelte/icons/gamepad';
import Hourglass from '@lucide/svelte/icons/hourglass';
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
import LocateFixed from '@lucide/svelte/icons/locate-fixed';
import Menu from '@lucide/svelte/icons/menu';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MousePointerClick from '@lucide/svelte/icons/mouse-pointer-click';
import Timer from '@lucide/svelte/icons/timer';
import X from '@lucide/svelte/icons/x';
import { page } from '$app/state';
import { fly } from 'svelte/transition';
import type { HTMLAttributes } from 'svelte/elements';
import * as NavigationMenu from '@cio/ui/base/navigation-menu';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
export let stars: number;
let showDrawer = false;
let showSolutions = false;
let activeLink = '';
function handleShowDrawer() {
showDrawer = !showDrawer;
}
function handleShowSolutions() {
showSolutions = !showSolutions;
}
const solutions = [
{
key: 'employee-training',
title: 'Employee Training',
subtitle: 'Keep your team in sync.'
},
{
key: 'bootcamps',
title: 'Bootcamps',
subtitle: 'Drive student satisfaction.'
},
{
key: 'customer-education',
title: 'Customer Education',
subtitle: 'Teach customers your product.'
}
];
const freeTools = [
{
key: 'progress',
title: 'Progress Tracker',
subtitle: 'Monitor learning journeys.'
},
{
key: 'pomodoro',
title: 'Pomodoro Timer',
subtitle: 'Boost focus and productivity.'
},
{
key: 'name-picker',
title: 'Name Picker',
subtitle: 'Randomly select names.'
},
{
key: 'stopwatch',
title: 'Activity Stopwatch',
subtitle: 'Track time accurately.'
},
{
key: 'tic-tac-toe',
title: 'Tic Tac Toe',
subtitle: 'Play the classic game.'
}
];
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));
</script>
{#snippet list_item({ title, subtitle, href, key, class: className, ...restProps }: ListItemProps)}
<li>
<NavigationMenu.Link>
{#snippet child()}
<a
{href}
class="flex w-full flex-row items-center rounded-lg p-2 no-underline transition-colors duration-200 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none {className} [&_svg]:stroke-blue-600"
{...restProps}
>
<div class="shrink-0">
{#if key === 'employee-training'}
<LibraryBigIcon size={24} />
{:else if key === 'bootcamps'}
<LocateFixed size={24} />
{:else if key === 'customer-education'}
<MessagesSquare size={24} />
{:else if key === 'progress'}
<LoaderCircle size={24} />
{:else if key === 'pomodoro'}
<Hourglass size={24} />
{:else if key === 'name-picker'}
<MousePointerClick size={24} />
{:else if key === 'stopwatch'}
<Timer size={24} />
{:else if key === 'tic-tac-toe'}
<Gamepad size={24} />
{/if}
</div>
<div class="ml-3 text-start">
<p class="text-sm">
{title}
</p>
<p class="mt-1 text-xs ui:text-muted-foreground">
{subtitle}
</p>
</div>
</a>
{/snippet}
</NavigationMenu.Link>
</li>
{/snippet}
<section class="z-50 fixed top-0 w-full bg-white border-b">
<div class="mx-auto flex w-full items-center justify-between gap-20 py-1 lg:w-[90%] lg:px-0 px-4">
<a href="/" class="w-[10%]">
<div class="flex w-full items-center">
<img loading="lazy" width="28" height="28" src="/logo-512.png" alt="classroomio logo" class="w-6 md:w-7" />
<h1 class="ml-2 text-xs font-medium md:text-base">ClassroomIO</h1>
</div>
</a>
<section class="hidden lg:block">
<NavigationMenu.Root class="rounded-lg bg-white 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"
>
<a href="/" class="no-underline"> Solutions </a>
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<ul class="grid w-[560px] grid-cols-2 gap-2 p-2">
{#each solutions as solution}
{@render list_item({
href: `/${solution.key}`,
title: solution.title,
subtitle: solution.subtitle,
key: solution.key
})}
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<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"
>
<a href="/tools" class="no-underline"> Free Tools </a>
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<ul class="grid w-[560px] grid-cols-2 gap-2 p-2">
{#each freeTools as tool}
{@render list_item({
href: `/tools/${tool.key}`,
title: tool.title,
subtitle: tool.subtitle,
key: tool.key
})}
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link>
{#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'
: ''}"
>
Blog
</a>
{/snippet}
</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link>
{#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'
: ''}"
>
Pricing
</a>
{/snippet}
</NavigationMenu.Link>
</NavigationMenu.Item>
</NavigationMenu.List>
</NavigationMenu.Root>
</section>
<div class="hidden flex-row items-center justify-between gap-3 md:hidden lg:flex">
<a href="/discord" target="_blank" class="flex items-center transition-opacity duration-200 hover:opacity-80">
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="h-5 w-6 cursor-pointer" />
</a>
<div class="flex items-center">
<a
href="/github"
target="_blank"
class="group flex items-center gap-1.5 rounded-md p-2 transition-all duration-200 ease-in-out hover:bg-gray-100"
>
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
class="h-5 w-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span
class="text-sm font-medium leading-none text-gray-600 transition-colors duration-200 group-hover:text-black"
>
{stars}
</span>
</a>
</div>
<a
class="text-sm font-medium transition-opacity duration-200 after:ml-2 after:content-['→'] hover:opacity-80"
href="https://app.classroomio.com"
>
Dashboard
</a>
</div>
<button type="button" aria-label="Hamburger Menu" class="block md:block lg:hidden" on:click={handleShowSolutions}>
<Menu size={24} />
</button>
{#if showSolutions}
<div
in:fly={{ x: 20, duration: 700 }}
out:fly={{ x: 20, duration: 400 }}
class="absolute right-0 top-0 h-screen w-[60%] border-2 bg-white px-3 pb-2 pt-3 sm:block md:w-[40%] md:p-7 lg:hidden"
>
<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}>
<X size={24} />
</button>
</div>
<nav>
<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}
>
Our Superpowers <ChevronDown />
</button>
{#if showDrawer}
<div in:fly={{ y: -20, duration: 700 }} out:fly={{ y: 20, duration: 400 }}>
{#each solutions as solution}
<a
href="/{solution.key}"
on:click={() => {
handleShowSolutions();
}}
>
<p
class="rounded-lg py-2.5 pl-5 text-xs font-normal text-gray-700 transition-colors duration-200 hover:bg-gray-100"
>
{solution.title}
</p>
</a>
{/each}
</div>
{/if}
</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}
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={() => {
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={() => {
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={() => {
handleShowSolutions();
}}
class:active={activeHash.includes('morefeatures')}
href="/#morefeatures"
>
<li>More features</li>
</a>
</ul>
</nav>
<div class="mt-5 flex flex-col items-start gap-y-2 border-t pt-5">
<a
href="/discord"
target="_blank"
class="flex w-full items-center rounded-md px-4 py-4 text-left text-sm transition-all duration-200 hover:bg-gray-100 md:text-lg"
>
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="mr-2 h-5 w-6 cursor-pointer" />
<span>Discord</span>
</a>
<a
href="/github"
target="_blank"
class="group flex w-full items-center rounded-md px-4 py-4 text-left text-sm transition-all duration-200 hover:bg-gray-100 md:text-lg"
>
<img
loading="lazy"
alt="github logo"
src="/github-mark.png"
class="h-5 w-5 cursor-pointer transition-transform duration-200 group-hover:scale-110"
/>
<span class="ml-3 transition-colors duration-200 group-hover:text-black">Github</span>
<span class="ml-1 text-sm font-medium text-gray-600 transition-colors duration-200 group-hover:text-black"
>({stars})</span
>
</a>
<a
class="w-full rounded-md px-4 py-4 text-left text-sm font-semibold transition-all duration-200 after:ml-2 after:content-['→'] hover:bg-gray-100 md:text-lg"
href="https://app.classroomio.com"
>
Dashboard
</a>
</div>
</div>
{/if}
</div>
</section>
<style>
.active {
background-color: #f3f4f6;
border-radius: 6px;
}
</style>
@@ -0,0 +1,33 @@
<script>
import { goto } from '$app/navigation';
import { Empty } from '@cio/ui/custom/empty';
import SearchXIcon from '@lucide/svelte/icons/search-x';
import { Button } from '@cio/ui/base/button';
export let isOrg = false;
export let className = '';
function handleClick() {
if (!isOrg) {
return goto('/');
}
return goto('https://classroomio.com');
}
</script>
<div class="{className} min-h-[80vh] w-screen flex items-center justify-center">
<Empty
title={isOrg ? 'Organization doesn\'t exist!' : 'Page not found'}
description="The page you are looking for doesn't exist or has been moved. Please go back to the homepage."
icon={SearchXIcon}
variant="page"
>
<Button
onclick={handleClick}
>
Go Home
</Button>
</Empty>
</div>
@@ -0,0 +1,7 @@
<script>
export let className = '';
</script>
<header class="hero-bg w-full border-b border-slate-100 pb-16 pt-32 lg:min-h-[20rem] {className}">
<slot />
</header>
@@ -0,0 +1,60 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
interface Props {
header?: string;
subText?: string;
btnLabel?: string;
link?: string;
demo?: boolean;
}
let { header = '', subText = '', btnLabel = '', link = '', demo = true }: Props = $props();
</script>
<div
class="floating grad relative mx-auto my-5 flex h-[61vh] w-full flex-col items-center justify-center px-6 py-12 text-center shadow-2xl lg:h-[50vh] lg:w-[70%] lg:rounded-3xl max-w-4xl space-y-8"
>
<div>
<h3
class="font-display mt-3 text-xl font-semibold 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">
{subText}
</p>
</div>
{#if demo}
<Button
variant="ghost"
data-cal-config="'layout':'month_view'"
data-cal-link="classroomio/demo"
class="bg-white!"
>
{btnLabel}
</Button>
{:else}
<Button
href={link}
variant="link"
target="_blank"
class="ui:bg-secondary ui:text-secondary-foreground"
rel="noopener noreferrer nofollow"
>
{btnLabel}
</Button>
{/if}
</div>
<style>
.grad {
/* background: rgb(2, 51, 189);
background: radial-gradient(circle at 50% 100%, rgba(2, 51, 189, 1) 1%, rgb(4, 2, 56) 100%); */
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
background-image: url('https://brand.cdn.clsrio.com/cta-bg.png');
}
</style>
@@ -1,11 +1,9 @@
<script>
import { Badge } from '@cio/ui/base/badge';
</script>
<section id="student-dashboard" class="text-center text-white py-[10%] px-3 lg:px-0 bg-black">
<div class="mb-10">
<a
href="#student-dashboard"
class="rounded-full px-4 py-2 text-sm font-medium hover:underline bg-blue-700 underline"
><span class="">yourorg</span>.classroomio.com</a
>
</div>
<Badge class="py-2! px-4! underline text-sm!">yourorg.classroomio.com</Badge>
<h1
class="mx-auto text-3xl md:text-4xl lg:text-6xl font-normal leading-[1.5] flex flex-col items-center gap-4"
>
@@ -1,7 +1,7 @@
<script>
import { goto } from '$app/navigation';
import Button from '$lib/Button/Button.svelte';
import { Button } from '@cio/ui/base/button';
</script>
<div
@@ -13,5 +13,5 @@
We'll get back to you as soon as possible. In the meantime, Explore and enjoy features on our
website
</p>
<Button label="Go to Home" className="px-10 md:px-20" onClick={() => goto('/')} />
<Button class="px-10 md:px-20" onclick={() => goto('/')}>Go to Home</Button>
</div>
@@ -0,0 +1,29 @@
<script>
import { SenjaEmbed } from '$lib/components';
import { Button } from '@cio/ui/base/button';
</script>
<section id="testimonial" class="flex flex-col items-center py-[5%] px-3 lg:px-0 space-y-2">
<h1
class="mx-auto text-3xl md:text-4xl lg:text-5xl font-normal leading-normal flex flex-col items-center gap-4 my-0"
>
Hear from Happy ClassroomIO Users
</h1>
<p
class="mx-auto text-center px-4 lg:p-0 text-base tracking-wide leading-6 ui:text-muted-foreground lg:w-2/4"
>
See what excites them about building and managing bootcamps with ClassroomIO.
</p>
<Button
href="/signup"
target="_blank"
rel="noopener noreferrer nofollow"
>
Sign Up for Free
</Button>
<div class="relative my-5 md:my-10 w-[85%] lg:w-[70%]">
<SenjaEmbed id="3e847d7a-13d3-483e-8a71-5b35273dcc6c" />
</div>
</section>
@@ -2,6 +2,6 @@
export let className = '';
</script>
<header class="tools-hero-bg text-center py-14 {className} border-b border-slate-100 bg-white">
<header class="tools-hero-bg py-24 text-center {className} border-b border-slate-100 bg-white">
<slot />
</header>
@@ -1,6 +1,6 @@
<script lang="ts">
import ImageCopy from 'carbon-icons-svelte/lib/ImageCopy.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
import Copy from '@lucide/svelte/icons/copy';
import Trash2 from '@lucide/svelte/icons/trash-2';
export let imageURL = '';
export let label = '';
@@ -35,7 +35,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div>
{#if label}
<p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
<p class="flex w-full items-center justify-between text-left dark:text-white {labelClassName}">
<span>
{label}
{#if isRequired}
@@ -45,13 +45,13 @@
{#if imageURL}
<span title="clear image" on:click={deleteImage}>
<TrashCan class="fill-red-500 " />
<Trash2 class="fill-red-500 " />
</span>
{/if}
</p>
{/if}
<div
class="flex items-center justify-center w-full border rounded-md min-h-[200px] p-4"
class="flex min-h-[200px] w-full items-center justify-center rounded-md border p-4"
on:click={() => fileInput.click()}
>
<input
@@ -63,13 +63,11 @@
required={isRequired}
/>
{#if imageURL}
<img src={imageURL} alt="selected file" class="w-[30%] h-auto mt-4" />
<img src={imageURL} alt="selected file" class="mt-4 h-auto w-[30%]" />
{:else}
<div class="space-y-4">
<ImageCopy size={24} class="mx-auto" />
<p class="text-center text-sm text-gray-500 my-2">
Max file size: 10MB, accepted: jpeg, jpg, png, gif
</p>
<Copy size={24} class="mx-auto" />
<p class="my-2 text-center text-sm text-gray-500">Max file size: 10MB, accepted: jpeg, jpg, png, gif</p>
</div>
{/if}
</div>
-2
View File
@@ -1,2 +0,0 @@
import img from './img.svelte';
export { img };
@@ -0,0 +1,87 @@
<script>
import { 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 { isFormValid } from '$lib/utils/is-form-valid';
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
const handleClick = async () => {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[FEEDBACK] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
};
</script>
<div>
{#if sent}
<Success />
{:else}
<div class="w-full my-10 space-y-5">
<Field.Group>
<div class="flex flex-col md:flex-row gap-10">
<Field.Field class="w-full">
<Field.Label for="feedback-name">Your Name</Field.Label>
<Input id="feedback-name" bind:value={name} required />
</Field.Field>
<Field.Field class="w-full">
<Field.Label for="feedback-email">Your Email</Field.Label>
<Input id="feedback-email" type="email" bind:value={email} required />
</Field.Field>
</div>
<Field.Field>
<Field.Label for="feedback-subject">Subject</Field.Label>
<Input id="feedback-subject" bind:value={title} required />
</Field.Field>
<Field.Field>
<Field.Label for="feedback-message">Message</Field.Label>
<Textarea id="feedback-message" bind:value={description} required />
</Field.Field>
{#if !!errorMessage}
<Field.Field>
<Field.Error>{errorMessage}</Field.Error>
</Field.Field>
{/if}
</Field.Group>
<Button
onclick={handleClick}
disabled={isLoading}
loading={isLoading}
>
Submit Feedback
</Button>
</div>
{/if}
</div>
@@ -0,0 +1,87 @@
<script>
import { 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 { isFormValid } from '$lib/utils/is-form-valid';
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
async function handleClick() {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[HELP] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
}
</script>
<div>
{#if sent}
<Success />
{:else}
<div class="w-full my-10 space-y-5">
<Field.Group>
<div class="flex flex-col md:flex-row gap-10">
<Field.Field class="w-full">
<Field.Label for="help-name">Your Name</Field.Label>
<Input id="help-name" bind:value={name} required />
</Field.Field>
<Field.Field class="w-full">
<Field.Label for="help-email">Your Email</Field.Label>
<Input id="help-email" type="email" bind:value={email} required />
</Field.Field>
</div>
<Field.Field>
<Field.Label for="help-subject">Subject</Field.Label>
<Input id="help-subject" bind:value={title} required />
</Field.Field>
<Field.Field>
<Field.Label for="help-message">Message</Field.Label>
<Textarea id="help-message" bind:value={description} required />
</Field.Field>
{#if !!errorMessage}
<Field.Field>
<Field.Error>{errorMessage}</Field.Error>
</Field.Field>
{/if}
</Field.Group>
<Button
onclick={handleClick}
disabled={isLoading}
loading={isLoading}
>
Submit Request
</Button>
</div>
{/if}
</div>
@@ -0,0 +1,82 @@
<script lang="ts">
import { openModal, htmlBody } from './store';
import * as Dialog from '@cio/ui/base/dialog';
interface Avatar {
src: string;
}
let isAddIcon = false;
let hoveredIndex: number | null = null;
let avatars: Avatar[] = [
{ src: 'avatar_a' },
{ src: 'avatar_b' },
{ src: 'avatar_c' },
{ src: 'avatar_d' },
{ src: 'avatar_e' },
{ src: 'avatar_f' },
{ src: 'avatar_g' },
{ src: 'avatar_h' },
{ src: 'avatar_i' },
{ src: 'avatar_j' },
{ src: 'avatar_k' },
{ src: 'avatar_l' },
{ src: 'avatar_m' },
{ src: 'avatar_n' },
{ src: 'avatar_o' },
{ src: 'avatar_p' },
{ src: 'avatar_q' },
{ src: 'avatar_r' },
{ src: 'avatar_s' },
{ src: 'avatar_t' }
];
function selectAvatar(avatar: Avatar) {
$htmlBody.avatar = avatar.src;
$openModal.avatar = false;
}
</script>
<Dialog.Root bind:open={$openModal.avatar}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Choose your Avatars</Dialog.Title>
</Dialog.Header>
<!-- map for each avatar -->
<div class="flex flex-wrap justify-evenly gap-y-3 mt-3">
{#each avatars as avatar, index}
<button
on:click={() => {
selectAvatar(avatar);
}}
on:mouseenter={() => {
hoveredIndex = index;
isAddIcon = true;
}}
on:mouseleave={() => {
hoveredIndex = null;
isAddIcon = false;
}}
class="relative w-[17%] flex justify-center items-center shadow-sm rounded-full hover:scale-110 transition-all duration-300"
>
<!-- + icon overlay -->
{#if isAddIcon && hoveredIndex === index}
<div
class="w-full h-full flex justify-center items-center bg-white border absolute rounded-full opacity-[0.7] blur-md"
></div>
<img src="/free-tools/progress-report/hover-plus-icon.svg" alt="" class="absolute" />
{/if}
<!-- avatar -->
<img
src="https://assets.cdn.clsrio.com/progress-report/avatar/{avatar.src}.svg"
alt=""
class="w-full"
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,81 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import { htmlBody, openModal } from './store';
interface Background {
src: string;
}
let backgrounds: Background[] = [
{
src: 'blue_tetiary_background'
},
{
src: 'blue_secondary_background'
},
{
src: 'blue_primary_background'
},
{
src: 'purple_tetiary_background'
},
{
src: 'purple_secondary_background'
},
{
src: 'purple_primary_background'
},
{
src: 'black_tetiary_background'
},
{
src: 'black_secondary_background'
},
{
src: 'black_primary_background'
},
{
src: 'yellow_tetiary_background'
},
{
src: 'yellow_secondary_background'
},
{
src: 'yellow_primary_background'
}
];
function selectBackground(background: Background) {
$htmlBody.background = background.src;
$openModal.background = false;
}
</script>
<Dialog.Root bind:open={$openModal.background}>
<Dialog.Content class="h-full max-h-[60vh]">
<Dialog.Header>
<Dialog.Title>Choose your Background</Dialog.Title>
</Dialog.Header>
<!-- -->
<div
class="flex flex-wrap justify-between gap-y-3 mt-3 overflow-y-auto overflow-x-hidden h-[85%]"
>
{#each backgrounds as background}
<button
on:click={() => {
selectBackground(background);
}}
class="flex w-[30%] items-center shadow-sm hover:scale-110 transition-all duration-300"
>
<img
src="https://assets.cdn.clsrio.com/progress-report/backgrounds/{background.src}.webp"
alt=""
class=""
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,51 @@
<script lang="ts">
import { toPng } from 'html-to-image';
import { nodeStore, htmlBody, openModal } from './store';
import { Button } from '@cio/ui/base/button';
export let isDisabled: boolean;
export let isDownloading: boolean;
export let text: string = 'Generate Progress Report';
function convertToPng() {
isDownloading = true;
$openModal.showFullscreenButton = false;
const fileName = `${$htmlBody.name} - ${$htmlBody.mood.text} (ClassroomIO_com)`;
if ($nodeStore) {
setTimeout(() => {
toPng($nodeStore, {
width: 500,
height: 500,
quality: 1
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = fileName;
link.href = dataUrl;
link.click();
})
.catch((error) => {
console.error('Oops, something went wrong!', error);
})
.finally(() => {
isDownloading = false;
$openModal.showFullscreenButton = false;
});
}, 300);
} else {
console.error('Node is not defined');
}
}
</script>
<Button
type="button"
disabled={isDownloading || !isDisabled}
onclick={convertToPng}
loading={isDownloading}
class="{text === 'Generate Progress Report' ? 'block md:hidden' : ''} mt-5 w-full"
>
{isDownloading ? 'Loading...' : text}
</Button>
@@ -0,0 +1,13 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import Report from './report.svelte';
import { openModal } from './store';
</script>
<Dialog.Root bind:open={$openModal.fullscreen}>
<Dialog.Content showCloseButton={false}>
<!-- -->
<Report />
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,94 @@
<script lang="ts">
import * as Dialog from '@cio/ui/base/dialog';
import { htmlBody, openModal } from './store';
interface Mood {
text: string;
iconSrc: string;
}
let moods: Mood[] = [
{
text: 'Unmotivated',
iconSrc: 'unmotivated'
},
{
text: 'Feeling Enlightened',
iconSrc: 'feeling-enlightened'
},
{
text: 'Enthusiastic',
iconSrc: 'enthusiastic'
},
{
text: 'Confused',
iconSrc: 'confused'
},
{
text: 'Feeling stuck',
iconSrc: 'feeling-stuck'
},
// {
// text: 'Believing in the process',
// iconSrc: 'believing-in-the-process'
// },
{
text: 'Curious',
iconSrc: 'curious'
},
{
text: 'Achieving Mastery',
iconSrc: 'achieving-mastery'
},
{
text: 'Feeling Positive',
iconSrc: 'feeling-positive'
},
{
text: 'Reflecting',
iconSrc: 'reflecting'
},
{
text: 'Making progress',
iconSrc: 'progress'
},
{
text: 'Feeling Sad',
iconSrc: 'sad'
}
];
function selectMood(mood: Mood) {
$htmlBody.mood.text = mood.text;
$htmlBody.mood.iconSrc = mood.iconSrc;
$openModal.mood = false;
}
</script>
<Dialog.Root bind:open={$openModal.mood}>
<Dialog.Content class="w-full md:w-[30%] md:min-w-[500px]">
<Dialog.Header>
<Dialog.Title>Choose your Mood</Dialog.Title>
</Dialog.Header>
<!-- mapping -->
<div class="flex flex-wrap justify-between gap-y-3 mt-3">
{#each moods as mood}
<button
on:click={() => {
selectMood(mood);
}}
class="flex gap-3 items-center shadow-sm rounded-full px-4 py-1 bg-[#F1F6FF] hover:bg-[#0233BD] hover:text-white transition-all duration-700"
>
<p class="text-xs md:text-sm font-semibold">{mood.text}</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{mood.iconSrc}.png"
alt={mood.text}
class="w-4"
/>
</button>
{/each}
</div>
<!-- -->
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,142 @@
<script lang="ts">
import { htmlBody, nodeStore, openModal } from './store';
import { derived } from 'svelte/store';
const colorMap = {
blue_tetiary_background: '#0542CC',
blue_secondary_background: '#0542CC',
blue_primary_background: '#0542CC',
purple_tetiary_background: '#9D4EDD',
purple_secondary_background: '#9D4EDD',
purple_primary_background: '#9D4EDD',
black_tetiary_background: '#252525',
black_secondary_background: '#252525',
black_primary_background: '#252525',
yellow_tetiary_background: '#FFBC42',
yellow_secondary_background: '#FFBC42',
yellow_primary_background: '#FFBC42'
};
type ColorMapKey = keyof typeof colorMap;
const backgroundColor = derived(htmlBody, ($htmlBody) => {
return colorMap[$htmlBody.background as ColorMapKey] || '#1e5ed2';
});
let node: any;
$: nodeStore.set(node);
$: rootBgImage = `https://assets.cdn.clsrio.com/progress-report/backgrounds/${
$htmlBody.background || 'blue_tetiary_background'
}.webp`;
</script>
<div
class="h-full min-h-[500px] max-h-[500px] md:h-[60vh] w-full border relative flex items-center justify-center"
style={`font-family: "Space Grotesk", sans-serif; background-image: url('${rootBgImage}'); background-size: cover; background-repeat: no-repeat`}
bind:this={node}
>
<!-- fullscreen button -->
{#if $openModal.showFullscreenButton}
{#if $openModal.fullscreen}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 p-1.5 rounded-full hover:scale-90 transition-all duration-300 bg-white"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="" class="" />
</button>
{:else}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 w-7 hover:scale-90 transition-all duration-300"
>
<img src="/free-tools/progress-report/full-screen-icon.svg" alt="" class="" />
</button>
{/if}
{/if}
<div class="mt-20">
<!-- using both md & lg for the width here because the lib doesn't consider md to be medium screens and above, using md alone will make the output break on desktop so i had to also specify for large screens -->
<div
class="rounded-lg shadow-md bg-white border-4 border-gray-100 z-20 mx-auto flex justify-center items-center w-[16rem] h-[16rem]"
>
<div class="relative w-full h-full flex flex-col justify-center gap-3 items-center px-2">
<!-- avatar -->
<img
src={`https://assets.cdn.clsrio.com/progress-report/avatar/${
$htmlBody.avatar || 'avatar_a'
}.svg`}
alt=""
class="absolute top-0 w-[25%] h-[25%] ml-2 -mt-[2.2rem]"
/>
<!-- mood -->
{#if $htmlBody.name || $htmlBody.mood.text}
<div
style="background-color: {$backgroundColor}; color: white; display: flex;"
class="px-3 py-0.5 border border-[#EDEDED] rounded-2xl mt-6 md:mt-3 font-semibold text-[10px] md:text-[0.7rem] flex items-center justify-between"
>
<span class="mr-0.5">{$htmlBody.name}</span>
{#if $htmlBody.mood.text}
<span class="flex gap-1 items-center">
<p class="font-semibold text-[10px] md:text-[0.7rem]">
is {$htmlBody.mood.text}
</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{$htmlBody.mood
.iconSrc}.png"
alt={$htmlBody.mood.text}
class="w-3"
/>
</span>
{/if}
</div>
{:else}
<p
style="background-color: {$backgroundColor}"
class="px-3 py-0.5 border border-[#EDEDED] text-white rounded-2xl mt-3 font-medium text-[9px]"
>
Add your name
</p>
{/if}
<!-- learning -->
<span
class="w-full md:my-2 p-1 overflow-hidden text-[0.7rem] md:text-[0.8rem] font-normal text-center outline-none h-[10vh] md:h-[12vh]"
>
{$htmlBody.learning}
</span>
<!-- progress -->
<div class="flex flex-col justify-center items-center mb-2 md:mb-0">
<div class="border border-[#d9e0f5] w-full h-4 rounded-full p-0.5 bg-[#f1f6ff]">
<div
class="progress-range rounded-full w-full h-full"
style="background: {$backgroundColor}; width: {$htmlBody.progress}%"
/>
</div>
<p class="text-[12px] font-semibold mt-1">Progress Achieved: {$htmlBody.progress}%</p>
</div>
</div>
</div>
<div
style="display: flex;"
class="border mt-20 bottom-5 w-[50%] mx-auto flex gap-1 items-center justify-center bg-white rounded-md px-2 py-1"
>
<img src="/logo-192.png" alt="classroomio logo" class="w-4" />
<p class="text-[10px] font-bold">ClassroomIO.com</p>
</div>
<!-- -->
</div>
</div>
<!-- importing font family -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:[email protected]&display=swap');
</style>
@@ -0,0 +1,57 @@
import { derived, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
interface OpenModal {
avatar: boolean;
background: boolean;
fullscreen: boolean;
showFullscreenButton: boolean;
mood: boolean;
}
export interface HtmlBody {
name: string;
learning: string;
progress: number;
avatar: string;
background: string;
mood: {
text: string;
iconSrc: string;
};
}
// store for the node binded to the report component for the component to image conversion
export const nodeStore = writable(null);
export const openModal: Writable<OpenModal> = writable({
avatar: false,
background: false,
fullscreen: false,
showFullscreenButton: false,
mood: false
});
// report html
export const htmlBody: Writable<HtmlBody> = writable({
name: '',
learning: '',
progress: 10,
avatar: '',
background: '',
mood: {
text: '',
iconSrc: ''
}
});
// a mini validation for the htmlBody store that ensures all fields are filled
export const isFormComplete = derived(htmlBody, ($htmlBody) => {
return !!(
$htmlBody.name.trim() &&
$htmlBody.learning.trim() &&
$htmlBody.progress > 0 &&
$htmlBody.mood.text.trim() &&
$htmlBody.mood.iconSrc.trim()
);
});
@@ -0,0 +1,70 @@
<script lang="ts">
import { sineInOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let activityName: string;
export let formattedTime: string;
export let displayTime: string;
export let resetTimer: () => void;
export let toggleSound: () => void;
export let toggleTimer: () => void;
export let restartTimer: () => void;
export let isPaused: boolean;
export let soundOn: boolean;
</script>
<div transition:fly={{ y: 100, delay: 0, easing: sineInOut }}>
<div class="px-6 pt-8 text-center">
<button
type="button"
on:click={resetTimer}
class="text-[10px] text-[#656565] font-semibold uppercase underline"
>set timer for another activity</button
>
<button
class="border mx-auto mt-7 mb-3 px-2 py-0.5 rounded-full flex items-center gap-1 text-sm text-[#656565] font-semibold border-[#C2D2FF] bg-[#DCE5FF]"
>
{activityName}:
<span class="text-[#0233BD]">{formattedTime}</span>
</button>
<!-- countdown -->
<h1 class="text-[#040F2D] mb-14 text-6xl font-bold">{displayTime}</h1>
</div>
<div class="flex">
<button
type="button"
on:click={toggleSound}
class="bg-[#F7F7F7] w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10 rounded-bl-md hover:bg-[#F1F6FF] transition-all duration-500"
>
{#if soundOn}
<img src="/free-tools/stopwatch/speaker-icon.svg" alt="" class="w-7 md:w-12" />
{:else}
<img src="/free-tools/stopwatch/no-speaker-icon.svg" alt="" class="w-7 md:w-12" />
{/if}
buzzer</button
>
<button
type="button"
on:click={toggleTimer}
class="bg-[#0F62FE] text-white w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10"
>
{#if isPaused}
<img src="/free-tools/stopwatch/play-icon.svg" alt="" class="w-7 md:w-12" />
{:else}
<img src="/free-tools/stopwatch/pause-icon.svg" alt="" class="w-7 md:w-12" />
{/if}
{isPaused ? 'play' : 'pause'}</button
>
<button
type="button"
on:click={restartTimer}
class="bg-[#F7F7F7] w-[34%] flex flex-col gap-1 items-center uppercase text-[10px] py-5 md:py-10 rounded-br-md hover:bg-[#F1F6FF] transition-all duration-500"
>
<img src="/free-tools/stopwatch/reset-icon.svg" alt="" class="w-7 md:w-12" />
restart</button
>
</div>
</div>
@@ -0,0 +1,99 @@
<script lang="ts">
import { sineInOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import WinningLine from './winning-line.svelte';
import { changeToComputer } from './store';
interface Game {
board: Array<Array<string>>;
click: (rowIndex: number, columnIndex: number) => Game;
}
export let game: Game;
export let winner = null;
let customDelay: number;
$: {
if ($changeToComputer.change) {
customDelay = 400;
} else {
customDelay = 0;
}
}
</script>
<div class="relative">
<table>
<tbody>
{#each game.board as row, rowIndex}
<tr>
{#each row as column, columnIndex}
<td
class="cursor-pointer text-center h-25 w-[1.5em] border-r-10 border-b-10 border-white last:border-r-0"
on:click={() => {
game = game.click(rowIndex, columnIndex);
}}
>
{#if column === 'X'}
<img
transition:fly={{ y: 100, delay: 0, easing: sineInOut }}
src="/free-tools/tic-tac/tic-tac-x.svg"
alt="X icon"
/>
{:else if column === 'O'}
<img
transition:fly={{ y: 100, delay: customDelay, easing: sineInOut }}
src="/free-tools/tic-tac/tic-tac-o.svg"
alt="O icon"
/>
{:else}
<!-- empty cell -->
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<WinningLine {winner} />
</div>
<style>
table {
overflow: hidden;
border-collapse: collapse;
font-size: 4em;
color: white;
border: 0px;
z-index: 10;
position: relative;
}
td {
overflow: hidden;
text-align: center;
height: 100px;
width: 100px;
border-right: 10px solid white;
border-bottom: 10px solid white;
position: relative;
z-index: 10;
}
td:last-child {
border-right: none;
}
tr:last-child td {
border-bottom: none;
}
@media (max-width: 600px) {
td {
width: 16vw;
height: 16vw;
}
}
</style>
@@ -0,0 +1,178 @@
import { scores, changeToComputer } from './store';
let activateComp = false;
changeToComputer.subscribe((value) => {
if (value && value.change === true) {
activateComp = true;
} else {
activateComp = false;
}
});
export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void, onTie: () => void }) {
const board = [
['', '', ''],
['', '', ''],
['', '', '']
];
let turn = 'X';
let done = false;
return {
board,
click(rowIndex:number, columnIndex:number) {
if (!done && !board[rowIndex][columnIndex]) {
board[rowIndex][columnIndex] = turn;
turn = turn === 'X' ? 'O' : 'X';
const winner = getWinner(board);
if (winner) {
onWin(winner);
done = true;
} else if (isBoardFull(board)) {
onTie();
scores.update((currentScores) => {
return { ...currentScores, draw: currentScores.draw + 1 };
});
done = true;
} else {
if (activateComp) {
const aiMove = getBestMove(board);
if (aiMove) {
board[aiMove.row][aiMove.column] = 'O';
turn = 'X';
const aiWinner = getWinner(board);
if (aiWinner) {
onWin(aiWinner);
done = true;
} else if (isBoardFull(board)) {
onTie();
scores.update((currentScores) => {
return { ...currentScores, draw: currentScores.draw + 1 };
});
done = true;
}
} else {
console.error("AI could not find a valid move.");
}
}
}
}
return this;
}
};
}
function getWinner(board: string[][]) {
// check rows
for (let row = 0; row < 3; row++) {
if (board[row][0] === board[row][1] && board[row][0] === board[row][2] && board[row][0]) {
return { symbol: board[row][0], direction: 'horizontal', row };
}
}
// check columns
for (let column = 0; column < 3; column++) {
if (
board[0][column] === board[1][column] &&
board[0][column] === board[2][column] &&
board[0][column]
) {
return { symbol: board[0][column], direction: 'vertical', column };
}
}
// check diagonals
if (board[0][0] === board[1][1] && board[0][0] == board[2][2] && board[0][0]) {
return { symbol: board[0][0], direction: 'diagonal', diagonal: 'top-left-to-bottom-right' };
}
if (board[0][2] === board[1][1] && board[0][2] == board[2][0] && board[0][2]) {
return { symbol: board[0][2], direction: 'diagonal', diagonal: 'top-right-to-bottom-left' };
}
// no winner
return null;
}
function isBoardFull(board: string[][]) {
for (let row = 0; row < 3; row++) {
for (let column = 0; column < 3; column++) {
if (!board[row][column]) {
return false;
}
}
}
return true;
}
function getBestMove(board: string[][]) {
let bestScore = -Infinity;
let move;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
// check if cell is empty
if (board[i][j] === '') {
board[i][j] = 'O'; // make the move
let score = minimax(board, 0, false); // call minimax on the new board
board[i][j] = ''; // undo the move
if (score > bestScore) {
bestScore = score;
move = { row: i, column: j };
}
}
}
}
return move;
}
function minimax(board: string[][], depth: number, isMaximizing: boolean) {
const result = getWinner(board);
if (result !== null) {
if (result.symbol === 'O') {
return 10 - depth; // ai wins
} else if (result.symbol === 'X') {
return depth - 10; // player wins
} else {
return 0; // draw
}
}
if (isMaximizing) {
let bestScore = -Infinity;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i][j] === '') {
board[i][j] = 'O';
let score = minimax(board, depth + 1, false);
board[i][j] = '';
bestScore = Math.max(score, bestScore);
}
}
}
return bestScore;
} else {
let bestScore = Infinity;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i][j] === '') {
board[i][j] = 'X';
let score = minimax(board, depth + 1, true);
board[i][j] = '';
bestScore = Math.min(score, bestScore);
}
}
}
return bestScore;
}
}
@@ -0,0 +1,22 @@
import { writable } from 'svelte/store';
import type {Writable} from 'svelte/store'
interface Scores {
X: number;
O: number;
draw: number;
}
interface ChangeToComputer {
change: boolean;
}
export const scores: Writable<Scores> = writable({
X: 0,
O: 0,
draw: 0
});
export const changeToComputer: Writable<ChangeToComputer> = writable({
change: false
});
@@ -0,0 +1,80 @@
<script lang="ts">
import { fade } from 'svelte/transition';
export let winner: Winner | null = null;
interface Winner {
direction: 'horizontal' | 'vertical' | 'diagonal';
row?: number;
column?: number;
diagonal?: 'top-left-to-bottom-right' | 'top-right-to-bottom-left';
}
$: if (winner) {
if (winner.direction === 'vertical' && winner.column !== undefined) {
document.documentElement.style.setProperty('--col-index', winner.column.toString());
} else if (winner.direction === 'horizontal' && winner.row !== undefined) {
document.documentElement.style.setProperty('--row-index', winner.row.toString());
}
}
</script>
{#if winner}
<div
transition:fade={{ delay: 500, duration: 2 }}
class="winning-line absolute bg-[#ffc300] z-[100]"
class:horizontal-line={winner.direction === 'horizontal'}
class:vertical-line={winner.direction === 'vertical'}
class:diagonal-line-1={winner.direction === 'diagonal' &&
winner.diagonal === 'top-left-to-bottom-right'}
class:diagonal-line-2={winner.direction === 'diagonal' &&
winner.diagonal === 'top-right-to-bottom-left'}
></div>
{/if}
<style>
.horizontal-line {
top: calc(var(--row-index) * 100px + 50px - 5px);
left: 0;
width: 100%;
height: 10px;
}
.vertical-line {
top: 0;
left: calc(var(--col-index) * 100px + 50px - 5px);
width: 10px;
height: 100%;
}
.diagonal-line-1 {
top: 0;
left: 0;
width: 140%;
height: 100%;
background: none;
border-top: 10px solid #ffc300;
transform: rotate(45deg);
transform-origin: top left;
}
.diagonal-line-2 {
top: 0;
right: 0;
width: 140%;
height: 100%;
background: none;
border-top: 10px solid #ffc300;
transform: rotate(-45deg);
transform-origin: top right;
}
@media (max-width: 600px) {
.horizontal-line {
top: calc(var(--row-index) * 16vw + 8vw - 5px);
}
.vertical-line {
left: calc(var(--col-index) * 16vw + 8vw - 5px);
}
}
</style>
+4
View File
@@ -0,0 +1,4 @@
// Re-export all components for convenient importing
// Usage: import { Footer, Navigation, Hero } from '$lib/components'
export * from './components';