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:
co-authored by
Rotimi Best
parent
690cf147ff
commit
e6a3cf9332
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 Open Source Platform for Tech 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>
|
||||
@@ -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>
|
||||
+3
-3
@@ -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
-1
@@ -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 Open Source Platform for Tech 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>
|
||||
+30
-39
@@ -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"
|
||||
@@ -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>
|
||||
+5
-7
@@ -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"
|
||||
>
|
||||
+2
-2
@@ -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>
|
||||
+1
-1
@@ -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>
|
||||
+8
-10
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
// Re-export all components for convenient importing
|
||||
// Usage: import { Footer, Navigation, Hero } from '$lib/components'
|
||||
export * from './components';
|
||||
|
||||
Reference in New Issue
Block a user