feat: setup drizzle (#594)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
const benefitDetails = [
|
||||
{
|
||||
icon: 'ai.svg',
|
||||
title: 'AI-Powered Learning',
|
||||
checkMark: 'check.svg',
|
||||
points: [
|
||||
'Create engaging courses with AI-assisted content generation.',
|
||||
'Automate grading and feedback processes, saving time and effort.',
|
||||
'Personalize learning experiences based on individual student needs.'
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'community.svg',
|
||||
title: 'Foster a Thriving Community',
|
||||
checkMark: 'check.svg',
|
||||
points: [
|
||||
'Create a supportive learning environment with built-in forums and interactive tools.',
|
||||
'Encourage collaboration and knowledge sharing among students.',
|
||||
'Build a strong community of learners and mentors.'
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'operations.svg',
|
||||
title: 'Streamline Operations',
|
||||
checkMark: 'check.svg',
|
||||
points: [
|
||||
'Manage your courses and students efficiently with our user-friendly platform.',
|
||||
'Issue custom certificates to validate student achievements.',
|
||||
'Provide a seamless learning experience for your students.'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const featureDetails = [
|
||||
{
|
||||
icon: 'certificate-i.svg',
|
||||
img: 'certificate.svg',
|
||||
title: 'Certificates',
|
||||
description:
|
||||
'We provide pre built certificate templates anyone can choose from and provide to their students right within our dashboard'
|
||||
},
|
||||
{
|
||||
icon: 'forum-i.svg',
|
||||
img: 'forum.svg',
|
||||
title: 'Forum area for question and answer',
|
||||
description:
|
||||
'Our community section allows for a robust question and answer experience encouraging collaboration between students. This also encourages knowledge sharing amongst students.'
|
||||
},
|
||||
{
|
||||
icon: 'ai.svg',
|
||||
img: 'ai-pic.svg',
|
||||
title: 'AI Powered Content creation',
|
||||
description:
|
||||
'We give you the ability to create courses with AI, upload videos, embed slides, create quizzes and even grade quizzes with AI.'
|
||||
},
|
||||
{
|
||||
icon: 'free-tools-i.svg',
|
||||
img: 'free-tools.svg',
|
||||
title: 'Free tools for students',
|
||||
description:
|
||||
'We provide a suite of free tools like Pomodoro timer, Random name picker, and a Progress report generator to make the lives of educators and students much easier.',
|
||||
ctaLabel: 'Try it out',
|
||||
ctaLink: '/tools'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Bootcamps | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="flex flex-col justify-center items-center">
|
||||
<PageHeader
|
||||
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div class="bg-[#DCE5FF] py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base text-[#4B5563] text-center font-medium">
|
||||
Solution for Bootcamps
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-8"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1
|
||||
class="mx-auto m-0 text-4xl lg:text-7xl md:text-6xl font-bold text-slate-900 lg:text-center"
|
||||
>
|
||||
The platform for fast
|
||||
<span class="text-blue-700 relative"> growing bootcamps</span>
|
||||
</h1>
|
||||
<p class="lg:w-[60%] text-base md:text-lg lg:text-center font-normal text-slate-700">
|
||||
From AI powered course creation to student collaboration tools, ClassroomIO provides
|
||||
everything you need to run a cutting-edge, high-impact bootcamp.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/signup"
|
||||
target="_blank"
|
||||
class="py-5 px-6 bg-[#1D4ED8] border-transparent outline-transparent rounded-lg text-white text-lg font-semibold"
|
||||
>
|
||||
Get Started for Free 🚀
|
||||
</a>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<section
|
||||
class="w-full lg:py-24 py-16 lg:px-28 md:px-16 px-6 flex flex-col lg:items-center lg:justify-between justify-center gap-y-12"
|
||||
>
|
||||
<div
|
||||
class="w-full gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
|
||||
>
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base font-medium">Our benefits</p>
|
||||
</div>
|
||||
<div
|
||||
class="lg:w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Inspire Student Success</h1>
|
||||
<p class="text-[#334155] text-sm md:text-base font-light lg:text-center md:w-[80%]">
|
||||
Scale your bootcamp with ClassroomIO's comprehensive toolkit. Create engaging content,
|
||||
foster community, and drive student achievement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex lg:flex-row flex-col lg:justify-between justify-start gap-6 lg:items-center items-start"
|
||||
>
|
||||
{#each benefitDetails as benefits}
|
||||
<div class="w-full md:p-6 p-4 border border-[#D9E0F5] rounded-2xl flex flex-col gap-y-6">
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="https://assets.cdn.clsrio.com/www/bootcamps/{benefits.icon}"
|
||||
alt="brain"
|
||||
/>
|
||||
|
||||
<div class="w-full flex flex-col justify-start align-start gap-y-2">
|
||||
<h1 class="text-xl font-medium">{benefits.title}</h1>
|
||||
|
||||
{#each benefits.points as point}
|
||||
<div class="flex flex-row justify-start items-start gap-x-3">
|
||||
<img
|
||||
src="https://assets.cdn.clsrio.com/www/bootcamps/{benefits.checkMark}"
|
||||
alt="check mark"
|
||||
/>
|
||||
<p class="leading-8 text-gray-500 font-[#656565] font-light">
|
||||
{point}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="w-full bg-[#F1F6FF] flex flex-col items-center justify-center gap-y-16 lg:py-24 py-16 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-screen-xl gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
|
||||
>
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base font-medium">Explore our features</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Why We're the Perfect Fit</h1>
|
||||
<p class="text-[#334155] text-sm md:text-base font-light lg:text-center md:w-[80%]">
|
||||
Discover why we’re the ideal partner for your bootcamp training needs. Experience
|
||||
innovative solutions designed to elevate student engagement and maximize learning
|
||||
outcomes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col justify-start items-center lg:gap-y-24 gap-y-16"
|
||||
>
|
||||
{#each featureDetails as feature, i}
|
||||
<div
|
||||
class="w-full lg:w-[70%] flex lg:flex-row {i % 2 !== 0
|
||||
? 'lg:flex-row-reverse'
|
||||
: ''} flex-col lg:justify-between justify-between lg:items-center items-start gap-10"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm lg:w-[40%] lg:gap-y-6 gap-y-3 flex flex-col items-start justify-start"
|
||||
>
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="https://assets.cdn.clsrio.com/www/bootcamps/{feature.icon}"
|
||||
alt="team icon"
|
||||
/>
|
||||
<div class="md:w-full flex flex-col items-start justify-start gap-y-4">
|
||||
<h1 class="font-bold text-4xl">{feature.title}</h1>
|
||||
<p class="leading-8 text-lg text-gray-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if feature.ctaLink}
|
||||
<a
|
||||
class="py-4 px-8 border border-[#1D4ED8] outline-transparent rounded text-[#1D4ED8] text-sm font-semibold"
|
||||
href={feature.ctaLink}
|
||||
target="_blank noopener noreferrer"
|
||||
>
|
||||
{feature.ctaLabel}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
class="py-4 px-8 border border-[#1D4ED8] outline-transparent rounded text-[#1D4ED8] text-sm font-semibold"
|
||||
data-cal-config="'layout':'month_view'"
|
||||
data-cal-link="classroomio/demo"
|
||||
>
|
||||
Book a Demo
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
class="w-full lg:max-w-[380px]"
|
||||
src="https://assets.cdn.clsrio.com/www/bootcamps/{feature.img}"
|
||||
alt="team management feature ui"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Why spend time and money on building your own platform?"
|
||||
subText="Invest that time and money back into giving your students the best experience possible."
|
||||
btnLabel="Sign me up"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,213 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
const benefitDetails = [
|
||||
{
|
||||
icon: 'personalised.svg',
|
||||
title: 'Personalized Learning',
|
||||
description: 'Offer branded landing pages and multilingual courses for diverse customers.'
|
||||
},
|
||||
{
|
||||
icon: 'structured.svg',
|
||||
title: 'Structured Education',
|
||||
description: 'Create engaging courses with AI-assisted content generation.'
|
||||
},
|
||||
{
|
||||
icon: 'simplified.svg',
|
||||
title: 'Simplified Management',
|
||||
description:
|
||||
'Centralize and easily manage, share, and distribute all customer education materials.'
|
||||
}
|
||||
];
|
||||
|
||||
const featureDetails = [
|
||||
{
|
||||
icon: 'landing-i.svg',
|
||||
img: 'landing.svg',
|
||||
title: 'Dedicated landing page',
|
||||
description:
|
||||
'We give you a customisable landing page that can be shared with your customers who are interested in learning about your products'
|
||||
},
|
||||
{
|
||||
icon: 'link.svg',
|
||||
img: 'domain.svg',
|
||||
title: 'Custom domains',
|
||||
description:
|
||||
'Use your own domain to link to our landing page instead of using our free subdomain'
|
||||
},
|
||||
{
|
||||
icon: 'globe.svg',
|
||||
img: 'language.svg',
|
||||
title: 'Multi language support',
|
||||
description:
|
||||
'We support 6 languages and you can deploy courses in different languages to support different demographics'
|
||||
},
|
||||
{
|
||||
icon: 'paths.svg',
|
||||
img: 'pathways.svg',
|
||||
title: 'Pathways',
|
||||
description:
|
||||
'With the introduction of pathways, you can group multiple courses into one comprehensive learning pathway'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Customer Education | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="flex flex-col justify-center items-center">
|
||||
<PageHeader
|
||||
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div class="bg-[#DCE5FF] py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base text-[#4B5563] text-center font-medium">
|
||||
Solution for Customer Education
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-8"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1
|
||||
class="w-[95%] mx-auto m-0 text-4xl lg:text-7xl md:text-6xl font-bold text-slate-900 lg:text-center"
|
||||
>
|
||||
Drive retention and revenue with
|
||||
<span class="text-blue-700 relative">Customer Education</span>
|
||||
</h1>
|
||||
<p class="lg:w-[60%] text-base md:text-lg lg:text-center font-normal text-slate-700">
|
||||
Yes you are making lots of sales however you keep experiencing churn and customers leaving
|
||||
you. Without a proper customer education strategy you are leaving a lot of money on the
|
||||
table.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/signup"
|
||||
target="_blank"
|
||||
class="py-5 px-6 bg-[#1D4ED8] border-transparent outline-transparent rounded-lg text-white text-lg font-semibold"
|
||||
>
|
||||
Get Started for Free 🚀
|
||||
</a>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<section
|
||||
class="w-full lg:py-24 py-16 lg:px-28 md:px-16 px-8 flex flex-col lg:items-center lg:justify-between justify-center gap-y-12"
|
||||
>
|
||||
<div
|
||||
class="w-full gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
|
||||
>
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class="text-sm md:text-base font-medium">Our benefits</p>
|
||||
</div>
|
||||
<div
|
||||
class="lg:w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1 class="text-3xl md:text-5xl font-bold">The customer education toolkit</h1>
|
||||
<p class="text-[#334155] text-sm md:text-base font-light lg:text-center md:w-[80%]">
|
||||
Create beautiful, branded landing pages to showcase your courses and get customers up to
|
||||
speed quickly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex lg:flex-row flex-col lg:justify-between justify-start gap-6 lg:items-center items-start"
|
||||
>
|
||||
{#each benefitDetails as benefits}
|
||||
<div class="w-full md:p-6 p-4 border border-[#D9E0F5] rounded-2xl flex flex-col gap-y-6">
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="https://assets.cdn.clsrio.com/www/customer-education/{benefits.icon}"
|
||||
alt="icon"
|
||||
/>
|
||||
|
||||
<div class="w-full flex flex-col justify-start align-start gap-y-2">
|
||||
<h1 class="text-xl font-medium">{benefits.title}</h1>
|
||||
|
||||
<div class="flex flex-row justify-start items-start gap-x-3">
|
||||
<p class="leading-8 text-gray-500 font-[#656565] font-light">
|
||||
{benefits.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="w-full bg-[#F1F6FF] flex flex-col items-center justify-center gap-y-16 lg:py-24 py-16 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div
|
||||
class="w-full gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
|
||||
>
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base text-[#4B5563] text-center font-medium">
|
||||
Explore our features
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="lg:w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Why Choose Us For Education Needs</h1>
|
||||
<p class="text-[#334155] text-sm md:text-base font-light lg:text-center md:w-[80%]">
|
||||
Discover how our innovative solutions can reduce stress, enhance productivity, and achieve
|
||||
your training goals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col justify-start items-center lg:gap-y-24 gap-y-16"
|
||||
>
|
||||
{#each featureDetails as feature, i}
|
||||
<div
|
||||
class="w-full lg:w-[70%] flex lg:flex-row {i % 2 !== 0
|
||||
? 'lg:flex-row-reverse'
|
||||
: ''} flex-col lg:justify-between justify-start lg:items-center items-start gap-10"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-sm lg:w-[40%] lg:gap-y-6 gap-y-3 flex flex-col items-start justify-start"
|
||||
>
|
||||
<img
|
||||
src="https://assets.cdn.clsrio.com/www/customer-education/{feature.icon}"
|
||||
alt="team icon"
|
||||
/>
|
||||
<div class="md:w-full flex flex-col items-start justify-start gap-y-4">
|
||||
<h1 class="font-bold text-4xl">{feature.title}</h1>
|
||||
<p class="leading-8 text-lg text-gray-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="py-4 px-8 border border-[#1D4ED8] outline-transparent rounded text-[#1D4ED8] text-sm font-semibold"
|
||||
data-cal-config="'layout':'month_view'"
|
||||
data-cal-link="classroomio/demo"
|
||||
>
|
||||
Book a Demo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
class="w-full lg:max-w-[380px]"
|
||||
src="https://assets.cdn.clsrio.com/www/customer-education/{feature.img}"
|
||||
alt="team management feature ui"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Stop losing customers to your competitors"
|
||||
subText="We help you retain customers by helping them understand your products and services."
|
||||
btnLabel="Book a demo"
|
||||
demo={true}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
const benefitDetails = [
|
||||
{
|
||||
icon: 'light-bulb.svg',
|
||||
title: 'Increased Engagement',
|
||||
description: 'Interactive courses and collaborative features keep employees motivated.'
|
||||
},
|
||||
{
|
||||
icon: 'learning.svg',
|
||||
title: 'Streamlined Learning',
|
||||
description: 'Centralized platform for all training materials and progress tracking.'
|
||||
},
|
||||
{
|
||||
icon: 'pie-chart.svg',
|
||||
title: 'Data-Driven Insights',
|
||||
description: 'Gain valuable insights into employee performance and learning patterns.'
|
||||
},
|
||||
{
|
||||
icon: 'paint-brush.svg',
|
||||
title: 'Customizable Experience',
|
||||
description: "Tailor the learning experience to your company's specific needs and branding."
|
||||
}
|
||||
];
|
||||
|
||||
const featureDetails = [
|
||||
{
|
||||
icon: 'team-mgnt.svg',
|
||||
img: 'team-mgnt2.svg',
|
||||
title: 'Team management',
|
||||
description:
|
||||
'You can invite managers and employees to create and take trainings at scale in your organization.'
|
||||
},
|
||||
{
|
||||
icon: 'colour-pallete.svg',
|
||||
img: 'custom.svg',
|
||||
title: 'Custom branding',
|
||||
description:
|
||||
'With our customizable platform, you can set your company logo, brand color and add a custom domain to your employee LMS.'
|
||||
},
|
||||
{
|
||||
icon: 'cup.svg',
|
||||
img: 'progress.svg',
|
||||
title: 'Track employee progress',
|
||||
description: 'You can track the progress of your employees and assist whoever needs help.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Employee Training | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="flex flex-col justify-center items-center">
|
||||
<PageHeader
|
||||
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div class="bg-[#DCE5FF] py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base text-[#4B5563] text-center font-medium">
|
||||
Unlock your team's potential
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-8"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1
|
||||
class="mx-auto m-0 text-4xl lg:text-7xl md:text-6xl font-bold text-slate-900 lg:text-center"
|
||||
>
|
||||
Employee training software for
|
||||
<br /><span class="text-blue-700 relative">modern teams</span>
|
||||
</h1>
|
||||
<p class="lg:w-[60%] text-base md:text-lg lg:text-center font-normal text-slate-700">
|
||||
With ClassroomIO, you can keep your teams in sync with company standards as they change
|
||||
quickly.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/signup"
|
||||
target="_blank"
|
||||
class="py-5 px-6 bg-[#1D4ED8] border-transparent outline-transparent rounded-lg text-white text-lg font-semibold"
|
||||
>
|
||||
Get Started for Free 🚀
|
||||
</a>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<section
|
||||
class="w-full max-w-screen-xl lg:py-24 py-16 lg:px-28 md:px-16 px-8 flex lg:flex-row flex-col lg:items-center lg:justify-between justify-center gap-x-6 gap-y-8"
|
||||
>
|
||||
<div class="w-full lg:w-[35%] gap-y-4 flex flex-col items-start justify-start">
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class=" text-sm md:text-base font-medium">Amazing benefits</p>
|
||||
</div>
|
||||
|
||||
<div class="md:w-full flex flex-col items-start justify-start gap-y-4">
|
||||
<h1 class="font-bold text-4xl">Unlock your team's potential</h1>
|
||||
<p class="leading-8 text-lg text-gray-500">
|
||||
Modern teams require solutions that are easy to use and allows them move quickly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="lg:w-[60%] lg:min-w-[450px] w-full flex flex-col lg:flex-wrap lg:flex-row justify-start items-center gap-6"
|
||||
>
|
||||
{#each benefitDetails as benefits}
|
||||
<div
|
||||
class="w-full lg:w-[47%] min-w-[220px] md:p-6 p-4 border border-[#D9E0F5] rounded-2xl flex flex-col grow gap-y-6"
|
||||
>
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="https://assets.cdn.clsrio.com/www/employee-training/{benefits.icon}"
|
||||
alt="light-bulb"
|
||||
/>
|
||||
|
||||
<div class="w-full flex flex-col justify-start align-start gap-y-2">
|
||||
<h1 class="text-xl font-medium">{benefits.title}</h1>
|
||||
<p class="leading-8 text-gray-500 font-[#656565] font-light">
|
||||
{benefits.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="w-full bg-[#F1F6FF] flex flex-col items-center justify-start gap-y-16 lg:py-24 py-16 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div
|
||||
class="w-full gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
|
||||
>
|
||||
<div class="w-fit py-2 px-4 rounded-full border-[#C2D2FF] border-2 text-center">
|
||||
<p class="text-sm md:text-base text-[#4B5563] text-center font-medium">
|
||||
Explore our features
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="lg:w-full max-w-screen-xl flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-4"
|
||||
>
|
||||
<h1 class="text-3xl md:text-5xl font-bold">What makes us the right choice</h1>
|
||||
<p class="text-[#334155] text-sm md:text-base font-light lg:text-center md:w-[80%]">
|
||||
Discover why we’re the perfect partner for your employee training needs. Experience
|
||||
innovative solutions designed to reduce stress and enhance productivity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex flex-col justify-start items-center lg:gap-y-24 gap-16 p-2"
|
||||
>
|
||||
{#each featureDetails as feature, i}
|
||||
<div
|
||||
class="w-full lg:w-[70%] flex lg:flex-row {i % 2 !== 0
|
||||
? 'lg:flex-row-reverse'
|
||||
: ''} flex-col justify-between lg:items-center items-start gap-10"
|
||||
>
|
||||
<div
|
||||
class="w-fit max-w-sm lg:w-[40%] lg:gap-y-6 gap-y-3 flex flex-col items-start justify-start"
|
||||
>
|
||||
<img
|
||||
class="w-[48px] h-[48px]"
|
||||
src="https://assets.cdn.clsrio.com/www/employee-training/{feature.icon}"
|
||||
alt="team icon"
|
||||
/>
|
||||
<div class="md:w-full flex flex-col items-start justify-start gap-y-4">
|
||||
<h1 class="font-bold text-4xl">{feature.title}</h1>
|
||||
<p class="leading-8 text-lg text-gray-500">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="py-4 px-8 border border-[#1D4ED8] outline-transparent rounded text-[#1D4ED8] text-sm font-semibold"
|
||||
data-cal-config="'layout':'month_view'"
|
||||
data-cal-link="classroomio/demo"
|
||||
>
|
||||
Book a Demo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
class="w-full lg:max-w-[380px]"
|
||||
src="https://assets.cdn.clsrio.com/www/employee-training/{feature.img}"
|
||||
alt="team management feature ui"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="w-full bg-[#040F2D] flex flex-col items-center justify-start gap-y-16 lg:py-24 py-16 lg:px-28 md:px-16 px-8"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-screen-xl flex lg:flex-row-reverse flex-col-reverse lg:justify-between justify-start lg:items-center items-start gap-y-8"
|
||||
>
|
||||
<div class="w-full lg:w-[40%] gap-y-4 flex flex-col items-start justify-start">
|
||||
<div class="md:w-full flex flex-col items-start justify-start gap-y-4">
|
||||
<h1 class="font-bold text-4xl text-white">
|
||||
Did you know you can request for custom features?
|
||||
</h1>
|
||||
<p class="leading-8 text-[#DEDEDE] text-xl font-light">
|
||||
Companies get the opportunity to request custom features from us.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/demo"
|
||||
target="_blank"
|
||||
class="py-4 px-8 border border-tranparent outline-transparent bg-white rounded text-[#040F2D] text-sm font-semibold"
|
||||
>
|
||||
Talk to sales
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<img
|
||||
class="size-96"
|
||||
src="https://assets.cdn.clsrio.com/www/employee-training/bro.svg"
|
||||
alt="male and female in an office"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Give your team the training they deserve"
|
||||
subText="It's free to sign up and start getting value out of the product."
|
||||
btnLabel="Sign me up"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'cal.com page visited'
|
||||
});
|
||||
|
||||
redirect(307, 'https://cal.com/classroomio/demo');
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async () => {
|
||||
redirect(307, 'https://docs.google.com/presentation/d/1eqvNPNk167ZcP21gZGVAatdDpWAFL9lfmWJHHrzsVFc');
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'discord page visited'
|
||||
});
|
||||
|
||||
redirect(307, 'https://discord.gg/dRmxBpJw59');
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<slot />
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = async ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'github page visited'
|
||||
});
|
||||
|
||||
redirect(307, 'https://github.com/rotimi-best/classroomio');
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async () => {
|
||||
redirect(307, 'https://t.me/thebestafricans');
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async () => {
|
||||
const url = dev ? 'http://localhost:5173' : 'https://app.classroomio.com';
|
||||
|
||||
redirect(307, `${url}/org/*/courses?create=true`);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async () => {
|
||||
redirect(307, 'https://www.producthunt.com/posts/classroomio-2');
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = async ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'privacy page visited'
|
||||
});
|
||||
|
||||
redirect(
|
||||
307,
|
||||
'https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594'
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'signup'
|
||||
});
|
||||
|
||||
redirect(307, 'https://app.classroomio.com/signup');
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { client } from '$lib/utils/posthog';
|
||||
|
||||
export const load = async ({ request }) => {
|
||||
client.capture({
|
||||
distinctId: request.headers.get('x-forwarded-for') || new Date().getTime().toString(),
|
||||
event: 'privacy page visited'
|
||||
});
|
||||
|
||||
redirect(307, 'https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594');
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async () => {
|
||||
redirect(307, 'https://form.classroomio.com/f/1v25DtmN');
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import Footer from '$lib/Footer/Footer.svelte';
|
||||
import Navigation from '$lib/Navigation/Navigation.svelte';
|
||||
import NotFound from '$lib/NotFound/NotFound.svelte';
|
||||
import extend from 'just-extend';
|
||||
import { onMount } from 'svelte';
|
||||
import { MetaTags } from 'svelte-meta-tags';
|
||||
import PageTransition from './transition.svelte';
|
||||
|
||||
import '../app.css';
|
||||
|
||||
export let data;
|
||||
|
||||
onMount(() => {
|
||||
if (dev) {
|
||||
localStorage.setItem('umami.disabled', '1');
|
||||
}
|
||||
});
|
||||
|
||||
$: metaTags = extend(true, {}, data.baseMetaTags, $page.data.pageMetaTags);
|
||||
</script>
|
||||
|
||||
<MetaTags {...metaTags} />
|
||||
|
||||
<div class="overflow-hidden bg-white">
|
||||
<Navigation />
|
||||
|
||||
<PageTransition url={data.url}>
|
||||
{#if $page.status === 404}
|
||||
<NotFound className="mt-5" />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</PageTransition>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function load({ url }) {
|
||||
return {
|
||||
baseMetaTags: getBaseMetaTags(url),
|
||||
url: url.pathname
|
||||
};
|
||||
}
|
||||
|
||||
function getBaseMetaTags(url: URL) {
|
||||
const metatags = Object.freeze({
|
||||
title: 'ClassroomIO | The Open Source Learning Management System for Companies',
|
||||
description:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations',
|
||||
canonical: new URL(url.pathname, url.origin).href,
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
url: new URL(url.pathname, url.origin).href,
|
||||
locale: 'en_IE',
|
||||
title: 'ClassroomIO | The Open Source Learning Management System for Companies',
|
||||
description:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations',
|
||||
siteName: 'ClassroomIO',
|
||||
images: [
|
||||
{
|
||||
url: 'https://brand.cdn.clsrio.com/og/classroomio-og.png',
|
||||
alt: 'ClassroomIO OG Image',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
secureUrl: 'https://brand.cdn.clsrio.com/og/classroomio-og.png',
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
handle: '@classroomio',
|
||||
site: '@classroomio',
|
||||
cardType: 'summary_large_image' as const,
|
||||
title: 'ClassroomIO | The Open Source Learning Management System for Companies',
|
||||
description:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations',
|
||||
image: 'https://brand.cdn.clsrio.com/og/classroomio-og.png',
|
||||
imageAlt: 'ClassroomIO OG Image'
|
||||
}
|
||||
}) satisfies MetaTagsProps;
|
||||
|
||||
return metatags;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import Hero from '$lib/Home/Hero.svelte';
|
||||
import CoreFeatures from '$lib/Home/CoreFeatures.svelte';
|
||||
import Faq from '$lib/Home/Faq.svelte';
|
||||
import StudentDashboard from '$lib/Home/StudentDashboard.svelte';
|
||||
import MoreFeatures from '$lib/Home/MoreFeatures.svelte';
|
||||
import Testimonial from '$lib/Home/Testimonial.svelte';
|
||||
import { PUBLIC_ENABLE_FAQ } from '$env/static/public';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
console.log('Welcome to CIO');
|
||||
});
|
||||
</script>
|
||||
|
||||
<Hero />
|
||||
<CoreFeatures />
|
||||
<StudentDashboard />
|
||||
<Testimonial />
|
||||
<MoreFeatures />
|
||||
|
||||
{#if PUBLIC_ENABLE_FAQ}
|
||||
<Faq />
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { BlogPost } from '$lib/types';
|
||||
|
||||
async function getBlogPosts() {
|
||||
let blogPosts: BlogPost[] = [];
|
||||
|
||||
const paths = import.meta.glob('/src/blog/*.md', { eager: true });
|
||||
|
||||
for (const path in paths) {
|
||||
const file = paths[path];
|
||||
const slug = path.split('/').at(-1)?.replace('.md', '');
|
||||
|
||||
if (file && typeof file === 'object' && 'metadata' in file && slug) {
|
||||
const metadata = file.metadata as Omit<BlogPost, 'slug'>;
|
||||
const blogPost = { ...metadata, slug } satisfies BlogPost;
|
||||
blogPost.published && blogPosts.push(blogPost);
|
||||
}
|
||||
}
|
||||
|
||||
blogPosts = blogPosts.sort(
|
||||
(first, second) => new Date(second.date).getTime() - new Date(first.date).getTime()
|
||||
);
|
||||
|
||||
return blogPosts;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const blogPosts = await getBlogPosts();
|
||||
|
||||
return json(blogPosts);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PUBLIC_SERVER_URL } from '$env/static/public';
|
||||
|
||||
const sendEmail = async (subject: string, content: string, replyTo?: string) => {
|
||||
try {
|
||||
const response = await fetch(`${PUBLIC_SERVER_URL}/sendEmail`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify([
|
||||
{
|
||||
from: `"ClassroomIO Support" <[email protected]>`,
|
||||
to: '[email protected]',
|
||||
subject,
|
||||
content,
|
||||
replyTo
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
console.log('Emails sent successfully:', responseData);
|
||||
|
||||
return 'sent';
|
||||
} catch (error) {
|
||||
console.error('Error sending email', error);
|
||||
return 'failed';
|
||||
}
|
||||
};
|
||||
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
const { title, description, replyTo } = await request.json();
|
||||
console.log(title, description, replyTo);
|
||||
const result = await sendEmail(title, description, replyTo);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'Issue created successfully',
|
||||
issue: result
|
||||
}),
|
||||
{
|
||||
status: 201,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('errors from api', error);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'Failed to create issue',
|
||||
error: (error as Error)?.message
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blog | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex flex-col items-center justify-center text-center">
|
||||
<h1
|
||||
class="mx-auto text-4xl md:text-7xl lg:text-6xl font-bold text-slate-900 flex flex-col items-center"
|
||||
>
|
||||
<span>What's new on</span>
|
||||
<span class="text-blue-700 relative">ClassroomIO?</span>
|
||||
</h1>
|
||||
<p class="w-[90%] md:w-[60%] text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7">
|
||||
Get the latest news from ClassroomIO, including product updates, team announcements and more!
|
||||
</p>
|
||||
</PageHeader>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<ul class="flex flex-col items-start justify-start gap-2 w-[90%] md:w-[50%]">
|
||||
{#each data.posts as post}
|
||||
<li class="py-10 w-full border-b border-gray-200">
|
||||
<BlogListItem {post} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Ready To Scale Your Training Efforts?"
|
||||
subText="It's free to sign up and start getting value out of the product."
|
||||
btnLabel="Sign me up"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { BlogPost } from '$lib/types';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ fetch }) {
|
||||
const response = await fetch('api/blog');
|
||||
const posts: BlogPost[] = await response.json();
|
||||
return { posts };
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { ChevronLeft } from 'carbon-icons-svelte';
|
||||
import Chip from '$lib/Chip/Chip.svelte';
|
||||
import { formatDate } from '$lib/utils/formatDate';
|
||||
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class=" mt-[10%] md:mt-16">
|
||||
{#if data}
|
||||
<article class="py-16">
|
||||
<!-- Title -->
|
||||
<hgroup class="flex flex-col items-center justify-center text-center w-full">
|
||||
<p class="text-sm text-gray-500">{formatDate(data.meta.date)}</p>
|
||||
<p class="font-bold text-3xl py-2 text-center md:w-[60%]">{@html data.meta.title}</p>
|
||||
</hgroup>
|
||||
<main class="mx-auto max-w-screen-md px-4 lg:px-8">
|
||||
<div class="flex items-center justify-start gap-4 my-2 border-y border-gray-200 py-4">
|
||||
<img loading="lazy" src={data.meta.avatar} alt="avatar" class="w-10 h-10 rounded-full" />
|
||||
<span>
|
||||
<p class="font-semibold">{data.meta.author}</p>
|
||||
<p class="text-gray-500">{data.meta.role}</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Post -->
|
||||
<div class="prose pt-2 pb-4 border-b-2 border-gray-200">
|
||||
<svelte:component this={data.content} />
|
||||
<!-- Tags -->
|
||||
<div class="flex gap-2 py-4">
|
||||
{#each data.meta.tags as tag}
|
||||
<Chip label={tag} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.relatedPosts.length > 0}
|
||||
<section class="mt-5">
|
||||
<p class="text-xl font-semibold">Related Posts</p>
|
||||
<ul class="flex items-start justify-start gap-3 overflow-x-scroll">
|
||||
{#each data.relatedPosts as post}
|
||||
<li class="py-10 min-w-[80%] sm:w-80 sm:min-w-0">
|
||||
<BlogListItem {post} isRecommended />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/blog"
|
||||
class="flex font-medium items-center gap-2 text-slate-700 text-lg underline my-10"
|
||||
>
|
||||
<ChevronLeft class=" w-6 h-6 font-medium" /> Back to all posts
|
||||
</a>
|
||||
</main>
|
||||
</article>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
article {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
:global(.prose a) {
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:global(.prose .gallery img) {
|
||||
max-height: 300px;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
:global(.prose .gallery) {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { BlogPost, RawBlogPost } from '$lib/types';
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params, fetch }) {
|
||||
try {
|
||||
const response = await fetch('/api/blog');
|
||||
const posts: BlogPost[] = await response.json();
|
||||
|
||||
const blog: RawBlogPost = await import(`../../../blog/${params.slug}.md`);
|
||||
|
||||
return {
|
||||
content: blog.default,
|
||||
meta: blog.metadata,
|
||||
slug: params.slug,
|
||||
pageMetaTags: getPageMetaTags(blog),
|
||||
relatedPosts: getRelatedPosts(posts, blog.metadata.tags, params.slug)
|
||||
};
|
||||
} catch (e) {
|
||||
error(404, `Could not find ${params.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getRelatedPosts(posts: BlogPost[], tags: string[], slug: string) {
|
||||
const related = posts
|
||||
.filter((post) => post.tags.some((tag) => tags.includes(tag)) && post.slug !== slug)
|
||||
.map((post) => ({
|
||||
...post,
|
||||
rank: post.tags.filter((tag) => tags.includes(tag)).length
|
||||
}))
|
||||
.sort((a, b) => b.rank - a.rank);
|
||||
|
||||
const topPosts = related.filter((post) => post.rank > 1);
|
||||
|
||||
if (topPosts.length > 2) {
|
||||
return topPosts.sort(() => Math.random() - 0.5).slice(0, 2);
|
||||
}
|
||||
|
||||
return related.slice(0, 2);
|
||||
}
|
||||
|
||||
function getPageMetaTags(blog: RawBlogPost): MetaTagsProps {
|
||||
return Object.freeze({
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
openGraph: {
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
images: [
|
||||
{
|
||||
url: blog.metadata.imageUrl,
|
||||
alt: blog.metadata.title,
|
||||
width: 280,
|
||||
height: 200,
|
||||
secureUrl: blog.metadata.imageUrl,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
handle: '@classroomio',
|
||||
site: '@classroomio',
|
||||
cardType: 'summary_large_image' as const,
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
image: blog.metadata.imageUrl,
|
||||
imageAlt: `${blog.metadata.title} OG`
|
||||
}
|
||||
}) satisfies MetaTagsProps;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import ArrowUpRight from 'carbon-icons-svelte/lib/ArrowUpRight.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
import FeedbackForm from '$lib/Contact/FeedbackForm.svelte';
|
||||
import HelpForm from '$lib/Contact/HelpForm.svelte';
|
||||
import { FORM_TYPE } from '$lib/types';
|
||||
import type { FORM_TYPE_KEY } from '$lib/types';
|
||||
|
||||
let currentForm: FORM_TYPE_KEY;
|
||||
|
||||
const formList = [
|
||||
{
|
||||
id: FORM_TYPE.BUG,
|
||||
title: '🐜 I want to report a bug',
|
||||
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml'
|
||||
},
|
||||
{
|
||||
id: FORM_TYPE.HELP,
|
||||
title: '🙋♂️ I need help'
|
||||
},
|
||||
{
|
||||
id: FORM_TYPE.FEATURE,
|
||||
title: '📮 I want to request a feature',
|
||||
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=&projects=&template=feature-request.yml'
|
||||
},
|
||||
{
|
||||
id: FORM_TYPE.FEEDBACK,
|
||||
title: '💭 I have feedback'
|
||||
}
|
||||
];
|
||||
|
||||
const onChange = (form: FORM_TYPE_KEY, to?: string) => {
|
||||
currentForm = form;
|
||||
|
||||
if (to) {
|
||||
window.open(to, '_blank');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Contact us | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex flex-col items-center justify-center bg-[#F5F8FE] text-center">
|
||||
<div class="mb-2 ml-[5%] lg:ml-0 flex w-full items-center justify-center">
|
||||
<span
|
||||
style="color: rgb(75, 85, 99);"
|
||||
class="rounded-full border-2 border-[#C2D2FF] px-4 py-1 text-sm font-medium bg-[#DCE5FF] text-blue-700"
|
||||
>
|
||||
Contact Us
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
class="mx-auto text-4xl md:text-7xl lg:text-6xl font-bold text-slate-900 flex flex-col items-center"
|
||||
>
|
||||
<span>Get in touch with <span class="text-blue-700">Us</span></span>
|
||||
</h1>
|
||||
<p
|
||||
class="w-[90%] md:w-[60%] max-w-3xl text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7"
|
||||
>
|
||||
Report any bugs you encounter, request new features you'd like to see, or ask any questions
|
||||
you have about ClassroomIO
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<div class="my-20 mx-auto">
|
||||
<!-- Content here -->
|
||||
<div
|
||||
class="mx-auto w-[90%] lg:w-[60%] place-items-center grid grid-cols-1 md:grid-cols-2 gap-10"
|
||||
>
|
||||
{#each formList as list}
|
||||
<button
|
||||
on:click={() => onChange(list.id, list.to)}
|
||||
class="text-start border-[1.5px] w-full rounded-md p-3 {currentForm === list.id
|
||||
? `border-[#0233BD]`
|
||||
: `border-gray-200`} flex items-center justify-between"
|
||||
>
|
||||
<p class="text-lg text-slate-700 font-normal">{list.title}</p>
|
||||
{#if list.to}
|
||||
<ArrowUpRight size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if currentForm === FORM_TYPE[currentForm]}
|
||||
<div class="mx-auto w-[90%] lg:w-[60%] px-5">
|
||||
{#if currentForm === FORM_TYPE.HELP}
|
||||
<HelpForm />
|
||||
{:else if currentForm === FORM_TYPE.FEEDBACK}
|
||||
<FeedbackForm />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- <PageSignupCTA
|
||||
header="Scale your Teaching Business in Minutes"
|
||||
subText="Don't wait, let's get you started."
|
||||
btnLabel="Register"
|
||||
link="/teach/register"
|
||||
demo={false}
|
||||
/> -->
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Our Open-source Friends | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex flex-col items-center justify-center text-center">
|
||||
<h1
|
||||
class="mx-auto text-3xl md:text-4xl lg:text-6xl font-normal leading-[1.5] text-slate-900 items-center"
|
||||
>
|
||||
Our <span class="text-blue-700">Open-source</span> Friends
|
||||
</h1>
|
||||
<p class="w-[90%] md:w-[60%] text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7">
|
||||
In ClassroomIO, we are committed to building a commercial open source company and here are
|
||||
some of our friends on the same mission.
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<ul role="list" class="m-4 mt-12 grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 xl:gap-x-8">
|
||||
{#each data.friends as friend}
|
||||
<li
|
||||
class="overflow-hidden rounded-md border border-gray-200 flex flex-col justify-between py-6"
|
||||
>
|
||||
<div>
|
||||
<div class="text-xl font-medium leading-6 text-gray-900 pb-1 px-6">
|
||||
{friend.name}
|
||||
</div>
|
||||
<p class="px-6 mt-2 text-gray-600 text-sm">
|
||||
{friend.description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-left px-6 mt-3">
|
||||
<a
|
||||
href={friend.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-block w-full"
|
||||
>
|
||||
<button
|
||||
class="font-semibold text-base text-white bg-gray-800 hover:bg-gray-900 hover:shadow-xl transition-all delay-100 px-4 py-3 rounded-md"
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { OssFriend } from '$lib/types';
|
||||
|
||||
export async function load() {
|
||||
const friends: OssFriend[] = [
|
||||
{
|
||||
name: 'Appsmith',
|
||||
description: 'Build custom software on top of your data.',
|
||||
href: 'https://www.appsmith.com'
|
||||
},
|
||||
{
|
||||
name: 'BoxyHQ',
|
||||
description:
|
||||
'BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.',
|
||||
href: 'https://boxyhq.com'
|
||||
},
|
||||
{
|
||||
name: 'Cal.com',
|
||||
description:
|
||||
'Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.',
|
||||
href: 'https://cal.com'
|
||||
},
|
||||
{
|
||||
name: 'Crowd.dev',
|
||||
description:
|
||||
'Centralize community, product, and customer data to understand which companies are engaging with your open source project.',
|
||||
href: 'https://www.crowd.dev'
|
||||
},
|
||||
{
|
||||
name: 'Documenso',
|
||||
description:
|
||||
'The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.',
|
||||
href: 'https://documenso.com'
|
||||
},
|
||||
{
|
||||
name: 'Dub',
|
||||
description:
|
||||
'Dub is an open-source link management tool for modern marketing teams to create, share, and track short links.',
|
||||
href: 'https://dub.co'
|
||||
},
|
||||
{
|
||||
name: 'Formbricks',
|
||||
description:
|
||||
'Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.',
|
||||
href: 'https://formbricks.com'
|
||||
},
|
||||
{
|
||||
name: 'Hanko',
|
||||
description:
|
||||
'Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.',
|
||||
href: 'https://www.hanko.io'
|
||||
},
|
||||
{
|
||||
name: 'Infisical',
|
||||
description:
|
||||
'Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.',
|
||||
href: 'https://infisical.com'
|
||||
},
|
||||
{
|
||||
name: 'Novu',
|
||||
description:
|
||||
'The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.',
|
||||
href: 'https://novu.co'
|
||||
},
|
||||
{
|
||||
name: 'OpenBB',
|
||||
description:
|
||||
'Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.',
|
||||
href: 'https://openbb.co'
|
||||
},
|
||||
{
|
||||
name: 'OpenStatus',
|
||||
description: 'Open-source monitoring platform with beautiful status pages.',
|
||||
href: 'https://www.openstatus.dev'
|
||||
},
|
||||
{
|
||||
name: 'Papermark',
|
||||
description:
|
||||
'Open-Source Docsend Alternative to securely share documents with real-time analytics.',
|
||||
href: 'https://papermark.io'
|
||||
},
|
||||
{
|
||||
name: 'Trigger',
|
||||
description:
|
||||
'Create long-running Jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.',
|
||||
href: 'https://trigger.dev'
|
||||
},
|
||||
{
|
||||
name: 'UnInbox',
|
||||
description: 'Open Source Email + Chat communication platform',
|
||||
href: 'https://uninbox.com'
|
||||
},
|
||||
{
|
||||
name: 'Webstudio',
|
||||
description:
|
||||
'Webstudio visually translates CSS without obscuring it, giving designers superpowers that were exclusive to developers in the past.',
|
||||
href: 'https://webstudio.is'
|
||||
}
|
||||
];
|
||||
return { friends };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = false;
|
||||
@@ -0,0 +1,209 @@
|
||||
<script>
|
||||
import { fade } from 'svelte/transition';
|
||||
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
||||
import { PLANS } from '@cio/utils/plans';
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
let isYearlyPlan = false;
|
||||
|
||||
function toggleIsYearlyPlan() {
|
||||
isYearlyPlan = !isYearlyPlan;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Pricing | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex flex-col items-center justify-center text-center">
|
||||
<h1
|
||||
class="mx-auto text-4xl md:text-7xl lg:text-6xl font-bold text-slate-900 flex flex-col items-center"
|
||||
>
|
||||
<span>There is room for</span>
|
||||
<span class="text-blue-700 relative">Everyone.</span>
|
||||
</h1>
|
||||
<p class="w-[90%] md:w-[60%] text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7">
|
||||
You get a customizable LMS, AI integration for productive educators and many more...
|
||||
</p>
|
||||
<div class="relative mt-10 flex items-center rounded-[30px] border-[2px] p-[2px] lg:scale-100">
|
||||
<button
|
||||
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
|
||||
? '#5e636b'
|
||||
: '#fff'}"
|
||||
class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||
on:click={toggleIsYearlyPlan}>Monthly</button
|
||||
>
|
||||
<button
|
||||
style="background-color: {isYearlyPlan ? '#1D4EE2' : ''}; color: {isYearlyPlan
|
||||
? '#fff'
|
||||
: '#5e636b'}"
|
||||
class="rounded-[30px] px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||
on:click={toggleIsYearlyPlan}>Annually</button
|
||||
>
|
||||
<div
|
||||
class="absolute right-[-40%] top-[-85%] scale-[90%] rounded-full bg-[#006600] px-3.5 py-1.5 text-xs text-white lg:right-[-43%] lg:top-[-75%] lg:scale-100"
|
||||
>
|
||||
Save 2 months
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script
|
||||
src="https://widget.senja.io/widget/b43ac234-427e-4d6f-8c23-633208154e54/platform.js"
|
||||
type="text/javascript"
|
||||
async
|
||||
></script>
|
||||
<div
|
||||
class="senja-embed mt-5"
|
||||
data-id="b43ac234-427e-4d6f-8c23-633208154e54"
|
||||
data-mode="shadow"
|
||||
data-lazyload="false"
|
||||
style="display: block;"
|
||||
></div>
|
||||
</PageHeader>
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-[6%]">
|
||||
<div class="isolate mt-10 grid grid-cols-1 gap-10 lg:grid-cols-3">
|
||||
<!-- Card 1 -->
|
||||
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||
{PLANS.BASIC.NAME}
|
||||
</p>
|
||||
|
||||
<p class="lg:text-base mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||
{PLANS.BASIC.DESCRIPTION}
|
||||
</p>
|
||||
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium lg:text-4xl">
|
||||
${PLANS.BASIC.PRICE.MONTHLY}
|
||||
</p>
|
||||
<span class="text-base">Free forever</span>
|
||||
|
||||
<a
|
||||
class="text-base mt-10 block w-full rounded-md bg-slate-900 py-3 text-center font-medium text-white hover:bg-slate-700 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
href={PLANS.BASIC.CTA.LINK}
|
||||
target="_blank"
|
||||
>
|
||||
{PLANS.BASIC.CTA.LABEL}
|
||||
</a>
|
||||
|
||||
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.BASIC.FEATURES as feature}
|
||||
<li class="flex items-center">
|
||||
<Checkmark
|
||||
size={24}
|
||||
fill="#1D4EE2"
|
||||
class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]"
|
||||
/>
|
||||
{feature}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 2 -->
|
||||
<div class="cio-bg-blue max-w-xl rounded-3xl p-8 lg:max-w-lg xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-white lg:text-xl">
|
||||
{PLANS.EARLY_ADOPTER.NAME}
|
||||
</p>
|
||||
|
||||
<p class="lg:text-base mt-4 text-sm font-light leading-6 text-white lg:leading-6">
|
||||
{PLANS.EARLY_ADOPTER.DESCRIPTION}
|
||||
</p>
|
||||
|
||||
{#key isYearlyPlan}
|
||||
<p
|
||||
class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium text-white lg:text-4xl"
|
||||
in:fade
|
||||
>
|
||||
${isYearlyPlan ? PLANS.EARLY_ADOPTER.PRICE.YEARLY : PLANS.EARLY_ADOPTER.PRICE.MONTHLY}
|
||||
</p>
|
||||
<span class="text-base text-white" in:fade>
|
||||
per {isYearlyPlan ? 'year' : 'month'}
|
||||
</span>
|
||||
{/key}
|
||||
|
||||
<a
|
||||
class="text-base mt-10 block w-full rounded-md bg-white py-3 text-center font-medium text-slate-900 hover:bg-indigo-50 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
href={PLANS.EARLY_ADOPTER.CTA.LINK}
|
||||
target="_blank"
|
||||
>
|
||||
{PLANS.EARLY_ADOPTER.CTA.LABEL}
|
||||
</a>
|
||||
<ul class="mt-8 space-y-3 text-sm font-light text-white lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.EARLY_ADOPTER.FEATURES as features}
|
||||
<li class="flex items-center">
|
||||
<Checkmark size={24} fill="#fff" class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]" />
|
||||
{features}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 3 -->
|
||||
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||
{PLANS.ENTERPRISE.NAME}
|
||||
</p>
|
||||
<p class="lg:text-base mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||
{PLANS.ENTERPRISE.DESCRIPTION}
|
||||
</p>
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-xl font-medium lg:mt-6 lg:text-xl">
|
||||
Request Pricing
|
||||
</p>
|
||||
<button
|
||||
class="text-base mt-10 w-full rounded-md bg-slate-900 py-3 font-medium text-white hover:bg-slate-700 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
data-cal-config={"{'layout':'month_view'}"}
|
||||
data-cal-link="classroomio/enterprise"
|
||||
>
|
||||
{PLANS.ENTERPRISE.CTA.LABEL}
|
||||
</button>
|
||||
|
||||
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.ENTERPRISE.FEATURES as features}
|
||||
<li class="flex items-center">
|
||||
<Checkmark
|
||||
size={24}
|
||||
fill="#1D4EE2"
|
||||
class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]"
|
||||
/>
|
||||
{features}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div
|
||||
class="flex flex-col lg:flex-row items-start lg:items-center mx-auto lg:mx-10 mt-[5%] border-[1px] rounded-[30px] py-[10%] lg:py-[5%] px-[10px] lg:px-[30px] lg:w-full md:max-w-[550px] lg:max-w-full"
|
||||
>
|
||||
<div class="mr-5 pl-5">
|
||||
<h2
|
||||
class="text-base lg:text-xl font-semibold font-display leading-8 tracking-wide lg:tracking-tight text-blue-700 mb-0 lg:mb-4"
|
||||
>
|
||||
Hobby
|
||||
</h2>
|
||||
<p
|
||||
class="m-0 lg:mt-1 text-base leading-7 lg:leading-6 text-gray-600"
|
||||
>
|
||||
For personal use. Hobby plan is free for up to 5 courses forever, no
|
||||
credit Card required.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="font-semibold text-base lg:text-base text-white leading-5 mt-5 lg:m-0 p-4 lg:py-5 ml-5 bg-blue-700 rounded-lg lg:rounded-md w-[70%] lg:w-full"
|
||||
>
|
||||
Create a free account
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Bring your bootcamp vision online"
|
||||
subText="Try before you buy. No credit card required."
|
||||
btnLabel="Sign up for free"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
import Timeline from '$lib/Timeline/Timeline.svelte';
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
|
||||
const newFeatures = [
|
||||
{
|
||||
title: 'LMS Customization',
|
||||
desc: 'Right in your settings you can customize what you students see in their dashboard in few clicks.'
|
||||
},
|
||||
{
|
||||
title: 'Course Newsfeed',
|
||||
desc: 'You can now send a broadcast message to all the students participating in your course right from our dashboard.'
|
||||
},
|
||||
{
|
||||
title: 'Multi language support',
|
||||
desc: 'Our dashboard now supports 7 new languages. In addition, we allow you create course content in those any of those languages.'
|
||||
},
|
||||
{
|
||||
title: 'Security RLS',
|
||||
desc: "All the tables we've created now have role level security enabled by default"
|
||||
},
|
||||
{
|
||||
title: 'Increased video limit',
|
||||
desc: 'You can now upload a single video of about 500mb without interruption.'
|
||||
},
|
||||
{
|
||||
title: 'More certificate templates',
|
||||
desc: 'We have shipped 4 more certificate templates in which you can choose from right within your course dashboard'
|
||||
}
|
||||
];
|
||||
|
||||
const upcomingFeatures = [
|
||||
{
|
||||
title: 'Pathways',
|
||||
desc: 'This feature gives you the ability to group courses into folder which we are calling - Pathways. Students can complete a series of courses before getting a certificate'
|
||||
},
|
||||
{
|
||||
title: 'Course Batches',
|
||||
desc: 'A course can be taken in batches, meaning that you can create different cohorts of students within one course. The benefit of this is that you can teach a course multiple times to different groups of students. This is useful mainly for bootcamps and teaching organizations.'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
desc: 'With tags you can group your courses into categories. This is useful for organizing your courses and making them easier to find.'
|
||||
},
|
||||
{
|
||||
title: 'API',
|
||||
desc: 'We are working on our public API which allows you to perform CRUD operations against your classroomio organization.'
|
||||
},
|
||||
{
|
||||
title: 'Embedable Course',
|
||||
desc: 'You can embed selected course form ClassroomIO on any website of your choice'
|
||||
},
|
||||
{
|
||||
title: 'Landing page builder',
|
||||
desc: 'With more templates and pages, we will allow educators create simple web pages using our platform to showcase their programs and offerings.'
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
desc: 'Educators can easily track students course completion rating, understand what topics are not clearly understood and much more.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Roadmap | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<PageHeader className="flex flex-col items-center justify-center text-center">
|
||||
<h1 class="text-start md:text-center text-5xl md:text-7xl font-bold text-slate-900">
|
||||
Product Roadmap
|
||||
</h1>
|
||||
<p class="text-start md:text-center w-[80%] md:w-[40%] text-slate-700 mt-10">
|
||||
This is a live document, and will be updated regularly. To submit an idea, or to view a list of
|
||||
feature requests and ideas that we're considering, check out our <a
|
||||
href="https://github.com/rotimi-best/classroomio/discussion"
|
||||
class="underline">GitHub Discussions</a
|
||||
>
|
||||
</p>
|
||||
</PageHeader>
|
||||
<section class="relative flex flex-col items-center w-full px-4 py-20 border-b-2">
|
||||
<img src="/roadmap-one.svg" alt="" class="w-60 lg:w-96 absolute bottom-0 right-0" />
|
||||
<img src="/roadmap-two.svg" alt="" class="hidden lg:block absolute top-0 left-0" />
|
||||
<div class="flex flex-col items-start space-y-10 px-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Latest Features</h1>
|
||||
</div>
|
||||
|
||||
<Timeline items={newFeatures} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col items-center w-full px-4 py-20 border-b-2">
|
||||
<div class="flex flex-col items-start space-y-10 px-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Upcoming Features</h1>
|
||||
</div>
|
||||
|
||||
<Timeline items={upcomingFeatures} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Share Your First Course With Your Audience In Minutes"
|
||||
subText="In about 4 Clicks you can already start creating courses."
|
||||
btnLabel="Get started for free"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,227 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
|
||||
const whyStart = [
|
||||
{
|
||||
src: '/teach/teach-from-anywhere.svg',
|
||||
header: 'Fully Online; Teach from anywhere',
|
||||
subText:
|
||||
' Facilitate a subject from the comfort of your space, engaging students from highly diverse background'
|
||||
},
|
||||
{
|
||||
src: '/teach/get-paid.svg',
|
||||
header: 'Get paid extra commission',
|
||||
subText:
|
||||
'Get paid for the course you design and earn extra comission based on course perfomance'
|
||||
},
|
||||
{
|
||||
src: '/teach/inspire-learners.svg',
|
||||
header: 'Inspire learners',
|
||||
subText:
|
||||
'Your opportunity to inspire thousands of learners to achieve dreams and academic pursue exellence'
|
||||
}
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Sign up',
|
||||
subText: 'Create a free account on our platform by going to classroomio.com/signup'
|
||||
},
|
||||
{
|
||||
title: 'Record a course',
|
||||
subText: 'Prepare your content however you want and then upload them onto our platform.'
|
||||
},
|
||||
{
|
||||
title: 'Launch the course',
|
||||
subText: 'Polish your course, make sure it is ready for students. Then publish it.'
|
||||
},
|
||||
{
|
||||
title: 'Start making sales',
|
||||
subText:
|
||||
'Share your course with your audience and on social media and start earning money on the courses you create'
|
||||
}
|
||||
];
|
||||
|
||||
let faqs = [
|
||||
{
|
||||
question: 'What kind of tutors does ClassroomIO look for?',
|
||||
answer:
|
||||
'We are looking for all kinds of tutors. The most important thing is that you have some experience teaching and you have proven track record of excellent communication',
|
||||
showAnswer: false
|
||||
},
|
||||
{
|
||||
question: 'What equipment do I need to teach on ClassroomIO?',
|
||||
answer: 'You just need internet access and a laptop.',
|
||||
showAnswer: false
|
||||
},
|
||||
{
|
||||
question: 'What happens after I apply?',
|
||||
answer:
|
||||
'We will get your application and review it. You should get a response from us within 2-5 working days depending on the volume of applications',
|
||||
showAnswer: false
|
||||
},
|
||||
{
|
||||
question: 'How much can I earn?',
|
||||
answer:
|
||||
'That depends on how much your course sells for and how many students buy your course. Your income is limitless, you can keep making money while you sleep.',
|
||||
showAnswer: false
|
||||
}
|
||||
];
|
||||
|
||||
function toggleAnswer(index: number) {
|
||||
faqs[index].showAnswer = !faqs[index].showAnswer;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Teach | ClassroomIO</title>
|
||||
<meta property="og:image" itemprop="image" content="https://brand.cdn.clsrio.com/og/teach.png" />
|
||||
<meta property="og:title" content="Teach on ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="On ClassroomIO you can make side income while doing what you love."
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/teach.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="Teach on ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="On ClassroomIO you can make side income while doing what you love."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/teach.png" />
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex items-center justify-evenly px-5 flex-wrap text-left">
|
||||
<div>
|
||||
<h1 class="text-5xl font-bold">Teach with us</h1>
|
||||
<p class="my-5 lg:w-[77%] lg:text-left text-center">
|
||||
With ClassroomIO, you can make side income while teaching the subject you love
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="/teach/register"
|
||||
target="_blank"
|
||||
class="w-full block text-center lg:w-[30%] py-3 text-base text-white rounded-md bg-blue-700"
|
||||
>
|
||||
Register
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class=" ">
|
||||
<img src="/teach/teacher-hero.png" alt="" class="w-full" />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div class="mt-10 mx-auto">
|
||||
<div class="px-[10%] pb-10 border-b border-slate-100">
|
||||
<h1 class="text-4xl pb-28 text-center font-bold flex gap-3 justify-center items-center">
|
||||
Why you should teach with us? <img src="/teach/star.svg" alt="" />
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap justify-center lg:justify-between mt-10">
|
||||
{#each whyStart as why}
|
||||
<figure
|
||||
class="flex gap-3 flex-col justify-center items-center px-2 w-full h-full max-w-[250px] max-h-[380px] text-center"
|
||||
>
|
||||
<img src={why.src} alt="" class="w-[80px] h-[80px]" />
|
||||
<figcaption>
|
||||
<h1 class="font-bold text-base">{why.header}</h1>
|
||||
<p class="text-sm mt-1">
|
||||
{why.subText}
|
||||
</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-[10%] mt-10 py-10">
|
||||
<h1 class="text-4xl mb-28 text-center font-bold flex gap-3 justify-center items-center">
|
||||
How to get started <img src="/teach/thumbs-up.svg" alt="" />
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-wrap justify-between gap-y-10 mx-auto mt-5">
|
||||
{#each steps as step, i}
|
||||
<div class="lg:max-w-[40%] px-10 py-5">
|
||||
<h1 class="font-bold text-2xl">
|
||||
<span class="px-2 py-1 mr-3 bg-[#F7F7F7] rounded-md">{i + 1}</span>
|
||||
{step.title}
|
||||
</h1>
|
||||
<p class="text-base text-gray-600 mt-5">
|
||||
{step.subText}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex justify-center px-[5%] mt-3">
|
||||
<a
|
||||
href="/teach/register"
|
||||
target="_blank"
|
||||
class="w-full lg:max-w-[20%] py-3 text-base text-white rounded-md bg-blue-700 text-center"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-2 lg:px-[12%] bg-[#F5F8FE] my-10 py-12 h-[75%] lg:h-[80%]">
|
||||
<h1 class="text-2xl text-center font-bold flex gap-3 justify-center items-center">
|
||||
Word from our fufilled facilitators
|
||||
</h1>
|
||||
|
||||
<div
|
||||
class="mt-10 w-full md:w-2/4 mx-auto border shadow-sm bg-white rounded-md text-center py-6 relative before:w-[4.5%] before:absolute before:left-0 before:top-0 before:h-full before:bg-[#0233BD] before:rounded-tl-md before:rounded-bl-md"
|
||||
>
|
||||
<h1 class="text-base font-medium leading-5 w-[77%] mx-auto">
|
||||
With ClassroomIO, I am training over 100 students. I am making more money than expected
|
||||
with little effort and my students are happy. Love the product
|
||||
</h1>
|
||||
<div class="flex flex-row gap-4 justify-center items-center mt-3">
|
||||
<img src="/teach/user-profile.png" alt="" class="w-8" />
|
||||
<p class="text-sm">
|
||||
<span class="font-semibold mr-2"> Bilik Diana </span> English tutor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-10 lg:px-[15%] lg:max-w-[65%] my-10 mt-10 mx-auto">
|
||||
<h1 class="text-2xl text-center font-bold">Frequently asked questions</h1>
|
||||
|
||||
<div class="mt-10">
|
||||
{#each faqs as faq, index}
|
||||
<div class="border-b border-black lg:px-3 pt-1 pb-2">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full font-medium text-sm md:text-lg my-2 flex justify-between text-left md:text-center"
|
||||
on:click={() => toggleAnswer(index)}
|
||||
>
|
||||
{faq.question} <span>{faq.showAnswer ? '-' : '+'}</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if faq.showAnswer}
|
||||
<div transition:slide class="text-base font-light leading-5">{faq.answer}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Kick off your Teaching Business in Minutes"
|
||||
subText="Don't wait, let's get you started."
|
||||
btnLabel="Register"
|
||||
link="/teach/register"
|
||||
demo={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = ({ request }) => {
|
||||
redirect(307, 'https://heyform.net/f/1qkguGkN');
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Free Tools | ClassroomIO</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
<meta property="og:title" content="ClassroomIO Free Tools" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="At ClassroomIO, we have built a suit of tools for anyone to use freely while collaborating with students"
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="ClassroomIO Free Tools" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="At ClassroomIO, we have built a suit of tools for anyone to use freely while collaborating with students"
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
|
||||
<PageSignupCTA
|
||||
header="Scale Your Bootcamp Today with ClassroomIO"
|
||||
subText="ClassroomIO gives small and medium bootcamps the power of online learning, simplified."
|
||||
btnLabel="Sign me up"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
import { tools } from '$lib/utils/constants/tools';
|
||||
|
||||
const freeTools = tools.filter((item) => item.show);
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<PageHeader>
|
||||
<!-- added bg-white to keep the background white on devices that are on darkmode -->
|
||||
<div class="bg-white text-left md:text-center md:w-2/4 px-7 md:p-0 mx-auto">
|
||||
<h1 class="text-5xl md:text-6xl font-bold">
|
||||
Explore our free <span class="text-blue-700">tools</span>
|
||||
</h1>
|
||||
<p class="my-4 md:my-8">
|
||||
Innovative tools to help you aid you example preparation, as well as making learning more
|
||||
fun for students and educator looking for engaging resources.
|
||||
</p>
|
||||
|
||||
<div class="flex relative w-[90%] md:w-[50%] my-3 md:mt-10 mx-auto bg-white">
|
||||
<!-- <img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-0 z-0"
|
||||
src="/free-tools/waec.svg"
|
||||
alt="Waec logo"
|
||||
/> -->
|
||||
<img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[55px] z-10"
|
||||
src="/free-tools/progress-report.svg"
|
||||
alt="Progress Report"
|
||||
/>
|
||||
<img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[100px] z-20"
|
||||
src="/free-tools/activity-stopwatch.svg"
|
||||
alt="Activity Stopwatch logo"
|
||||
/>
|
||||
<img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[150px] z-30"
|
||||
src="/free-tools/name-picker.svg"
|
||||
alt="Name picker logo"
|
||||
/>
|
||||
<img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[200px] z-40"
|
||||
src="/free-tools/tic-tac.svg"
|
||||
alt="Tic tac logo"
|
||||
/>
|
||||
<img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[250px] z-50 bg-white"
|
||||
src="/free-tools/pomodoro.svg"
|
||||
alt="Pomodoro Timer logo"
|
||||
/>
|
||||
<!-- <img
|
||||
class="w-[18%] rounded-full border hover:scale-110 transition-all hover:z-50 absolute left-[250px] z-50"
|
||||
src="/free-tools/jamb.svg"
|
||||
alt="Jamb logo"
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<div class="p-10 px-2 md:px-0 bg-white">
|
||||
<h1 class="text-center font-bold text-3xl">Featured Tools</h1>
|
||||
|
||||
<div class="flex justify-evenly gap-y-8 flex-wrap w-full md:w-[70%] mt-10 mx-auto bg-white">
|
||||
{#each freeTools as tool}
|
||||
<a
|
||||
href="/tools/{tool.slug}"
|
||||
class="w-full flex md:max-w-[370px] border rounded-md shadow-sm hover:scale-95 transition-all ease-in-out"
|
||||
>
|
||||
<img class="w-[30%] rounded-l-md" src={tool.src} alt="" />
|
||||
<div class="bg-[#F5F8FE] p-5 rounded-r-md w-[70%]">
|
||||
<h1 class="font-bold text-sm md:text-base">{tool.title}</h1>
|
||||
<p class="text-xs text-gray-600 mt-1.5">
|
||||
{tool.subText}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,258 @@
|
||||
<script lang="ts">
|
||||
import ToolsHeader from '$lib/ToolsHeader/ToolsHeader.svelte';
|
||||
|
||||
let namesInput = '';
|
||||
let numNames = 0;
|
||||
let wordCount = 0;
|
||||
let selectedNames: string[] = [];
|
||||
let avatarUrls: string[] = [];
|
||||
let avatarUrlsFetched = false;
|
||||
|
||||
// function to shuffle an array
|
||||
const shuffleArray = (array: string[]) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
// function to reset entries
|
||||
const resetEntry = () => {
|
||||
namesInput = '';
|
||||
numNames = 0;
|
||||
wordCount = 0;
|
||||
selectedNames = [];
|
||||
};
|
||||
|
||||
// function to sort the input alphabetically
|
||||
const sortRandom = () => {
|
||||
const sortedNames = namesInput
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
namesInput = sortedNames.join(', ');
|
||||
};
|
||||
|
||||
// function to unsort
|
||||
const unsortRandom = () => {
|
||||
const unsortedNames = namesInput
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.sort(() => 0.5 - Math.random());
|
||||
namesInput = unsortedNames.join(', ');
|
||||
};
|
||||
|
||||
const selectName = async () => {
|
||||
const namesArray = namesInput.split(',').map((name) => name.trim());
|
||||
|
||||
if (numNames > namesArray.length) {
|
||||
console.error('Number of names to pick exceeds the length of the name list.');
|
||||
return;
|
||||
}
|
||||
|
||||
wordCount = namesArray.length;
|
||||
const shuffledNames = shuffleArray(namesArray);
|
||||
|
||||
selectedNames = shuffledNames.slice(0, numNames);
|
||||
avatarUrls = []; // reset avatar URLs
|
||||
avatarUrlsFetched = false; // reset avatarUrlsFetched flag
|
||||
|
||||
try {
|
||||
// set loading state
|
||||
avatarUrlsFetched = false;
|
||||
|
||||
// generate avatar for each selected name
|
||||
for (const name of selectedNames) {
|
||||
const avatarUrl = await generateRandomAvatar('pixel-art');
|
||||
if (avatarUrl) {
|
||||
avatarUrls.push(avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// set avatarUrlsFetched to true after avatars are fetched
|
||||
avatarUrlsFetched = true;
|
||||
} catch (error) {
|
||||
console.error('Error fetching avatars:', error);
|
||||
avatarUrlsFetched = true;
|
||||
}
|
||||
};
|
||||
|
||||
// function to generate a random avatar
|
||||
async function generateRandomAvatar(styleName: string, format = 'svg') {
|
||||
const baseUrl = 'https://api.dicebear.com/8.x/';
|
||||
const randomSeed = Math.random().toString(36).substring(7);
|
||||
|
||||
try {
|
||||
const apiUrl = `${baseUrl}${styleName}/svg?seed=${randomSeed}`;
|
||||
const response = await fetch(apiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch avatar');
|
||||
}
|
||||
|
||||
return response.url;
|
||||
} catch (error) {
|
||||
console.error('Error generating random avatar:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$: wordCount = namesInput?.split(',').filter((name) => name.trim())?.length ?? 0;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Random Name Picker | ClassroomIO</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
<meta property="og:title" content="Random Name Picker | ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Use this online name picker to draw a random name from a list of names for your online or physical classroom."
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="Random Name Picker | ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Use this online name picker to draw a random name from a list of names for your online or physical classroom."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="mt-[10%] md:mt-16 px-5 md:px-0 bg-white">
|
||||
<ToolsHeader>
|
||||
<img
|
||||
src="/free-tools/name-picker.svg"
|
||||
class="w-[15%] md:w-[5%] mx-auto border rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-[#040F2D] my-3">Random Name Picker</h1>
|
||||
<p class="text-md text-[#656565] font-light md:font-normal md:w-[45%] mx-auto">
|
||||
Use this online name picker to draw a random name from a list of names, or to draw several
|
||||
names randomly out of a list. You can use it as a name randomizer for a class activities.
|
||||
</p>
|
||||
</ToolsHeader>
|
||||
|
||||
<div class="border rounded-md md:w-[60%] my-10 py-8 md:py-[3%] mx-auto shadow-md bg-white">
|
||||
<div class="w-[85%] md:w-[70%] mx-auto bg-white">
|
||||
<h1 class="font-bold text-sm">List of names</h1>
|
||||
|
||||
<!-- container -->
|
||||
<div
|
||||
class="relative focus-within:border-[#0233BD] border-2 mt-5 h-[12rem] pl-3 pt-2 overflow-hidden rounded-sm bg-[#F1F2F4]"
|
||||
>
|
||||
<!-- sidebar -->
|
||||
<div class="flex flex-col gap-y-3 absolute right-2 md:right-7 top-3">
|
||||
<button
|
||||
type="button"
|
||||
on:click={sortRandom}
|
||||
class="p-1.5 rounded-full bg-[#D9E0F5] flex justify-center items-center"
|
||||
>
|
||||
<img src="/sort-icon.svg" class="w-2.5" alt="" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={unsortRandom}
|
||||
class="p-1.5 rounded-full bg-[#D9E0F5] flex justify-center items-center"
|
||||
>
|
||||
<img src="/unsort-icon.svg" class="w-2.5" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class="z-10 h-[80%]">
|
||||
<span
|
||||
role="textbox"
|
||||
tabindex={0}
|
||||
contenteditable
|
||||
bind:textContent={namesInput}
|
||||
class="w-full pr-9 md:pr-14 overflow-y-auto text-xs block border-0 outline-none h-full"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<!-- word count -->
|
||||
<p
|
||||
class="rounded-xl w-[80px] mt-1 py-1 bg-[#D9E0F5] text-center text-[#0F62FE] text-[10px] font-semibold"
|
||||
>
|
||||
name count: {wordCount}
|
||||
</p>
|
||||
</div>
|
||||
<p class="py-4 border-b text-xs">Each name must be separated by a comma</p>
|
||||
|
||||
<div class="mt-10">
|
||||
<h1 class="font-bold text-sm">Number of Names</h1>
|
||||
<!-- number input & button -->
|
||||
<div class="flex flex-wrap gap-y-3 justify-between w-full mt-5">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={numNames}
|
||||
class="px-3 py-2 text-sm w-full md:w-2/4 border-2 rounded-sm outline focus:border-[#0233BD] bg-[#F1F2F4] outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
on:click={selectName}
|
||||
class="bg-[#0F62FE] rounded-md text-sm py-3 md:py-0 text-white w-[70%] md:w-[35%]"
|
||||
>Select name(s) at random</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- footer logo -->
|
||||
<div class="py-3 mt-10 mb-5 bg-[#F1F6FF] flex justify-center items-center gap-3">
|
||||
<img src="/free-tools/name-picker.svg" class="w-[12%] md:w-[9%]" alt="" />
|
||||
{#if selectedNames.length > 0}
|
||||
<h1 class="text-[#0233BD] text-xl font-bold">Selected Names</h1>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- loading state -->
|
||||
{#if selectedNames.length > 0 && !avatarUrlsFetched}
|
||||
<div class="w-full flex justify-center items-center">
|
||||
<p class="font-bold">Loading...</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- generated name -->
|
||||
{#if selectedNames.length > 0 && avatarUrlsFetched}
|
||||
<div>
|
||||
<ul>
|
||||
{#each selectedNames as name, index}
|
||||
<li class="py-2 border-b">
|
||||
<div class="flex item-center gap-3 md:gap-5 justify-center">
|
||||
<img src={avatarUrls[index]} alt="" class="w-7" />
|
||||
<p class="font-medium text-sm md:text-base flex justify-center items-center">
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="flex flex-wrap justify-between md:justify-center items-center md:gap-3 mt-3">
|
||||
<button
|
||||
type="button"
|
||||
on:click={resetEntry}
|
||||
class="py-2 border text-xs md:text-sm rounded-md w-[45%] md:w-[20%]"
|
||||
>Reset Entries</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={selectName}
|
||||
class="py-2 bg-[#0F62FE] rounded-md text-xs md:text-sm text-white w-[45%] md:w-[27%]"
|
||||
>Select other names</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,393 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ToolsHeader from '$lib/ToolsHeader/ToolsHeader.svelte';
|
||||
|
||||
interface TodoItem {
|
||||
id: number;
|
||||
content: string;
|
||||
time: string;
|
||||
isPaused: boolean;
|
||||
isEditing: boolean;
|
||||
isDone: boolean;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
let addInput: HTMLInputElement;
|
||||
let buzzSound: HTMLAudioElement;
|
||||
let todoList: TodoItem[] = [];
|
||||
|
||||
let countdownTime: number = 25 * 60;
|
||||
let countdownDisplay: string = '25:00';
|
||||
let timerState: string = 'pomodoro';
|
||||
|
||||
let isPaused: boolean = true;
|
||||
let timerInterval: any = null;
|
||||
let isVisible: boolean = false;
|
||||
|
||||
const timerSequence = ['pomodoro', 'long-break', 'short-break'];
|
||||
|
||||
function saveTodoList() {
|
||||
localStorage.setItem('todoList', JSON.stringify(todoList));
|
||||
}
|
||||
|
||||
function loadTodoList() {
|
||||
const storedTodoList = localStorage.getItem('todoList');
|
||||
if (storedTodoList) {
|
||||
todoList = JSON.parse(storedTodoList);
|
||||
}
|
||||
}
|
||||
|
||||
function setEditing(i: number, isEditing: boolean) {
|
||||
todoList[i].isEditing = isEditing;
|
||||
saveTodoList();
|
||||
}
|
||||
|
||||
function deleteTodo(i: number) {
|
||||
todoList.splice(i, 1);
|
||||
todoList = todoList;
|
||||
isVisible = false;
|
||||
saveTodoList();
|
||||
}
|
||||
|
||||
function markTodoAsDone(i: number) {
|
||||
todoList[i].isDone = true;
|
||||
todoList = [...todoList];
|
||||
todoList[i].isVisible = false;
|
||||
saveTodoList();
|
||||
}
|
||||
|
||||
function addTodo() {
|
||||
todoList = [
|
||||
...todoList,
|
||||
{
|
||||
id: new Date().getMilliseconds(),
|
||||
content: '',
|
||||
isEditing: true,
|
||||
time: '25:00',
|
||||
isPaused: true,
|
||||
isDone: false,
|
||||
isVisible: false
|
||||
}
|
||||
];
|
||||
|
||||
setTimeout(() => {
|
||||
addInput.focus();
|
||||
}, 50);
|
||||
|
||||
saveTodoList();
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (!isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPaused = false;
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
if (countdownTime > 0) {
|
||||
countdownTime--;
|
||||
updateCountdownDisplay();
|
||||
} else {
|
||||
clearInterval(timerInterval);
|
||||
countdownDisplay = '00:00';
|
||||
buzzSound.play();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function pauseCountdown() {
|
||||
if (isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPaused = true;
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
|
||||
function resetCountdown() {
|
||||
clearInterval(timerInterval);
|
||||
|
||||
switch (timerState) {
|
||||
case 'pomodoro':
|
||||
countdownTime = 25 * 60;
|
||||
break;
|
||||
case 'short-break':
|
||||
countdownTime = 15 * 60;
|
||||
break;
|
||||
case 'long-break':
|
||||
countdownTime = 5 * 60;
|
||||
break;
|
||||
}
|
||||
|
||||
isPaused = true;
|
||||
|
||||
updateCountdownDisplay();
|
||||
}
|
||||
|
||||
function nextTimerState() {
|
||||
const currentIndex = timerSequence.indexOf(timerState);
|
||||
const nextIndex = (currentIndex + 1) % timerSequence.length;
|
||||
setTimerState(timerSequence[nextIndex]);
|
||||
}
|
||||
|
||||
function updateCountdownDisplay() {
|
||||
const minutes = Math.floor(countdownTime / 60);
|
||||
const seconds = countdownTime % 60;
|
||||
countdownDisplay = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function setTimerState(state: string) {
|
||||
timerState = state;
|
||||
resetCountdown();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
buzzSound = new Audio('https://assets.cdn.clsrio.com/beeping-sound.mp3');
|
||||
loadTodoList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Pomodoro Timer | ClassroomIO</title>
|
||||
<meta property="og:image" itemprop="image" content="" />
|
||||
<meta property="og:title" content="Pomodoro Timer | ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Make your workday more engaging and effective with the Pomodoro timer, break work into 25-minute focused intervals called 'pomodoros', followed by 5-minute breaks"
|
||||
/>
|
||||
|
||||
<meta property="og:image:secure_url" itemprop="image" content="" />
|
||||
|
||||
<meta name="twitter:title" content="Pomodoro Timer | ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Make your workday more engaging and effective with the Pomodoro timer, break work into 25-minute focused intervals called 'pomodoros', followed by 5-minute breaks"
|
||||
/>
|
||||
<meta name="twitter:image" content="" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="mt-[10%] md:mt-16 px-1 md:px-0 w-full md:w-full bg-white">
|
||||
<ToolsHeader>
|
||||
<img
|
||||
src="/free-tools/pomodoro.svg"
|
||||
class="w-[15%] md:w-[5%] mx-auto border rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-[#040F2D] my-3">Pomodoro Timer</h1>
|
||||
<p class="text-sm mt-10 text-[#656565] font-light md:font-normal md:w-[45%] mx-auto">
|
||||
Make your workday more engaging and effective with the Pomodoro timer, break work into
|
||||
25-minute focused intervals called "pomodoros," followed by 5-minute breaks. take a longer
|
||||
break.
|
||||
</p>
|
||||
</ToolsHeader>
|
||||
|
||||
<div
|
||||
class="body w-full md:h-[60vh] flex flex-col gap-y-10 md:flex-row justify-evenly px-5 md:px-10 my-10 text-white text-center"
|
||||
>
|
||||
<!-- left side -->
|
||||
<div
|
||||
class="w-full md:w-[50%] bg-[#0D4CFF] rounded-lg md:rounded-2xl py-7 px-5 md:p-10 flex flex-col gap-y-16 md:gap-y-0 justify-between"
|
||||
>
|
||||
<!-- types -->
|
||||
<div class="flex item-center justify-evenly">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => setTimerState('pomodoro')}
|
||||
class:bg-[#3ADFECED]={timerState === 'pomodoro'}
|
||||
class:bg-[#0233BD]={timerState !== 'pomodoro'}
|
||||
class="transition-all duration-500 italic py-1.5 md:py-2 w-[25%] text-xs md:text-sm md:font-medium rounded-md hover:bg-[#3ADFECED]"
|
||||
>Pomodoro</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => setTimerState('long-break')}
|
||||
class:bg-[#3ADFECED]={timerState === 'long-break'}
|
||||
class:bg-[#0233BD]={timerState !== 'long-break'}
|
||||
class="transition-all duration-500 italic py-1.5 md:py-2 w-[25%] text-xs md:text-sm md:font-medium rounded-md hover:bg-[#3ADFECED]"
|
||||
>Short Break
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => setTimerState('short-break')}
|
||||
class:bg-[#3ADFECED]={timerState === 'short-break'}
|
||||
class:bg-[#0233BD]={timerState !== 'short-break'}
|
||||
class="transition-all duration-500 italic py-1.5 md:py-2 w-[25%] text-xs md:text-sm md:font-medium rounded-md hover:bg-[#3ADFECED]"
|
||||
>Long Break
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- countdown -->
|
||||
<h1 class="text-7xl md:text-9xl font-bold">{countdownDisplay}</h1>
|
||||
|
||||
<!-- controls -->
|
||||
<div class="flex items-center justify-center gap-7">
|
||||
<!-- reset -->
|
||||
<button type="button" on:click={resetCountdown}>
|
||||
<img
|
||||
src="/free-tools/pomodoro/restart-icon.svg"
|
||||
alt="Restart Icon"
|
||||
class="w-7 hover:scale-110 transition-all duration-300"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- start (i intentionally disabled this button once the user clicks start so they can use the pause button to actually pause the countdown) -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={isPaused ? startCountdown : pauseCountdown}
|
||||
class="bg-white text-[#0542CC] border text-base font-bold uppercase py-3 px-14 rounded-md hover:bg-transparent hover:text-white hover:border-white hover:border transition-all duration-300"
|
||||
>
|
||||
{isPaused ? 'Start' : 'Pause'}
|
||||
</button>
|
||||
|
||||
<!-- next -->
|
||||
<button type="button" on:click={nextTimerState}>
|
||||
<img
|
||||
src="/free-tools/pomodoro/timer-play-icon.svg"
|
||||
alt="Play Icon"
|
||||
class="w-7 hover:scale-110 transition-all duration-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- right side -->
|
||||
<div class="w-full md:w-[36%] relative">
|
||||
<h1
|
||||
class="bg-[#040F2D] uppercase text-xl font-bold py-4"
|
||||
style="clip-path: polygon(0 0, 100% 1%, 100% 65%, 0% 100%);"
|
||||
>
|
||||
To do list
|
||||
</h1>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<!-- todos -->
|
||||
<div class="overflow-y-auto mt-3 max-h-[40vh] md:max-h-[50vh]">
|
||||
{#each todoList as todo, i}
|
||||
<div class="border text-black p-5">
|
||||
{#if todo.isEditing}
|
||||
<form on:submit|preventDefault={() => setEditing(i, false)}>
|
||||
<p class="text-xs text-left text-[#656565] font-semibold">Pomodoro name</p>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={addInput}
|
||||
bind:value={todo.content}
|
||||
class="bg-[#F1F2F4] border-[#EAEAEA] w-full border mt-2 px-3 py-2 text-base outline-none"
|
||||
/>
|
||||
<div class="flex justify-end items-center gap-5 mt-5">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => deleteTodo(i)}
|
||||
class="rounded-sm py-2 px-5 text-xs font-semibold bg-[#F7F7F7]"
|
||||
>Discard</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-sm py-2 px-5 text-xs font-semibold bg-[#0D4CFF] text-white"
|
||||
>Save</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="">
|
||||
<div class="flex justify-between">
|
||||
<p class="text-sm font-medium">{todo.content}</p>
|
||||
|
||||
<!-- pen and menu icon -->
|
||||
<div class="relative flex justify-between w-[15%]">
|
||||
<button type="button" on:click={() => setEditing(i, true)}>
|
||||
<img src="/free-tools/pomodoro/pen-icon.svg" alt="Pen icon" class="w-5" />
|
||||
</button>
|
||||
|
||||
<button type="button" on:click={() => (todo.isVisible = !todo.isVisible)}>
|
||||
<img src="/free-tools/pomodoro/menu-icon.svg" alt="Menu icon" class="w-5" />
|
||||
</button>
|
||||
|
||||
{#if todo.isVisible}
|
||||
<ul
|
||||
class="z-40 absolute top-6 -right-3 text-xs w-[8rem] bg-white border shadow-md rounded-[5px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => markTodoAsDone(i)}
|
||||
class="w-full font-medium text-left py-3 px-5"
|
||||
>
|
||||
Mark as done
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => deleteTodo(i)}
|
||||
class="w-full font-medium text-left py-3 px-5 bg-red-600 text-white rounded-b-[5px]"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-10">
|
||||
<p
|
||||
class="rounded-[3px] text-xs font-medium text-[#656565] bg-[#EAEAEA] py-0.5 px-1.5"
|
||||
>
|
||||
{todo.time}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (todo.isPaused) {
|
||||
startCountdown();
|
||||
} else {
|
||||
pauseCountdown();
|
||||
}
|
||||
|
||||
todo.isPaused = !todo.isPaused;
|
||||
}}
|
||||
class="{todo.isDone
|
||||
? 'bg-[#00D06C] border-none text-white'
|
||||
: todo.isPaused
|
||||
? 'bg-white text-[#0D4CFF] border-[#0D4CFF]'
|
||||
: 'bg-[#0D4CFF] text-white border-none'}
|
||||
|
||||
flex items-center border-2 justify-center gap-2 py-1.5 px-4 rounded-[4px] text-xs font-bold uppercase"
|
||||
>
|
||||
{#if todo.isDone}
|
||||
<img src="/free-tools/pomodoro/done-icon.svg" alt="Done icon" />
|
||||
done
|
||||
{:else if todo.isPaused}
|
||||
<img src="/free-tools/pomodoro/list-play-icon.svg" alt="Play icon" />
|
||||
play task
|
||||
{:else}
|
||||
<img src="/free-tools/pomodoro/pause-icon.svg" alt="Pause icon" />
|
||||
pause task
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- todo button -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={addTodo}
|
||||
class="bg-[#1D4ED8] rounded-md text-center font-medium mt-10 py-3 w-full flex justify-center items-center gap-3"
|
||||
>
|
||||
Add new item
|
||||
<img src="/free-tools/pomodoro/add-icon.svg" alt="Add icon" class="w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap');
|
||||
|
||||
.body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,426 @@
|
||||
<script lang="ts">
|
||||
import { LogoFacebook, LogoLinkedin } from 'carbon-icons-svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
import Mood from './components/Mood.svelte';
|
||||
import ToolsHeader from '$lib/ToolsHeader/ToolsHeader.svelte';
|
||||
import Avatar from './components/Avatar.svelte';
|
||||
import Background from './components/Background.svelte';
|
||||
import Report from './components/Report.svelte';
|
||||
import FullView from './components/FullView.svelte';
|
||||
import DownloadButton from './components/DownloadButton.svelte';
|
||||
import { htmlBody, type HtmlBody, openModal, isFormComplete } from './components/store';
|
||||
|
||||
const MAX_CHARS = 160;
|
||||
let remainingChars = MAX_CHARS;
|
||||
let showReport: boolean = false;
|
||||
let showSetter: boolean = true;
|
||||
let isDownloading: boolean = false;
|
||||
let isDisabled: boolean;
|
||||
|
||||
// sets the result of the mini validation for the htmlBody store
|
||||
const unsubscribe = isFormComplete.subscribe((value) => {
|
||||
isDisabled = value;
|
||||
});
|
||||
|
||||
function handleInputChange(event: any, property: keyof HtmlBody) {
|
||||
$htmlBody[property] = event.target.value;
|
||||
|
||||
if (property === 'learning') {
|
||||
remainingChars = MAX_CHARS - event.target.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
function shareTemplate() {
|
||||
return encodeURIComponent(
|
||||
`Learning Progress Update: ${$htmlBody.learning} and I'm ${$htmlBody.mood.text}. \nGenerated yours on classroomio.com\n`
|
||||
);
|
||||
}
|
||||
|
||||
function shareOnTwitter() {
|
||||
const text = shareTemplate();
|
||||
const hashtags = encodeURIComponent('classroomIO,progressReport');
|
||||
const url = `https://x.com/intent/post?text=${text}&hashtags=${hashtags}`;
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
function shareOnFacebook() {
|
||||
const text = shareTemplate();
|
||||
window.open(
|
||||
`https://www.facebook.com/dialog/share?app_id=145634995501895&display=popup&href=https%3A%2F%2Fclassroomio.com%2Fdocs%2F&redirect_uri=https%3A%2F%2Fclassroomio.com%2Ftools%2Fexplorer&hashtag=${text}`
|
||||
);
|
||||
}
|
||||
|
||||
function shareOnLinkedIn() {
|
||||
const text = shareTemplate();
|
||||
const url = `http://www.linkedin.com/shareArticle?summary=${text}&text=${text}`;
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Random Name Picker | ClassroomIO</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
<meta property="og:title" content="Random Name Picker | ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Use this online name picker to draw a random name from a list of names for your online or physical classroom."
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="Random Name Picker | ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Use this online name picker to draw a random name from a list of names for your online or physical classroom."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="mt-[10%] md:mt-16 px-1 md:px-0 w-full md:w-full">
|
||||
<ToolsHeader>
|
||||
<img
|
||||
src="/free-tools/progress-report.svg"
|
||||
class="w-[15%] md:w-[5%] mx-auto border rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-[#040F2D] my-3">Progress Report</h1>
|
||||
<p class="text-md text-[#656565] font-light md:font-normal md:w-[45%] mx-auto">
|
||||
Generate cool reports of your learning progress. Share reports with your network for
|
||||
collaborative learning
|
||||
</p>
|
||||
</ToolsHeader>
|
||||
|
||||
<!-- modals -->
|
||||
<Avatar />
|
||||
<Mood />
|
||||
<Background />
|
||||
<FullView />
|
||||
|
||||
<div
|
||||
class="border rounded-md w-full md:w-11/12 lg:w-[80%] my-10 mx-auto shadow-md flex md:flex-row flex-col justify-evenly"
|
||||
>
|
||||
<!-- left side -->
|
||||
{#if showSetter}
|
||||
<div
|
||||
transition:fly={{ y: 100, easing: sineInOut }}
|
||||
class="w-full mx-auto md:w-[48%] md:max-w-[500px] p-5 md:border-r"
|
||||
>
|
||||
<!-- preview button -->
|
||||
<div class="flex md:hidden justify-end mb-5">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showSetter = false;
|
||||
showReport = true;
|
||||
}}
|
||||
class="flex justify-between items-center gap-2 px-3 py-1 rounded-full text-white text-xs bg-[#0233BD]"
|
||||
>
|
||||
<img
|
||||
src="/free-tools/progress-report/preview-icon.svg"
|
||||
alt="preview icon"
|
||||
class="w-4"
|
||||
/>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- name input -->
|
||||
<div>
|
||||
<p class="text-sm text-[#656565]">Add your name</p>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$htmlBody.name}
|
||||
on:input={(event) => handleInputChange(event, 'name')}
|
||||
placeholder="Enter your name here"
|
||||
class="w-full border my-3 py-2 px-3 outline-none rounded-sm bg-[#F1F2F4] text-xs placeholder:text-sm placeholder:text-[#ADADAD]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- mood input -->
|
||||
<div class="mt-3">
|
||||
<p class="text-sm text-[#656565]">Select your mood</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.mood = true)}
|
||||
class="w-full flex justify-between items-center border my-3 py-2 px-3 outline-none rounded-sm bg-[#F1F2F4] text-gray-400 text-sm"
|
||||
>
|
||||
{#if $htmlBody.mood.text}
|
||||
<div
|
||||
class="px-3 py-1 border border-[#EDEDED] text-white bg-blue-500 rounded-2xl font-semibold text-[7px] md:text-xs flex items-center justify-between"
|
||||
>
|
||||
{#if $htmlBody.mood.text}
|
||||
<span class="flex gap-1 items-center">
|
||||
<p class="font-semibold text-[9px] md:text-xs">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}
|
||||
What feeling describes you?
|
||||
<img
|
||||
src="/free-tools/progress-report/smiley-face-icon.svg"
|
||||
alt="smiley face"
|
||||
class="w-3"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- learning input -->
|
||||
<div class="mt-5">
|
||||
<p class="text-sm text-[#656565]">What are you learning?</p>
|
||||
<textarea
|
||||
bind:value={$htmlBody.learning}
|
||||
maxlength={MAX_CHARS}
|
||||
on:input={(event) => handleInputChange(event, 'learning')}
|
||||
placeholder="Tell us what you are learning"
|
||||
class="w-full h-[17vh] border my-3 py-2 px-3 outline-none rounded-sm bg-[#F1F2F4] text-sm placeholder:text-sm placeholder:text-[#ADADAD]"
|
||||
></textarea>
|
||||
<p class="text-xs text-right text-[#656565]">{remainingChars} characters remaining</p>
|
||||
</div>
|
||||
|
||||
<!-- range input -->
|
||||
<div class="mt-3">
|
||||
<p class="text-sm text-[#656565] pb-4">Estimate your progress</p>
|
||||
<div class="flex justify-between items-center">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
bind:value={$htmlBody.progress}
|
||||
class="range-input"
|
||||
on:input={(event) => handleInputChange(event, 'progress')}
|
||||
style="background: linear-gradient(to right, #0F62FE {$htmlBody.progress}%, #ccc {$htmlBody.progress}%);"
|
||||
/>
|
||||
<p class="text-sm font-semibold">{$htmlBody.progress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- avatar button -->
|
||||
<div class="flex flex-wrap justify-between items-center mt-3">
|
||||
<div class="w-full md:w-2/4">
|
||||
<p class="text-[12px] md:text-sm text-[#656565]">Choose your avatar</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.avatar = true)}
|
||||
class="bg-[#F7F7F7] w-full py-1.5 px-5 md:px-0 mt-3 flex md:w-[65%] items-center justify-between md:justify-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={`https://assets.cdn.clsrio.com/progress-report/avatars/${
|
||||
$htmlBody.avatar || 'avatar_m'
|
||||
}.svg`}
|
||||
alt=""
|
||||
class="w-[15%] md:w-[30%]"
|
||||
/>
|
||||
<img
|
||||
src="/free-tools/progress-report/question-mark.svg"
|
||||
alt="A question mark"
|
||||
class="w-[15%] md:w-[30%]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- & background -->
|
||||
<div class="w-full md:w-2/4">
|
||||
<p class="text-[12px] md:text-sm text-[#656565]">Choose your background</p>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.background = true)}
|
||||
class="bg-[#F7F7F7] w-full py-1.5 px-5 md:px-0 mt-3 flex md:w-[65%] items-center justify-between md:justify-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={`https://assets.cdn.clsrio.com/progress-report/backgrounds/${
|
||||
$htmlBody.background || 'blue_tetiary_background'
|
||||
}.webp`}
|
||||
alt="An avatar"
|
||||
class="w-[15%] md:w-[30%]"
|
||||
/>
|
||||
<img
|
||||
src="/free-tools/progress-report/question-mark.svg"
|
||||
alt="A question mark"
|
||||
class="w-[15%] md:w-[30%]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DownloadButton {isDisabled} {isDownloading} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- right side -->
|
||||
|
||||
<div
|
||||
transition:fly={{ y: 100, easing: sineInOut }}
|
||||
class="w-full mx-auto md:w-[48%] md:min-w-[400px] md:max-w-[500px] px-5 md:px-1 md:mt-0 mt-5 md:p-5"
|
||||
>
|
||||
<div class="hidden md:block">
|
||||
<Report />
|
||||
|
||||
<!-- download & share button -->
|
||||
<div class="mt-9 pt-8 px-2 h-auto border-t">
|
||||
<DownloadButton {isDisabled} {isDownloading} text="Download Image" />
|
||||
|
||||
<!-- share button -->
|
||||
<div class="w-full md:w-[70%] mx-auto my-5">
|
||||
<h1 class="text-sm font-semibold text-center">Share on image on social media:</h1>
|
||||
|
||||
<div
|
||||
class="flex justify-evenly border rounded-xl gap-5 w-[60%] md:w-[80%] px-5 py-2 mt-2 mx-auto"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnFacebook}
|
||||
class="w-10 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<LogoFacebook size={32} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnLinkedIn}
|
||||
class="w-10 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<LogoLinkedin size={32} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnTwitter}
|
||||
class="w-5 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<img src="/twitter_logo.png" alt="X.com" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showReport}
|
||||
<!-- hide button -->
|
||||
<div class="flex justify-end mb-5">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showReport = false;
|
||||
showSetter = true;
|
||||
}}
|
||||
class="flex justify-between items-center gap-2 px-3 py-1 rounded-full text-white text-xs bg-[#0233BD]"
|
||||
>
|
||||
<img src="/free-tools/progress-report/hide-icon.svg" alt="preview icon" class="w-4" />
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<Report />
|
||||
|
||||
<!-- download & share button -->
|
||||
<div class="mt-9 pt-8 px-2 h-auto border-t">
|
||||
<DownloadButton {isDisabled} {isDownloading} />
|
||||
|
||||
<!-- share button -->
|
||||
<div class="w-full md:w-[70%] mx-auto my-5">
|
||||
<h1 class="text-sm font-semibold text-center">Share on image on social media:</h1>
|
||||
|
||||
<div
|
||||
class="flex justify-evenly border rounded-xl gap-5 w-[60%] md:w-[80%] px-5 py-2 mt-2 mx-auto"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnFacebook}
|
||||
class="w-10 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<LogoFacebook size={32} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnLinkedIn}
|
||||
class="w-10 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<LogoLinkedin size={32} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={shareOnTwitter}
|
||||
class="w-5 hover:scale-[1.2] transition-all duration-300"
|
||||
>
|
||||
<img src="/twitter_logo.png" alt="X.com" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.range-input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 80%;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border-radius: 15px;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.range-input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
background-color: #0f62fe;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.range-input::-moz-range-thumb {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
background-color: yellow;
|
||||
border-radius: 50%;
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.range-input:active::-webkit-slider-thumb {
|
||||
border: 3px solid #fff;
|
||||
filter: drop-shadow(0px 1px 1px #000000);
|
||||
}
|
||||
|
||||
.range-input:focus::-webkit-slider-thumb {
|
||||
border: 3px solid #fff;
|
||||
filter: drop-shadow(0px 1px 1px #000000);
|
||||
}
|
||||
|
||||
.range-input::-moz-range-thumb:hover {
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 0 0 10px rgba(255, 85, 0, 0.1);
|
||||
}
|
||||
|
||||
.range-input:active::-moz-range-thumb {
|
||||
border: 3px solid #fff;
|
||||
filter: drop-shadow(0px 1px 1px #000000);
|
||||
}
|
||||
|
||||
.range-input:focus::-moz-range-thumb {
|
||||
border: 3px solid #fff;
|
||||
filter: drop-shadow(0px 1px 1px #000000);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { openModal, htmlBody } from './store';
|
||||
import Modal from '$lib/Modal/Modal.svelte';
|
||||
|
||||
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>
|
||||
|
||||
<Modal bind:open={$openModal.avatar}>
|
||||
<!-- heading -->
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-sm font-semibold">Choose your Avatars</h1>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.avatar = false)}
|
||||
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
|
||||
>
|
||||
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- -->
|
||||
</Modal>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/Modal/Modal.svelte';
|
||||
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>
|
||||
|
||||
<Modal className="h-full max-h-[60vh]" bind:open={$openModal.background}>
|
||||
<!-- -->
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-sm font-semibold">Choose your Background</h1>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.background = false)}
|
||||
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
|
||||
>
|
||||
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- -->
|
||||
<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>
|
||||
<!-- -->
|
||||
</Modal>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { toPng } from 'html-to-image';
|
||||
import { nodeStore, htmlBody, openModal } from './store';
|
||||
|
||||
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 ? true : false}
|
||||
on:click={convertToPng}
|
||||
class="{isDownloading || !isDisabled
|
||||
? 'bg-gray-200 cursor-not-allowed text-gray-600'
|
||||
: 'bg-[#0233BD] text-white'} {text === 'Generate Progress Report'
|
||||
? 'block md:hidden'
|
||||
: ''} mt-5 transition-all delay-100 text-white text-xs font-semibold w-full py-3 rounded-md"
|
||||
>{isDownloading ? 'Loading...' : text}</button
|
||||
>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/Modal/Modal.svelte';
|
||||
import Report from './Report.svelte';
|
||||
import { openModal } from './store';
|
||||
</script>
|
||||
|
||||
<Modal bind:open={$openModal.fullscreen}>
|
||||
<!-- -->
|
||||
<Report />
|
||||
<!-- -->
|
||||
</Modal>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/Modal/Modal.svelte';
|
||||
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>
|
||||
|
||||
<Modal bind:open={$openModal.mood}>
|
||||
<!-- header -->
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-sm font-semibold">Choose your Mood</h1>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => ($openModal.mood = false)}
|
||||
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
|
||||
>
|
||||
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- -->
|
||||
</Modal>
|
||||
@@ -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,240 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
import Timer from './components/Timer.svelte';
|
||||
import ToolsHeader from '$lib/ToolsHeader/ToolsHeader.svelte';
|
||||
|
||||
let timer: NodeJS.Timeout;
|
||||
let buzzSound: HTMLAudioElement;
|
||||
let isNextStep: boolean = false;
|
||||
let isPaused: boolean = false;
|
||||
let soundOn: boolean = true;
|
||||
|
||||
let hours: number = 0;
|
||||
let minutes: number = 0;
|
||||
let seconds: number = 0;
|
||||
|
||||
let activityName: string = '';
|
||||
let displayTime: string = '00:00:00';
|
||||
let totalSeconds: number = 0;
|
||||
let formattedTime: string = '00:00';
|
||||
|
||||
export function toggleTimer() {
|
||||
if (isPaused) {
|
||||
// Start or resume the timer
|
||||
isPaused = false;
|
||||
if (totalSeconds <= 0) {
|
||||
totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
if (totalSeconds <= 0) {
|
||||
alert('Please set a valid timer.');
|
||||
isPaused = true;
|
||||
return;
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
if (totalSeconds <= 0) {
|
||||
if (soundOn) {
|
||||
buzzSound.play();
|
||||
}
|
||||
clearInterval(timer);
|
||||
isPaused = true;
|
||||
} else {
|
||||
totalSeconds--;
|
||||
updateDisplayTime();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
// Pause the timer
|
||||
clearInterval(timer);
|
||||
isPaused = true;
|
||||
}
|
||||
isNextStep = true;
|
||||
}
|
||||
|
||||
export function startTimer() {
|
||||
toggleTimer();
|
||||
totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
updateDisplayTime();
|
||||
}
|
||||
|
||||
function toggleSound() {
|
||||
soundOn = !soundOn;
|
||||
}
|
||||
|
||||
function updateDisplayTime() {
|
||||
const hrs = Math.floor(totalSeconds / 3600);
|
||||
const mins = Math.floor((totalSeconds % 3600) / 60);
|
||||
const secs = totalSeconds % 60;
|
||||
displayTime = `${String(hrs).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(
|
||||
secs
|
||||
).padStart(2, '0')}`;
|
||||
|
||||
if (formattedTime === '00:00') {
|
||||
formattedTime = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function restartTimer() {
|
||||
clearInterval(timer);
|
||||
totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
if (totalSeconds <= 0) {
|
||||
alert('Please set a valid timer.');
|
||||
return;
|
||||
}
|
||||
isPaused = false;
|
||||
timer = setInterval(() => {
|
||||
if (totalSeconds <= 0) {
|
||||
if (soundOn) {
|
||||
buzzSound.play();
|
||||
}
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
totalSeconds--;
|
||||
updateDisplayTime();
|
||||
}
|
||||
}, 1000);
|
||||
updateDisplayTime();
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
isNextStep = false;
|
||||
clearInterval(timer);
|
||||
isPaused = false;
|
||||
hours = 0;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
totalSeconds = 0;
|
||||
activityName = '';
|
||||
displayTime = '00:00:00';
|
||||
formattedTime = '00:00';
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
buzzSound = new Audio('https://assets.cdn.clsrio.com/beeping-sound.mp3');
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Activity Stopwatch | ClassroomIO</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
<meta property="og:title" content="Activity Stopwatch | ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Stay on track with a customizable stopwatch for timed tasks, quizzes, and study sessions"
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="Activity Stopwatch | ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Stay on track with a customizable stopwatch for timed tasks, quizzes, and study sessions"
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="mt-[10%] md:mt-16 px-5 md:px-0 bg-white">
|
||||
<ToolsHeader>
|
||||
<img
|
||||
src="/free-tools/name-picker.svg"
|
||||
class="w-[15%] md:w-[5%] mx-auto border rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-[#040F2D] my-3">Activity Stopwatch</h1>
|
||||
<p class="text-md text-[#656565] font-light md:font-normal md:w-[45%] mx-auto">
|
||||
Stay on track and enhance productivity with our customizable stopwatch for timed tasks,
|
||||
quizzes, and study sessions.
|
||||
</p>
|
||||
</ToolsHeader>
|
||||
|
||||
<div class="w-full md:w-2/4 my-10 mx-auto border rounded-md overflow-hidden bg-white">
|
||||
<!-- countdown setter -->
|
||||
{#if !isNextStep}
|
||||
<div transition:fly={{ y: -300, delay: 0, easing: sineInOut }} class="px-6 py-8 bg-white">
|
||||
<form on:submit|preventDefault={startTimer}>
|
||||
<div>
|
||||
<h1 class="font-bold text-sm">Name of activity</h1>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={activityName}
|
||||
class="w-full rounded-sm border py-2 mt-3 bg-[#F1F2F4] outline-none px-3 focus-within:border-[#0233BD] focus-within:border"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 bg-white">
|
||||
<h1 class="font-bold text-sm">Set an Activity</h1>
|
||||
<div class="flex flex-wrap justify-between gap-y-5 w-full mt-3 uppercase font-bold">
|
||||
<div class="bg-[#F1F2F4] w-full md:w-[30%] border px-4 py-5">
|
||||
<div class="w-2/4 md:w-full mx-auto flex items-end gap-2">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={hours}
|
||||
class="w-[80%] md:w-[75%] outline-none text-center py-3 px-1.5 text-3xl placeholder:text-3xl placeholder:text-black"
|
||||
/>
|
||||
<p class="text-[10px]">hrs</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-[#F1F2F4] w-full md:w-[30%] border px-4 py-5">
|
||||
<div class="w-2/4 md:w-full mx-auto flex items-end gap-2">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={minutes}
|
||||
class="w-[80%] md:w-[75%] outline-none text-center py-3 px-1.5 text-3xl placeholder:text-3xl placeholder:text-black"
|
||||
/>
|
||||
<p class="text-[10px]">mins</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-[#F1F2F4] w-full md:w-[30%] border px-4 py-5">
|
||||
<div class="w-2/4 md:w-full mx-auto flex items-end gap-2">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={seconds}
|
||||
class="w-[80%] md:w-[75%] outline-none text-center py-3 px-1.5 text-3xl placeholder:text-3xl placeholder:text-black"
|
||||
/>
|
||||
<p class="text-[10px]">sec</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-10">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-[#0F62FE] text-white text-sm px-5 py-3 rounded-md flex gap-3 items-center"
|
||||
>
|
||||
<img src="/free-tools/stopwatch/loading-timer.svg" alt="" />
|
||||
Start Timer</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- countdown -->
|
||||
{#if isNextStep}
|
||||
<Timer
|
||||
{activityName}
|
||||
{formattedTime}
|
||||
{displayTime}
|
||||
{resetTimer}
|
||||
{toggleTimer}
|
||||
{toggleSound}
|
||||
{restartTimer}
|
||||
{isPaused}
|
||||
{soundOn}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -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,236 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
import Board from './components/Board.svelte';
|
||||
import { TicTacToe } from './components/board';
|
||||
import { scores, changeToComputer } from './components/store';
|
||||
import ToolsHeader from '$lib/ToolsHeader/ToolsHeader.svelte';
|
||||
|
||||
let startGame = false;
|
||||
let message: string | null = null;
|
||||
let winner: Winner | null = null;
|
||||
let game = createGame();
|
||||
|
||||
interface Winner {
|
||||
symbol?: string;
|
||||
}
|
||||
|
||||
function createGame() {
|
||||
message = '';
|
||||
winner = null;
|
||||
|
||||
return TicTacToe({
|
||||
onWin(winnerInfo) {
|
||||
console.log('winner object', winnerInfo);
|
||||
winner = winnerInfo;
|
||||
|
||||
scores.update((currentScores) => {
|
||||
if (winner!.symbol === 'X') {
|
||||
return { ...currentScores, X: currentScores.X + 1 };
|
||||
} else if (winner!.symbol === 'O') {
|
||||
return { ...currentScores, O: currentScores.O + 1 };
|
||||
}
|
||||
return currentScores;
|
||||
});
|
||||
|
||||
if (winner!.symbol === 'O') {
|
||||
message = `Winner: Player 2 wins the round`;
|
||||
} else if (winner!.symbol === 'X') {
|
||||
message = `Winner: Player 1 wins the round`;
|
||||
}
|
||||
},
|
||||
|
||||
onTie() {
|
||||
message = `Tie game!`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function newGame() {
|
||||
// reset scores to zero
|
||||
scores.update(() => ({
|
||||
X: 0,
|
||||
O: 0,
|
||||
draw: 0
|
||||
}));
|
||||
|
||||
// disable ai
|
||||
$changeToComputer = { change: false };
|
||||
|
||||
// create a new game instance
|
||||
game = createGame();
|
||||
}
|
||||
|
||||
function playAgain() {
|
||||
message = '';
|
||||
// create a new game instance
|
||||
game = createGame();
|
||||
}
|
||||
|
||||
function changePlayer() {
|
||||
changeToComputer.update((value) => ({ change: !value.change }));
|
||||
}
|
||||
|
||||
function startTicTac() {
|
||||
startGame = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Tic Tac Toe | ClassroomIO</title>
|
||||
<meta
|
||||
property="og:image"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
<meta property="og:title" content="Tic Tac Toe | ClassroomIO" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A simple and free tic tac toe game to play to your friends in class or outside the classroom."
|
||||
/>
|
||||
|
||||
<meta
|
||||
property="og:image:secure_url"
|
||||
itemprop="image"
|
||||
content="https://brand.cdn.clsrio.com/og/free-tools.png"
|
||||
/>
|
||||
|
||||
<meta name="twitter:title" content="Tic Tac Toe | ClassroomIO" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A simple and free tic tac toe game to play to your friends in class or outside the classroom."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="mt-[10%] md:mt-16 px-5 md:px-0 bg-white">
|
||||
<ToolsHeader className="mb-10">
|
||||
<img
|
||||
src="/free-tools/tic-tac.svg"
|
||||
class="w-14 md:w-[5%] mx-auto border rounded-full"
|
||||
alt="Tic Tac Icon"
|
||||
/>
|
||||
<h1 class="text-4xl md:text-6xl font-bold text-[#040F2D] my-3">Tic-Tac game</h1>
|
||||
<p class="text-md text-[#656565] font-light md:font-normal md:w-[45%] mx-auto">
|
||||
More than just a game; it's an educational tool that teaches pattern recognition, and
|
||||
decision-making.
|
||||
</p>
|
||||
</ToolsHeader>
|
||||
|
||||
<div
|
||||
class="overflow-hidden relative tic-tac-bg text-white w-full h-[55vh] md:w-[59%] md:h-[82vh] border-8 border-black mx-auto flex justify-center items-center bg-white"
|
||||
>
|
||||
<!-- congratulatory message -->
|
||||
{#if message}
|
||||
<div
|
||||
transition:fly={{ x: -100, delay: 600, easing: sineInOut }}
|
||||
class="absolute w-full top-[43%] flex flex-col justify-center items-center z-[150]"
|
||||
>
|
||||
<div
|
||||
class="flex gap-10 justify-center py-1 h-[8vh] md:h-[13vh] w-full items-center bg-white font-bold text-[#0233BD]"
|
||||
>
|
||||
{#if message != 'Tie game!'}
|
||||
<img src="/free-tools/tic-tac/winner-star-icon.svg" alt="Star icon" class="h-full" />
|
||||
{/if}
|
||||
<h2 class="md:text-base text-xs">{message}</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={playAgain}
|
||||
class="px-16 pt-1 pb-1.5 md:text-base text-xs border border-white font-bold rounded-md mt-10 bg-[#0F62FE]"
|
||||
>Play Again</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- this condition renders the game -->
|
||||
{#if startGame}
|
||||
<div class="w-full h-full p-5 flex flex-col border justify-between items-center">
|
||||
<header class="flex w-full items-center justify-between">
|
||||
<button
|
||||
on:click={changePlayer}
|
||||
class="text-[10px] md:text-xs w-[35%] gap-2 md:w-28 flex flex-row items-center leading-3 font-medium"
|
||||
>
|
||||
<img
|
||||
src="/free-tools/tic-tac/computer-icon.svg"
|
||||
alt="Computer icon"
|
||||
class="w-4 md:w-6"
|
||||
/>
|
||||
{#if $changeToComputer.change}
|
||||
Play two players
|
||||
{:else}
|
||||
Change to computer
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
on:click={newGame}
|
||||
class="text-[10px] w-24 flex flex-col gap-1 p-2 shadow-md rounded-md items-center leading-3 font-medium border text-[#0542CC] bg-white"
|
||||
>
|
||||
<img src="/free-tools/tic-tac/restart-icon.svg" alt="Restart icon" class="w-3 md:w-4" />
|
||||
Reset Score</button
|
||||
>
|
||||
</header>
|
||||
|
||||
<!-- board -->
|
||||
<Board {game} {winner} />
|
||||
|
||||
<!-- scores -->
|
||||
<div
|
||||
class="flex justify-between md:justify-center items-center gap-10 w-full text-white font-semibold"
|
||||
>
|
||||
<div class="md:text-3xl text-2xl flex items-center md:gap-5 gap-2">
|
||||
<span class="px-1 md:p-1 bg-[#FE28CB99] md:text-sm text-[10px]">Player 1</span>
|
||||
{$scores.X}
|
||||
</div>
|
||||
<div class="md:text-3xl text-2xl flex items-center md:gap-5 gap-2">
|
||||
<span class="px-1 md:p-1 bg-[#0F62FE] md:text-sm text-[10px]">Draw</span>
|
||||
{$scores.draw}
|
||||
</div>
|
||||
<div class="md:text-3xl text-2xl flex items-center md:gap-5 gap-2">
|
||||
<span class="px-1 md:p-1 bg-[#94F7FFB2] md:text-sm text-[10px]">Player 2</span>
|
||||
{$scores.O}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- this condition renders the homepage -->
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<img
|
||||
src="/free-tools/tic-tac/tic-tac-start-icon.svg"
|
||||
alt="Tic tac toe game icon"
|
||||
class="w-[50%] md:w-[70%]"
|
||||
/>
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
changePlayer();
|
||||
startTicTac();
|
||||
}}
|
||||
class="mt-5 px-[3.3rem] pt-1 pb-1.5 border border-white text-sm font-bold rounded-md bg-[#0F62FE]"
|
||||
>
|
||||
Play with computer</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
startTicTac();
|
||||
}}
|
||||
class="px-16 pt-1 pb-1.5 border border-white text-sm font-bold rounded-md bg-[#0F62FE]"
|
||||
>Play two players</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.tic-tac-bg {
|
||||
background: url('/free-tools/tic-tac/tic-tac-bg.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { sineInOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import WinningLine from './WinningLine.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,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,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,16 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
export let url: string;
|
||||
</script>
|
||||
|
||||
{#key url}
|
||||
<div class="transition" in:fade>
|
||||
<slot />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.transition {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user