migrate web to svelte 5

This commit is contained in:
Rotimi Best
2025-12-25 05:37:21 +00:00
parent 50d6ee3111
commit 334c67f22a
63 changed files with 621 additions and 479 deletions
+16 -2
View File
@@ -5,6 +5,7 @@ This document describes the folder structure for the `apps/website` project, spe
## Overview
The codebase follows a clear separation of concerns with three main categories:
- **Components**: Presentational UI components (dummy components)
- **Features**: Business logic and functional components
- **Utils**: Utility functions, constants, and shared types
@@ -87,10 +88,12 @@ src/lib/
## Naming Conventions
### Files and Folders
- **Kebab-case**: All file and folder names use kebab-case (e.g., `blog-list-item.svelte`, `feedback-form.svelte`)
- **Single-file folders**: If a folder contains only one file, it should be flattened to a single file (e.g., `Footer/Footer.svelte` → `components/footer.svelte`)
### Components
- Component files use `.svelte` extension
- Component names match their file names in kebab-case
- Example: `blog-list-item.svelte` exports a component that can be imported as `BlogListItem`
@@ -102,18 +105,21 @@ src/lib/
**Purpose**: Presentational components with minimal or no business logic. These are "dummy" components that receive props and render UI.
**Characteristics**:
- No API calls or complex state management
- Primarily focused on rendering and styling
- May have simple local state for UI interactions (e.g., open/close modals)
- Reusable across different pages/features
**Examples**:
- `footer.svelte` - Site footer component
- `navigation.svelte` - Site navigation bar
- `hero.svelte` - Hero section component
- `modal.svelte` - Generic modal wrapper
**Import Pattern**:
```svelte
// ✅ CORRECT - Use named imports from the index
import { Footer, Navigation, Hero } from '$lib/components';
@@ -129,17 +135,20 @@ import { Footer, Navigation, Hero } from '$lib/components';
**Purpose**: Components and modules that contain business logic, API calls, form handling, state management, or domain-specific functionality.
**Characteristics**:
- Contains business logic and data handling
- May include API calls, form submissions, or data transformations
- Often includes stores, utilities, or helper functions specific to the feature
- Organized by feature domain (e.g., `contact/`, `tools/`)
**Examples**:
- `features/contact/feedback-form.svelte` - Form with submission logic
- `features/tools/progress/store.ts` - State management for progress tool
- `features/tools/tic-tac-toe/board.ts` - Game logic
**Import Pattern**:
```svelte
import FeedbackForm from '$lib/features/contact/feedback-form.svelte';
import { htmlBody, openModal } from '$lib/features/tools/progress/store';
@@ -150,12 +159,14 @@ import { htmlBody, openModal } from '$lib/features/tools/progress/store';
**Purpose**: Shared utility functions, constants, types, and helper modules used across the application.
**Characteristics**:
- Pure functions or utility modules
- No component-specific logic
- Reusable across different features/components
- May include TypeScript types and interfaces
**Examples**:
- `utils/format-date.ts` - Date formatting utility
- `utils/is-form-valid.ts` - Form validation helper
- `utils/submit-form.ts` - Generic form submission utility
@@ -163,6 +174,7 @@ import { htmlBody, openModal } from '$lib/features/tools/progress/store';
- `utils/constants/tools.ts` - Application constants
**Import Pattern**:
```typescript
import { formatDate } from '$lib/utils/format-date';
import { isFormValid } from '$lib/utils/is-form-valid';
@@ -219,7 +231,7 @@ src/lib/
<script>
// ✅ CORRECT - Named imports from centralized index
import { Footer, Navigation, Hero, Testimonial } from '$lib/components';
// ❌ INCORRECT - Direct file imports are not allowed
// import Footer from '$lib/components/footer.svelte';
</script>
@@ -307,6 +319,7 @@ Routes now import from `$lib/features/tools/*/` instead of `./components/`.
## Assets
Static assets like images, SVGs, and other media files remain in their respective locations:
- `emojis/` - Emoji SVG files
- `public/` - Public static assets (images, fonts, etc.)
@@ -323,6 +336,7 @@ export { default as Navigation } from './navigation.svelte';
```
This allows for clean, consistent imports:
```svelte
import { Footer, Navigation, Hero } from '$lib/components';
```
@@ -332,6 +346,7 @@ import { Footer, Navigation, Hero } from '$lib/components';
## Summary
This structure provides:
- ✅ Clear separation between UI and business logic
- ✅ Consistent naming conventions (kebab-case)
- ✅ Flattened structure (no unnecessary single-file folders)
@@ -341,4 +356,3 @@ This structure provides:
- ✅ Scalable for future growth
For questions or suggestions about this structure, please refer to the project maintainers.
+15 -15
View File
@@ -1,17 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+6 -6
View File
@@ -1,12 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};
+73 -73
View File
@@ -1,20 +1,19 @@
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="theme-color" content="#333333" />
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="theme-color" content="#333333" />
<!-- <title>ClassroomIO –&nbsp;The Open Source Learning Management System for Companies</title>
<!-- <title>ClassroomIO –&nbsp;The Open Source Learning Management System for Companies</title>
<meta name="description" content="ClassroomIO" />
<meta name="application-name" content="ClassroomIO" />
<meta name="apple-mobile-web-app-title" content="ClassroomIO" />
<meta name="theme-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> -->
<!-- Facebook Meta Tags -->
<!-- <meta property="og:url" content="https://www.classroomio.com/" />
<!-- Facebook Meta Tags -->
<!-- <meta property="og:url" content="https://www.classroomio.com/" />
<meta property="og:image" itemprop="image" content="https://brand.cdn.clsrio.com/og/classroomio-og.png" />
<meta property="og:title" content="ClassroomIO –&nbsp;The Open Source Learning Management System for Companies" />
<meta property="og:description"
@@ -34,75 +33,76 @@
content="Launch your bootcamp quichttps://brand.cdn.clsrio.com/og/classroomio-og.pngnline teaching platform." />
<meta name="twitter:image" content="https://classroomio.com/classroomio-opengraph-image.png" /> -->
<!-- <link rel="stylesheet" href="global.css"> -->
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
<!-- <link rel="stylesheet" href="global.css"> -->
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
<!-- <link rel="stylesheet" href="global.css"> -->
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
<!-- <link rel="stylesheet" href="global.css"> -->
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" type="image/png" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png" />
<script
defer
src="https://umami.hz.oncws.com/script.js"
data-website-id="5321d227-751a-4d43-8556-f516fba60891"
></script>
<script defer src="https://umami.hz.oncws.com/script.js"
data-website-id="5321d227-751a-4d43-8556-f516fba60891"></script>
<script defer type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '69176b47-5f63-42cc-bb9b-68d4927b6276';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script defer type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '69176b47-5f63-42cc-bb9b-68d4927b6276';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script defer type="text/javascript">
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement('script')).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === 'string' ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
return;
}
p(cal, ar);
<script defer type="text/javascript">
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
})(window, 'https://app.cal.com/embed/embed.js', 'init');
Cal('init', { origin: 'https://cal.com' });
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement('script')).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === 'string' ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
return;
}
p(cal, ar);
};
})(window, 'https://app.cal.com/embed/embed.js', 'init');
Cal('init', { origin: 'https://cal.com' });
Cal('ui', {
theme: 'light',
styles: { branding: { brandColor: '#1D4EE2' } },
hideEventTypeDetails: false,
layout: 'month_view'
});
</script>
%sveltekit.head%
</head>
Cal('ui', {
theme: 'light',
styles: { branding: { brandColor: '#1D4EE2' } },
hideEventTypeDetails: false,
layout: 'month_view'
});
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
@@ -13,7 +13,7 @@ published: true
![classroomio-brand-customisations](https://assets.cdn.clsrio.com/blog/brand-customisations.png)
_ClassroomIO Brand Customisations_
We are excited to announce more customisations coming into our learning management system (LMS). In this blog post, we will like to share with you how we've made *YOUR* *STUDENT DASHBOARD* much more customisable than before.
We are excited to announce more customisations coming into our learning management system (LMS). In this blog post, we will like to share with you how we've made _YOUR_ _STUDENT DASHBOARD_ much more customisable than before.
## Demo
@@ -40,7 +40,7 @@ However that is not the UI that your audience will see when they consume your co
![classroomio-lms-student-dashboard](https://brand.cdn.clsrio.com/classroomio-lms-courses.png)
_The LMS aka Student dashboard - yourorg.classroomio.com_
Typically this page can be found at *yourorg.classroomio.com*, where *yourorg* is a slug you set when you first set up your account. For example, we've built an internal bootcamp called ClassroomIO Developers, in this bootcamp we train other developers on some of the key tools we use internally. You can checkout our page here: [ciodevs.classroomio.com](https://ciodevs.classroomio.com), in this case you can see that our slug is *ciodevs* and you should be able to easily see yours when you log into the dashboard `/org/{yourorg}`.
Typically this page can be found at _yourorg.classroomio.com_, where _yourorg_ is a slug you set when you first set up your account. For example, we've built an internal bootcamp called ClassroomIO Developers, in this bootcamp we train other developers on some of the key tools we use internally. You can checkout our page here: [ciodevs.classroomio.com](https://ciodevs.classroomio.com), in this case you can see that our slug is _ciodevs_ and you should be able to easily see yours when you log into the dashboard `/org/{yourorg}`.
In your student dashboard, your audience can:
@@ -25,7 +25,7 @@ A bit about EduAI Hub before I jump into my experience at the summit:
### My experience
Now to my experience - __it was nothing less than amazing__.
Now to my experience - **it was nothing less than amazing**.
I could see how the eyes of some of the educators lite up when I said we can allow them build a customisable alternative to Google Classroom without writing a single line of code. I spoke about the importance of a customisable no-code elearning platform that anyone could use to deploy their own learning management system that suits their brand. I also gave an analogy using Shopify and Wordpress, how Shopify is allowing businesses create ecommerce stores in minutes or Wordpress allowing writters and businesses create robust websites in minutes - that's exactly what we want to do in the educational space starting with bootcamps.
@@ -33,7 +33,7 @@ You can find my slides [here](https://docs.google.com/presentation/d/1yU9EFmYA5O
This was my first actual event speaking about ClassroomIO and I was amazed by the amount of people that wanted to chat about how they could use the product for their online courses.
Apart from the talk, I met amazing people working on other edtech related products and how they also use AI to empower their users.
Apart from the talk, I met amazing people working on other edtech related products and how they also use AI to empower their users.
### Unexpected learning
+2 -2
View File
@@ -48,6 +48,6 @@ Oh we even hooked up our AI content generator with it as well, you can switch to
## Next steps
All translations in our app are stored in a JSON file which we generate and keep up to date using Google translate. This means that it might not be 100% accurate and there could be some mistakes when use our dashboard in other languages. If you do find any issues with our translations, please do open an [issue on Github](https://github.com/classroomio/classroomio/issues/new/choose) with the correct translation and we'd be so happy to fix it.
All translations in our app are stored in a JSON file which we generate and keep up to date using Google translate. This means that it might not be 100% accurate and there could be some mistakes when use our dashboard in other languages. If you do find any issues with our translations, please do open an [issue on Github](https://github.com/classroomio/classroomio/issues/new/choose) with the correct translation and we'd be so happy to fix it.
If you are a native speaker of any of the 7 languages and want to contribute, please feel free to nitpick into our [translations](https://github.com/classroomio/classroomio/tree/main/apps/dashboard/src/lib/utils/translations) and open a PR with the fixes. Every contribution is valuable and will be gladly welcomed 😊
If you are a native speaker of any of the 7 languages and want to contribute, please feel free to nitpick into our [translations](https://github.com/classroomio/classroomio/tree/main/apps/dashboard/src/lib/utils/translations) and open a PR with the fixes. Every contribution is valuable and will be gladly welcomed 😊
+1 -1
View File
@@ -17,7 +17,7 @@ Today we are happy to share that you can now easily self-host classroomio and us
If you are interested in just trying out the product, it is advisable to use the [cloud version](https://app.classroomio.com). You can use almost all the features for free on the cloud version unless you need more students. Right now the main reason you'd have to upgrade is because your students are growing. If you don't want to upgrade and/or face this, then you'd be fine self hosting the whole application.
For self-hosting, we have written up a [guide that uses Railway](https://classroomio.com/docs/quickstart/self-hosting) to help deploy our stack and you get to manage the servers on Railway as you wish. We hope this is a good step in helping you educate your audience at ease.
For self-hosting, we have written up a [guide that uses Railway](https://classroomio.com/docs/quickstart/self-hosting) to help deploy our stack and you get to manage the servers on Railway as you wish. We hope this is a good step in helping you educate your audience at ease.
Below is a demo showing you step by step how to self host the project
+16 -16
View File
@@ -22,14 +22,14 @@ The audience page as you already know allows you to view the students in your **
<br />
<video
loop
muted
autoplay
preload="auto"
autoPictureInPicture
class="rounded-md" src="https://assets.cdn.clsrio.com/blog/organisation-student-analytics.mp4" controls
>
</video>
loop
muted
autoplay
preload="auto"
autoPictureInPicture
class="rounded-md" src="https://assets.cdn.clsrio.com/blog/organisation-student-analytics.mp4" controls
> </video>
## Course Student Analytics
@@ -43,14 +43,14 @@ Further more, when you go into any course and go to the people tab, you can view
<br />
<video loop
muted
autoplay
preload="auto"
autoPictureInPicture
class="rounded-md"
src="https://assets.cdn.clsrio.com/blog/course-students-analytics.mp4" controls
>
</video>
muted
autoplay
preload="auto"
autoPictureInPicture
class="rounded-md"
src="https://assets.cdn.clsrio.com/blog/course-students-analytics.mp4" controls
> </video>
## What's Next?
@@ -2,8 +2,14 @@
import { Badge } from '@cio/ui/base/badge';
import { formatDate } from '$lib/utils/format-date';
export let post;
export let isRecommended = false;
/**
* @typedef {Object} Props
* @property {any} post
* @property {boolean} [isRecommended]
*/
/** @type {Props} */
let { post, isRecommended = false } = $props();
</script>
{#if !isRecommended}
@@ -1,13 +1,29 @@
<script>
import { flexible, leftFist, rightFist, robotArm, thumbsUp } from '$lib/emojis';
export let id = '';
export let rightToLeft = false;
export let tagline = '';
export let title = '';
export let description = '';
export let video = '';
export let taglineIcon = '';
/**
* @typedef {Object} Props
* @property {string} [id]
* @property {boolean} [rightToLeft]
* @property {string} [tagline]
* @property {string} [title]
* @property {string} [description]
* @property {string} [video]
* @property {string} [taglineIcon]
* @property {import('svelte').Snippet} [more]
*/
/** @type {Props} */
let {
id = '',
rightToLeft = false,
tagline = '',
title = '',
description = '',
video = '',
taglineIcon = '',
more
} = $props();
</script>
<section {id} class="w-full py-32 border-b border-x-0 border-t-0 border-gray-200">
@@ -32,15 +48,15 @@
{/if}
<p class="font-medium text-base">{tagline}</p>
</div>
<h2 class="font-bold text-4xl mb-3 lg:mb-6">{title}</h2>
<h2 class="text-3xl mb-3 lg:mb-6">{title}</h2>
<p class="leading-8 text-lg text-gray-500">
{description}
</p>
{#if $$slots.more}
{#if more}
<br />
<p class="leading-8 text-lg text-gray-500">
<slot name="more" />
{@render more?.()}
</p>
{/if}
</div>
@@ -1,6 +1,10 @@
<script lang="ts">
export let src: string;
export let alt: string;
interface Props {
src: string;
alt: string;
}
let { src, alt }: Props = $props();
</script>
<figure>
@@ -4,7 +4,7 @@
import Youtube from '@lucide/svelte/icons/youtube';
</script>
<div class="border-t-[1px] px-[2%] lg:px-[14%]">
<div class="border-t px-[2%] lg:px-[14%]">
<div class="flex flex-col items-center justify-between py-[10%] pl-5 lg:flex-row lg:pl-0">
<div class="w-full min-w-[200px] lg:w-3/12">
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="classroomio logo" class="mb-5" />
+2 -2
View File
@@ -11,8 +11,8 @@
{ label: 'Customers', color: '#0233BD' },
{ label: 'Students', color: '#9747FF' }
];
let animate = false;
let titleIndex = 0;
let animate = $state(false);
let titleIndex = $state(0);
let timeout: NodeJS.Timeout;
onMount(() => {
-1
View File
@@ -19,4 +19,3 @@ export { default as Testimonial } from './testimonial.svelte';
export { default as Timeline } from './timeline.svelte';
export { default as ToolsHeader } from './tools-header.svelte';
export { default as UploadWidget } from './upload-widget.svelte';
+10 -3
View File
@@ -1,6 +1,13 @@
<script>
export let className = '';
export let open = false;
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {boolean} [open]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', open = false, children } = $props();
</script>
{#if open}
@@ -10,7 +17,7 @@
<div
class="fixed z-[3001] {className} w-full md:w-[30%] md:min-w-[500px] bg-white rounded-md mx-auto py-6 px-7"
>
<slot />
{@render children?.()}
</div>
</div>
{/if}
@@ -4,8 +4,8 @@
import { folder, globe, team, transcription } from '$lib/emojis';
import { tools } from '$lib/utils/constants/tools';
let selected = 0;
let interval: NodeJS.Timer;
let selected = $state(0);
let interval: NodeJS.Timer = $state();
const moreFeatures = [
{
@@ -22,13 +22,13 @@
'Extend invitations and manage your teaching institution seamlessly through ClassroomIO.',
image: '/team-management.png'
},
{
icon: transcription,
title: 'Auto Video Transcription',
description:
'AI automatically transcribes videos, making content searchable for seamless student access.',
image: '/video-transcription.png'
},
// {
// icon: transcription,
// title: 'Auto Video Transcription',
// description:
// 'AI automatically transcribes videos, making content searchable for seamless student access.',
// image: '/video-transcription.png'
// },
{
icon: folder,
title: 'Downloadable Lesson PDF',
@@ -52,7 +52,7 @@
<section id="morefeatures" class="py-[10%] px-3 lg:px-0 bg-gray-50">
<div class="mx-0 lg:mx-[12%]">
<div class="w-full lg:w-4/5 mx-4 lg:mx-0 mb-14">
<h2 class="mt-2 text-3xl font-bold font-display tracking-tight lg:text-4xl">
<h2 class="mt-2 text-3xl tracking-tight">
There is More...
</h2>
<p class="mt-4 text-lg text-gray-500 w-[94%] lg:w-4/5">
@@ -67,8 +67,8 @@
{#each moreFeatures as moreFeature, i}
<button
class="p-4 rounded-md {i === selected &&
'selected'} mb-3 text-start min-w-[75vw] lg:min-w-[unset]"
on:mouseenter={() => {
'selected'} mb-3 text-start min-w-[75vw] lg:min-w-[unset] cursor-pointer"
onclick={() => {
selected = i;
clearInterval(interval);
}}
@@ -82,11 +82,11 @@
alt=""
class="w-8"
/>
<p class="text-lg font-semibold">
<p class="text-lg">
{moreFeature.title}
</p>
</div>
<p>{moreFeature.description}</p>
<p class="text-sm">{moreFeature.description}</p>
</button>
{/each}
</div>
@@ -99,7 +99,7 @@
height="100%"
loading="lazy"
id="image"
in:fly={{ y: 100, duration: 1000 }}
in:fly={{ x: 100, duration: 800 }}
class="w-full flex flex-col items-center"
src={moreFeatures[selected].image}
alt={moreFeatures[selected].title}
@@ -14,13 +14,28 @@
import type { HTMLAttributes } from 'svelte/elements';
import * as NavigationMenu from '@cio/ui/base/navigation-menu';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import { cn } from '@cio/ui/tools';
export let stars: number;
let { stars }: { stars: number } = $props();
type ListItemProps = HTMLAttributes<HTMLAnchorElement> & {
title: string;
href: string;
subtitle: string;
key: string;
};
let showDrawer = false;
let showSolutions = false;
let activeLink = '';
let activeLink = $derived(page.url.pathname);
let activeHash = $derived(page.url.hash);
$effect(() => {
console.log('activeLink', activeLink);
console.log('activeHash', activeHash);
console.log('isSolutionsActive', isSolutionsActive);
});
function handleShowDrawer() {
showDrawer = !showDrawer;
@@ -76,16 +91,7 @@
}
];
type ListItemProps = HTMLAttributes<HTMLAnchorElement> & {
title: string;
href: string;
subtitle: string;
key: string;
};
$: activeLink = page.url.pathname;
$: activeHash = page.url.hash;
$: isSolutionsActive = solutions.some((s) => activeHash.includes(s.key));
let isSolutionsActive = $derived(!!solutions.some((s) => activeLink.includes(s.key)));
</script>
{#snippet list_item({ title, subtitle, href, key, class: className, ...restProps }: ListItemProps)}
@@ -140,11 +146,14 @@
</a>
<section class="hidden lg:block">
<NavigationMenu.Root class="rounded-lg bg-white p-1">
<NavigationMenu.Root class="p-1">
<NavigationMenu.List class="flex w-full items-center justify-center gap-1">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100"
class={cn(
"flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100",
isSolutionsActive && 'bg-gray-100!'
)}
>
<a href="/" class="no-underline"> Solutions </a>
</NavigationMenu.Trigger>
@@ -164,7 +173,7 @@
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100"
class={cn("flex cursor-pointer items-center rounded-md px-4 py-2 text-sm font-medium text-gray-800 transition-all duration-200 hover:bg-gray-100 data-[state=open]:bg-gray-100", activeLink.startsWith('/tools') && 'bg-gray-100!')}
>
<a href="/tools" class="no-underline"> Free Tools </a>
</NavigationMenu.Trigger>
@@ -187,11 +196,10 @@
{#snippet child()}
<a
href="/blog"
class="cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100 {activeLink.startsWith(
'/blog'
)
? 'bg-gray-100'
: ''}"
class={cn(
'cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100',
activeLink.startsWith('/blog') && 'bg-gray-100'
)}
>
Blog
</a>
@@ -204,11 +212,10 @@
{#snippet child()}
<a
href="/pricing"
class="cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100 {activeLink.startsWith(
'/pricing'
)
? 'bg-gray-100'
: ''}"
class={cn(
'cursor-pointer rounded-md px-4 py-2 text-sm font-medium text-gray-800 no-underline transition-all duration-200 hover:bg-gray-100',
activeLink.startsWith('/pricing') && 'bg-gray-100'
)}
>
Pricing
</a>
@@ -250,7 +257,7 @@
</a>
</div>
<button type="button" aria-label="Hamburger Menu" class="block md:block lg:hidden" on:click={handleShowSolutions}>
<button type="button" aria-label="Hamburger Menu" class="block md:block lg:hidden" onclick={handleShowSolutions}>
<Menu size={24} />
</button>
@@ -262,7 +269,7 @@
>
<div class="mb-5 flex justify-between py-2">
<img loading="lazy" width="20" height="20" src="/logo-512.png" alt="classroomio logo" class="w-[15%]" />
<button class="mr-5" on:click={handleShowSolutions}>
<button class="mr-5" onclick={handleShowSolutions}>
<X size={24} />
</button>
</div>
@@ -270,9 +277,11 @@
<ul class="flex w-full flex-col items-center justify-between lg:flex-row">
<li class="w-full cursor-pointer text-sm font-semibold text-gray-800 md:text-lg">
<button
class="flex w-full items-center justify-between rounded-lg px-4 py-3 transition-all duration-200 hover:bg-gray-100"
on:click={handleShowDrawer}
class:active={isSolutionsActive}
class={cn(
"flex w-full items-center justify-between rounded-lg px-4 py-3 transition-all duration-200 hover:bg-gray-100",
isSolutionsActive && 'bg-gray-100'
)}
onclick={handleShowDrawer}
>
Our Superpowers <ChevronDown />
</button>
@@ -281,7 +290,7 @@
{#each solutions as solution}
<a
href="/{solution.key}"
on:click={() => {
onclick={() => {
handleShowSolutions();
}}
>
@@ -297,37 +306,43 @@
</li>
<a
class="w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={handleShowSolutions}
onclick={handleShowSolutions}
href="/tools"
>
<li>Free Tools</li>
</a>
<a
class="w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeLink.startsWith('/morefeatures') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
class:active={activeLink.startsWith('/blog')}
href="/blog"
>
<li>Blog</li>
</a>
<a
class="w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-xl px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeLink.startsWith('/pricing') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
href="/pricing"
class:active={activeLink.startsWith('/pricing')}
>
<li>Pricing</li>
</a>
<a
class="w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg"
on:click={() => {
class={cn(
"w-full cursor-pointer rounded-md px-4 py-3 text-sm font-semibold text-gray-800 transition-all duration-200 hover:bg-gray-100 md:text-lg",
activeHash.includes('morefeatures') && 'bg-gray-100'
)}
onclick={() => {
handleShowSolutions();
}}
class:active={activeHash.includes('morefeatures')}
href="/#morefeatures"
>
<li>More features</li>
@@ -4,8 +4,14 @@
import SearchXIcon from '@lucide/svelte/icons/search-x';
import { Button } from '@cio/ui/base/button';
export let isOrg = false;
export let className = '';
/**
* @typedef {Object} Props
* @property {boolean} [isOrg]
* @property {string} [className]
*/
/** @type {Props} */
let { isOrg = false, className = '' } = $props();
function handleClick() {
if (!isOrg) {
@@ -1,7 +1,14 @@
<script>
export let className = '';
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', children } = $props();
</script>
<header class="hero-bg w-full border-b border-slate-100 pb-16 pt-32 lg:min-h-[20rem] {className}">
<slot />
{@render children?.()}
</header>
@@ -17,11 +17,11 @@
>
<div>
<h3
class="font-display mt-3 text-xl font-semibold tracking-tight text-white lg:mt-2 lg:text-4xl max-w-2xl"
class="font-display mt-3 text-xl tracking-tight text-white lg:mt-2 lg:text-4xl max-w-2xl"
>
{header}
</h3>
<p class="mt-6 text-xl text-gray-300 lg:text-lg">
<p class="mt-6 text-md text-gray-300">
{subText}
</p>
</div>
@@ -2,7 +2,11 @@
import { embedSenjaWidget } from '@cio/utils/senja';
import { onMount } from 'svelte';
export let id = '';
interface Props {
id?: string;
}
let { id = '' }: Props = $props();
onMount(() => {
embedSenjaWidget(id);
@@ -7,7 +7,7 @@
<div
class="relative overflow-hidden mt-10 flex space-y-3 flex-col text-center items-center border border-[#D9E0F5] justify-center w-full rounded-md min-h-[300px] p-4 bg-[#F5F8FE]"
>
<div class="absolute w-20 h-20 bg-[#0233BD] -top-14 -left-9 rotate-[50deg]" />
<div class="absolute w-20 h-20 bg-[#0233BD] -top-14 -left-9 rotate-[50deg]"></div>
<h1 class="font-bold text-3xl">Your message has been sent 🎉</h1>
<p class="w-[70%] font-normal text-sm">
We'll get back to you as soon as possible. In the meantime, Explore and enjoy features on our
@@ -1,25 +1,29 @@
<script lang="ts">
export let items: {
interface Props {
items?: {
title: string;
desc: string;
}[] = [];
}[];
}
let { items = [] }: Props = $props();
</script>
<div class="relative w-fit flex justify-center space-x-8 md:space-x-10">
<div
class="w-[3px] md:w-[2px] bg-blue-600 mt-10 before:bg-gradient-to-t before:from-white before:to-transparent before:w-[4px] before:h-[9rem] before:absolute before:bottom-0"
/>
class="w-[3px] md:w-[2px] bg-blue-600 mt-10 before:bg-linear-to-t before:from-white before:to-transparent before:w-[4px] before:h-36 before:absolute before:bottom-0"
></div>
<div class="space-y-8">
{#each items as card, index}
<div class="md:w-[35rem] w-full border-2 rounded-lg px-4 py-8 space-y-4 bg-white">
<div class="md:w-140 w-full border-2 rounded-lg px-4 py-8 space-y-4 bg-white">
<div class="flex items-center">
<div
class={`z-50 absolute -left-3 ${
index > 0 &&
'before:bg-gradient-to-t before:from-white before:to-transparent before:w-[4px] before:h-[9rem] before:absolute before:-top-36'
'before:bg-linear-to-t before:from-white before:to-transparent before:w-[4px] before:h-36 before:absolute before:-top-36'
} flex items-center justify-center w-7 h-7 bg-blue-100 rounded-full`}
>
<div class="w-4 h-4 bg-blue-800 rounded-full z-50" />
<div class="w-4 h-4 bg-blue-800 rounded-full z-50"></div>
</div>
<h1 class="text-2xl font-bold text-blue-800">{card.title}</h1>
</div>
@@ -1,7 +1,14 @@
<script>
export let className = '';
/**
* @typedef {Object} Props
* @property {string} [className]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { className = '', children } = $props();
</script>
<header class="tools-hero-bg py-24 text-center {className} border-b border-slate-100 bg-white">
<slot />
{@render children?.()}
</header>
@@ -3,12 +3,21 @@
import Trash2 from '@lucide/svelte/icons/trash-2';
import { ALLOWED_IMAGE_TYPES, validateImageUpload } from '@cio/utils/functions/fileValidation';
export let imageURL = '';
export let label = '';
export let isRequired = false;
export let labelClassName = '';
interface Props {
imageURL?: string;
label?: string;
isRequired?: boolean;
labelClassName?: string;
}
let {
imageURL = $bindable(''),
label = '',
isRequired = false,
labelClassName = ''
}: Props = $props();
let isUploading = false;
let fileInput: HTMLInputElement;
let fileInput: HTMLInputElement = $state();
const onFileSelected = () => {
const file = fileInput?.files?.[0];
@@ -52,8 +61,8 @@
};
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div>
{#if label}
<p class="flex w-full items-center justify-between text-left dark:text-white {labelClassName}">
@@ -65,7 +74,7 @@
</span>
{#if imageURL}
<span title="clear image" on:click={deleteImage}>
<span title="clear image" onclick={deleteImage}>
<Trash2 class="fill-red-500 " />
</span>
{/if}
@@ -73,14 +82,14 @@
{/if}
<div
class="flex min-h-[200px] w-full items-center justify-center rounded-md border p-4"
on:click={() => fileInput.click()}
onclick={() => fileInput.click()}
>
<input
type="file"
accept=".jpg,.jpeg,.png,.gif,.webp,image/jpeg,image/png,image/gif,image/webp"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
onchange={onFileSelected}
disabled={isUploading}
required={isRequired}
/>
+1 -11
View File
@@ -9,14 +9,4 @@ import team from './team.svg';
import transcription from './printer.svg';
import folder from './folder.svg';
export {
flexible,
robotArm,
thumbsUp,
leftFist,
rightFist,
globe,
team,
transcription,
folder,
};
export { flexible, robotArm, thumbsUp, leftFist, rightFist, globe, team, transcription, folder };
@@ -7,14 +7,14 @@
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = '';
let email = '';
let title = '';
let description = '';
let name = $state('');
let email = $state('');
let title = $state('');
let description = $state('');
let isLoading = false;
let errorMessage = '';
let sent = false;
let isLoading = $state(false);
let errorMessage = $state('');
let sent = $state(false);
const handleClick = async () => {
const data = { name, email, title, description };
@@ -7,14 +7,14 @@
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = '';
let email = '';
let title = '';
let description = '';
let name = $state('');
let email = $state('');
let title = $state('');
let description = $state('');
let isLoading = false;
let errorMessage = '';
let sent = false;
let isLoading = $state(false);
let errorMessage = $state('');
let sent = $state(false);
async function handleClick() {
const data = { name, email, title, description };
@@ -6,8 +6,8 @@
src: string;
}
let isAddIcon = false;
let hoveredIndex: number | null = null;
let isAddIcon = $state(false);
let hoveredIndex: number | null = $state(null);
let avatars: Avatar[] = [
{ src: 'avatar_a' },
@@ -48,14 +48,14 @@
<div class="flex flex-wrap justify-evenly gap-y-3 mt-3">
{#each avatars as avatar, index}
<button
on:click={() => {
onclick={() => {
selectAvatar(avatar);
}}
on:mouseenter={() => {
onmouseenter={() => {
hoveredIndex = index;
isAddIcon = true;
}}
on:mouseleave={() => {
onmouseleave={() => {
hoveredIndex = null;
isAddIcon = false;
}}
@@ -63,7 +63,7 @@
>
{#each backgrounds as background}
<button
on:click={() => {
onclick={() => {
selectBackground(background);
}}
class="flex w-[30%] items-center shadow-sm hover:scale-110 transition-all duration-300"
@@ -3,9 +3,13 @@
import { nodeStore, htmlBody, openModal } from './store';
import { Button } from '@cio/ui/base/button';
export let isDisabled: boolean;
export let isDownloading: boolean;
export let text: string = 'Generate Progress Report';
interface Props {
isDisabled: boolean;
isDownloading: boolean;
text?: string;
}
let { isDisabled, isDownloading = $bindable(), text = 'Generate Progress Report' }: Props = $props();
function convertToPng() {
isDownloading = true;
@@ -75,7 +75,7 @@
<div class="flex flex-wrap justify-between gap-y-3 mt-3">
{#each moods as mood}
<button
on:click={() => {
onclick={() => {
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"
@@ -26,11 +26,13 @@
let node: any;
$: nodeStore.set(node);
$effect(() => {
nodeStore.set(node);
});
$: rootBgImage = `https://assets.cdn.clsrio.com/progress-report/backgrounds/${
let rootBgImage = $derived(`https://assets.cdn.clsrio.com/progress-report/backgrounds/${
$htmlBody.background || 'blue_tetiary_background'
}.webp`;
}.webp`);
</script>
<div
@@ -43,7 +45,7 @@
{#if $openModal.fullscreen}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
onclick={() => ($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="" />
@@ -51,7 +53,7 @@
{:else}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
onclick={() => ($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="" />
@@ -118,7 +120,7 @@
<div
class="progress-range rounded-full w-full h-full"
style="background: {$backgroundColor}; width: {$htmlBody.progress}%"
/>
></div>
</div>
<p class="text-[12px] font-semibold mt-1">Progress Achieved: {$htmlBody.progress}%</p>
</div>
@@ -2,22 +2,36 @@
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;
interface Props {
activityName: string;
formattedTime: string;
displayTime: string;
resetTimer: () => void;
toggleSound: () => void;
toggleTimer: () => void;
restartTimer: () => void;
isPaused: boolean;
soundOn: boolean;
}
let {
activityName,
formattedTime,
displayTime,
resetTimer,
toggleSound,
toggleTimer,
restartTimer,
isPaused,
soundOn
}: Props = $props();
</script>
<div transition:fly={{ y: 100, delay: 0, easing: sineInOut }}>
<div class="px-6 pt-8 text-center">
<button
type="button"
on:click={resetTimer}
onclick={resetTimer}
class="text-[10px] text-[#656565] font-semibold uppercase underline"
>set timer for another activity</button
>
@@ -36,7 +50,7 @@
<div class="flex">
<button
type="button"
on:click={toggleSound}
onclick={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}
@@ -48,7 +62,7 @@
>
<button
type="button"
on:click={toggleTimer}
onclick={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}
@@ -60,7 +74,7 @@
>
<button
type="button"
on:click={restartTimer}
onclick={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" />
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { sineInOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import WinningLine from './winning-line.svelte';
@@ -9,18 +11,22 @@
click: (rowIndex: number, columnIndex: number) => Game;
}
export let game: Game;
export let winner = null;
interface Props {
game: Game;
winner?: any;
}
let customDelay: number;
let { game = $bindable(), winner = null }: Props = $props();
$: {
let customDelay: number = $state();
run(() => {
if ($changeToComputer.change) {
customDelay = 400;
} else {
customDelay = 0;
}
}
});
</script>
<div class="relative">
@@ -31,7 +37,7 @@
{#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={() => {
onclick={() => {
game = game.click(rowIndex, columnIndex);
}}
>
@@ -10,7 +10,7 @@ changeToComputer.subscribe((value) => {
}
});
export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void, onTie: () => void }) {
export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void; onTie: () => void }) {
const board = [
['', '', ''],
['', '', ''],
@@ -23,7 +23,7 @@ export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void, onTi
return {
board,
click(rowIndex:number, columnIndex:number) {
click(rowIndex: number, columnIndex: number) {
if (!done && !board[rowIndex][columnIndex]) {
board[rowIndex][columnIndex] = turn;
turn = turn === 'X' ? 'O' : 'X';
@@ -43,25 +43,24 @@ export function TicTacToe({ onWin, onTie }: { onWin: (winner: any) => void, onTi
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;
}
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.");
console.error('AI could not find a valid move.');
}
}
}
}
}
@@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import type {Writable} from 'svelte/store'
import type { Writable } from 'svelte/store';
interface Scores {
X: number;
@@ -1,6 +1,12 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { fade } from 'svelte/transition';
export let winner: Winner | null = null;
interface Props {
winner?: Winner | null;
}
let { winner = null }: Props = $props();
interface Winner {
direction: 'horizontal' | 'vertical' | 'diagonal';
@@ -9,13 +15,15 @@
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());
run(() => {
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}
-1
View File
@@ -1,4 +1,3 @@
// Re-export all components for convenient importing
// Usage: import { Footer, Navigation, Hero } from '$lib/components'
export * from './components';
+7 -7
View File
@@ -2,31 +2,31 @@ export const faqs = [
{
question: 'Who can benefit from ClassroomIO?',
answer:
'We built ClassroomIO from the ground up for Educators, meaning if you teach online or in a physical classroom you are good to go. It works best for Educators who already have students and want an efficient way to manage their learning experience.',
'We built ClassroomIO from the ground up for Educators, meaning if you teach online or in a physical classroom you are good to go. It works best for Educators who already have students and want an efficient way to manage their learning experience.'
},
{
question: 'Will I be able to set up ClassroomIO without technical skills?',
answer:
'Most definitely. You can use ClassroomIO as a no-code tool to build your own LMS platform. We provide an intuitive user interface and documentation to help you give your students the best learning experience.',
'Most definitely. You can use ClassroomIO as a no-code tool to build your own LMS platform. We provide an intuitive user interface and documentation to help you give your students the best learning experience.'
},
{
question: 'How much customization can I do with ClassroomIO?',
answer:
'Things like theming, organization name, landing pages, content that your students see and many more customization options to come.',
'Things like theming, organization name, landing pages, content that your students see and many more customization options to come.'
},
{
question: 'Is ClassroomIO suitable for a large number of students?',
answer:
'Absolutely. ClassroomIO is built for a large number of students in any course. You can have multiple courses with as many students as you need.',
'Absolutely. ClassroomIO is built for a large number of students in any course. You can have multiple courses with as many students as you need.'
},
{
question: 'What kind of team plans does ClassroomIO offer?',
answer:
'ClassroomIO offers a Team plan designed to cater to organizations of any size. Depending on the plan you select, you can invite other teachers into your organization.',
'ClassroomIO offers a Team plan designed to cater to organizations of any size. Depending on the plan you select, you can invite other teachers into your organization.'
},
{
question: 'Can I try ClassroomIO before I buy it?',
answer:
'Definitely. We’ve got a generous free plan to help you get started with the platform. As your requirements grow, we’ve got other plans that can grow with you.',
},
'Definitely. We’ve got a generous free plan to help you get started with the platform. As your requirements grow, we’ve got other plans that can grow with you.'
}
];
+12 -2
View File
@@ -1,6 +1,16 @@
<script lang="ts" context="module">
<script>
/**
* @typedef {Object} Props
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { children } = $props();
</script>
<script lang="ts" module>
import { CustomImg as img } from '$lib/components';
export { img };
</script>
<slot />
{@render children?.()}
@@ -112,7 +112,7 @@
<div
class="lg:w-full max-w-7xl 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>
<h1 class="text-3xl">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.
@@ -132,7 +132,7 @@
/>
<div class="w-full flex flex-col justify-start align-start gap-y-2">
<h1 class="text-xl font-medium">{benefits.title}</h1>
<h1 class="text-md">{benefits.title}</h1>
{#each benefits.points as point}
<div class="flex flex-row justify-start items-start gap-x-3">
@@ -162,7 +162,7 @@
<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>
<h1 class="text-3xl">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
@@ -187,7 +187,7 @@
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>
<h1 class="text-2xl">{feature.title}</h1>
<p class="leading-8 text-lg text-gray-500">
{feature.description}
</p>
@@ -92,11 +92,11 @@
<div
class="w-full gap-y-4 flex flex-col lg:items-center items-start lg:justify-center justify-start"
>
<Badge class="py-2! px-4!" variant="outline">Our benefits</Badge>
<Badge class="py-1! px-4!" variant="outline">Our benefits</Badge>
<div
class="lg:w-full max-w-7xl 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>
<h1 class="text-3xl">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
@@ -116,7 +116,7 @@
/>
<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="text-lg">{benefits.title}</p>
<div class="flex flex-row justify-start items-start gap-x-3">
<p class="leading-8 text-gray-500 font-[#656565] font-light">
@@ -136,14 +136,14 @@
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">
<p class="text-sm text-[#4B5563] text-center">
Explore our features
</p>
</div>
<div
class="lg:w-full max-w-7xl 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>
<h1 class="text-3xl">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.
@@ -154,9 +154,9 @@
<div class="w-full max-w-7xl 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
class="w-full 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"
: ''} flex-col lg:justify-evenly 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"
@@ -166,7 +166,7 @@
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>
<h1 class="text-2xl">{feature.title}</h1>
<p class="leading-8 text-lg text-gray-500">
{feature.description}
</p>
@@ -86,7 +86,7 @@
<Badge variant="outline">Amazing benefits</Badge>
<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>
<h1 class="text-3xl">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>
@@ -106,8 +106,8 @@
/>
<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">
<h1 class="text-lg">{benefits.title}</h1>
<p class="text-base leading-8 text-gray-500 font-[#656565] font-light">
{benefits.description}
</p>
</div>
@@ -127,7 +127,7 @@
<div
class="lg:w-full max-w-7xl 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>
<h1 class="text-3xl">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.
@@ -151,7 +151,7 @@
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>
<h1 class="text-2xl">{feature.title}</h1>
<p class="leading-8 text-lg text-gray-500">
{feature.description}
</p>
@@ -185,10 +185,10 @@
>
<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">
<h1 class="text-2xl text-white">
Did you know you can request for custom features?
</h1>
<p class="leading-8 text-[#DEDEDE] text-xl font-light">
<p class="leading-8 text-[#DEDEDE] text-base">
Companies get the opportunity to request custom features from us.
</p>
</div>
@@ -1,5 +1,8 @@
import { redirect } from '@sveltejs/kit';
export const load = async () => {
redirect(307, 'https://docs.google.com/presentation/d/1eqvNPNk167ZcP21gZGVAatdDpWAFL9lfmWJHHrzsVFc');
redirect(
307,
'https://docs.google.com/presentation/d/1eqvNPNk167ZcP21gZGVAatdDpWAFL9lfmWJHHrzsVFc'
);
};
@@ -7,8 +7,5 @@ export const load = async ({ request }) => {
event: 'privacy page visited'
});
redirect(
307,
'https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594'
);
redirect(307, 'https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594');
};
+3 -3
View File
@@ -9,7 +9,7 @@
import '../app.css';
export let data;
let { children, data } = $props();
onMount(() => {
if (dev) {
@@ -17,7 +17,7 @@
}
});
$: metaTags = extend(true, {}, data.baseMetaTags, page.data.pageMetaTags);
let metaTags = $derived(extend(true, {}, data.baseMetaTags, page.data.pageMetaTags));
</script>
<MetaTags {...metaTags} />
@@ -29,7 +29,7 @@
{#if page.status === 404}
<NotFound className="mt-5" />
{:else}
<slot />
{@render children?.()}
{/if}
</PageTransition>
+1 -1
View File
@@ -1,7 +1,7 @@
<script>
import { PageSignupCTA, PageHeader, BlogListItem } from '$lib/components';
export let data;
let { data } = $props();
</script>
<svelte:head>
@@ -6,7 +6,7 @@
import { BlogListItem } from '$lib/components';
export let data;
let { data } = $props();
</script>
<div class=" mt-[10%] md:mt-16">
@@ -28,7 +28,7 @@
<!-- Post -->
<div class="prose border-b-2 border-gray-200 pb-4 pt-2">
<svelte:component this={data.content} />
<data.content />
<!-- Tags -->
<div class="flex gap-2 py-4">
{#each data.meta.tags as tag}
+2 -2
View File
@@ -8,7 +8,7 @@
import { PageHeader } from '$lib/components';
import FeedbackForm from '$lib/features/contact/feedback-form.svelte';
let currentForm: FORM_TYPE_KEY;
let currentForm: FORM_TYPE_KEY = $state();
const formList = [
{
@@ -72,7 +72,7 @@
>
{#each formList as list}
<button
on:click={() => onChange(list.id, list.to)}
onclick={() => onChange(list.id, list.to)}
class="w-full rounded-md border-[1.5px] p-3 text-start {currentForm === list.id
? `border-[#0233BD]`
: `border-gray-200`} flex items-center justify-between"
@@ -2,7 +2,7 @@
import { PageHeader } from '$lib/components';
import { Button } from '@cio/ui/base/button';
export let data;
let { data } = $props();
</script>
<svelte:head>
+3 -3
View File
@@ -6,7 +6,7 @@
import { PageSignupCTA, PageHeader } from '$lib/components';
let isYearlyPlan = false;
let isYearlyPlan = $state(false);
function toggleIsYearlyPlan() {
isYearlyPlan = !isYearlyPlan;
@@ -32,14 +32,14 @@
? '#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
onclick={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
onclick={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"
+2 -2
View File
@@ -79,7 +79,7 @@
<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>
<h1 class="text-3xl">Latest Features</h1>
</div>
<Timeline items={newFeatures} />
@@ -89,7 +89,7 @@
<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>
<h1 class="text-3xl">Upcoming Features</h1>
</div>
<Timeline items={upcomingFeatures} />
+29 -37
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { PageSignupCTA, PageHeader } from '$lib/components';
import { Button } from '@cio/ui/base/button';
import * as Accordion from '@cio/ui/base/accordion';
const whyStart = [
{
@@ -41,35 +41,31 @@
}
];
let faqs = [
const faqs = [
{
value: 'tutors',
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
'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'
},
{
value: 'equipment',
question: 'What equipment do I need to teach on ClassroomIO?',
answer: 'You just need internet access and a laptop.',
showAnswer: false
answer: 'You just need internet access and a laptop.'
},
{
value: 'apply',
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
'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'
},
{
value: 'earn',
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
'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.'
}
];
function toggleAnswer(index: number) {
faqs[index].showAnswer = !faqs[index].showAnswer;
}
</script>
<svelte:head>
@@ -88,7 +84,7 @@
<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>
<h1 class="text-3xl lg:text-5xl">Teach with us</h1>
<p class="my-5 text-center lg:w-[77%] lg:text-left">
With ClassroomIO, you can make side income while teaching the subject you love
</p>
@@ -108,7 +104,7 @@
<div class="mx-auto mt-10">
<div class="border-b border-slate-100 px-[10%] pb-10">
<h1 class="flex items-center justify-center gap-3 pb-28 text-center text-4xl font-bold">
<h1 class="flex items-center justify-center gap-3 pb-28 text-center text-3xl">
Why you should teach with us? <img src="/teach/star.svg" alt="" />
</h1>
@@ -119,7 +115,7 @@
>
<img src={why.src} alt="" class="h-[80px] w-[80px]" />
<figcaption>
<h1 class="text-base font-bold">{why.header}</h1>
<h1 class="text-base">{why.header}</h1>
<p class="mt-1 text-sm">
{why.subText}
</p>
@@ -130,14 +126,14 @@
</div>
<div class="mt-10 px-[10%] py-10">
<h1 class="mb-28 flex items-center justify-center gap-3 text-center text-4xl font-bold">
<h1 class="mb-28 flex items-center justify-center gap-3 text-center text-3xl">
How to get started <img src="/teach/thumbs-up.svg" alt="" />
</h1>
<div class="mx-auto mt-5 flex flex-wrap justify-between gap-y-10">
{#each steps as step, i}
<div class="px-10 py-5 lg:max-w-[40%]">
<h1 class="text-2xl font-bold">
<h1 class="text-2xl">
<span class="mr-3 rounded-md bg-[#F7F7F7] px-2 py-1">{i + 1}</span>
{step.title}
</h1>
@@ -159,7 +155,7 @@
</div>
<div class="my-10 h-[75%] bg-[#F5F8FE] px-2 py-12 lg:h-[80%] lg:px-[12%]">
<h1 class="flex items-center justify-center gap-3 text-center text-2xl font-bold">
<h1 class="flex items-center justify-center gap-3 text-center text-2xl">
Word from our fufilled facilitators
</h1>
@@ -180,25 +176,21 @@
</div>
<div class="mx-auto my-10 mt-10 px-10 lg:max-w-[65%] lg:px-[15%]">
<h1 class="text-center text-2xl font-bold">Frequently asked questions</h1>
<h1 class="text-center text-xl">Frequently asked questions</h1>
<div class="mt-10">
{#each faqs as faq, index}
<div class="border-b border-black pb-2 pt-1 lg:px-3">
<div>
<button
type="button"
class="my-2 flex w-full justify-between text-left text-sm font-medium md:text-center md:text-lg"
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}
<Accordion.Root type="single" class="w-full">
{#each faqs as faq}
<Accordion.Item value={faq.value}>
<Accordion.Trigger class="text-left md:text-center">
{faq.question}
</Accordion.Trigger>
<Accordion.Content>
{faq.answer}
</Accordion.Content>
</Accordion.Item>
{/each}
</Accordion.Root>
</div>
</div>
+8 -1
View File
@@ -1,5 +1,12 @@
<script>
import { PageSignupCTA } from '$lib/components';
/**
* @typedef {Object} Props
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { children } = $props();
</script>
<svelte:head>
@@ -29,7 +36,7 @@
<meta name="twitter:image" content="https://brand.cdn.clsrio.com/og/free-tools.png" />
</svelte:head>
<slot />
{@render children?.()}
<PageSignupCTA
header="Scale Your Bootcamp Today with ClassroomIO"
+3 -3
View File
@@ -9,7 +9,7 @@
<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">
<h1 class="text-3xl md:text-5xl">
Explore our free <span class="text-blue-700">tools</span>
</h1>
<p class="my-4 md:my-8">
@@ -58,7 +58,7 @@
</PageHeader>
<div class="p-10 px-2 md:px-0 bg-white">
<h1 class="text-center font-bold text-3xl">Featured Tools</h1>
<h1 class="text-center text-2xl">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}
@@ -68,7 +68,7 @@
>
<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>
<h1 class="text-sm md:text-base">{tool.title}</h1>
<p class="text-xs text-gray-600 mt-1.5">
{tool.subText}
</p>
@@ -1,12 +1,14 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { ToolsHeader } from '$lib/components';
let namesInput = '';
let numNames = 0;
let wordCount = 0;
let selectedNames: string[] = [];
let avatarUrls: string[] = [];
let avatarUrlsFetched = false;
let namesInput = $state('');
let numNames = $state(0);
let wordCount = $state(0);
let selectedNames: string[] = $state([]);
let avatarUrls: string[] = $state([]);
let avatarUrlsFetched = $state(false);
// function to shuffle an array
const shuffleArray = (array: string[]) => {
@@ -98,7 +100,9 @@
}
}
$: wordCount = namesInput?.split(',').filter((name) => name.trim())?.length ?? 0;
run(() => {
wordCount = namesInput?.split(',').filter((name) => name.trim())?.length ?? 0;
});
</script>
<svelte:head>
@@ -123,7 +127,7 @@
<section class="bg-white px-5 md:px-0">
<ToolsHeader>
<img src="/free-tools/name-picker.svg" class="mx-auto w-[15%] rounded-full border md:w-[5%]" alt="" />
<h1 class="my-3 text-4xl font-bold text-[#040F2D] md:text-6xl">Random Name Picker</h1>
<h1 class="my-3 text-3xl text-[#040F2D] md:text-5xl">Random Name Picker</h1>
<p class="text-md mx-auto font-light text-[#656565] md:w-[45%] md:font-normal">
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.
@@ -132,24 +136,24 @@
<div class="mx-auto my-10 rounded-md border bg-white py-8 shadow-md md:w-[60%] md:py-[3%]">
<div class="mx-auto w-[85%] bg-white md:w-[70%]">
<h1 class="text-sm font-bold">List of names</h1>
<h1 class="text-sm">List of names</h1>
<!-- container -->
<div
class="relative mt-5 h-[12rem] overflow-hidden rounded-sm border-2 bg-[#F1F2F4] pl-3 pt-2 focus-within:border-[#0233BD]"
class="relative mt-5 h-48 overflow-hidden rounded-sm border-2 bg-[#F1F2F4] pl-3 pt-2 focus-within:border-[#0233BD]"
>
<!-- sidebar -->
<div class="absolute right-2 top-3 flex flex-col gap-y-3 md:right-7">
<button
type="button"
on:click={sortRandom}
onclick={sortRandom}
class="flex items-center justify-center rounded-full bg-[#D9E0F5] p-1.5"
>
<img src="/sort-icon.svg" class="w-2.5" alt="" />
</button>
<button
type="button"
on:click={unsortRandom}
onclick={unsortRandom}
class="flex items-center justify-center rounded-full bg-[#D9E0F5] p-1.5"
>
<img src="/unsort-icon.svg" class="w-2.5" alt="" />
@@ -175,7 +179,7 @@
<p class="border-b py-4 text-xs">Each name must be separated by a comma</p>
<div class="mt-10">
<h1 class="text-sm font-bold">Number of Names</h1>
<h1 class="text-sm">Number of Names</h1>
<!-- number input & button -->
<div class="mt-5 flex w-full flex-wrap justify-between gap-y-3">
<input
@@ -185,7 +189,7 @@
/>
<button
type="button"
on:click={selectName}
onclick={selectName}
class="w-[70%] rounded-md bg-[#0F62FE] py-3 text-sm text-white md:w-[35%] md:py-0"
>Select name(s) at random</button
>
@@ -196,7 +200,7 @@
<div class="mb-5 mt-10 flex items-center justify-center gap-3 bg-[#F1F6FF] py-3">
<img src="/free-tools/name-picker.svg" class="w-[12%] md:w-[9%]" alt="" />
{#if selectedNames.length > 0}
<h1 class="text-xl font-bold text-[#0233BD]">Selected Names</h1>
<h1 class="text-xl text-[#0233BD]">Selected Names</h1>
{/if}
</div>
@@ -226,12 +230,12 @@
<div class="mt-3 flex flex-wrap items-center justify-between md:justify-center md:gap-3">
<button
type="button"
on:click={resetEntry}
onclick={resetEntry}
class="w-[45%] rounded-md border py-2 text-xs md:w-[20%] md:text-sm">Reset Entries</button
>
<button
type="button"
on:click={selectName}
onclick={selectName}
class="w-[45%] rounded-md bg-[#0F62FE] py-2 text-xs text-white md:w-[27%] md:text-sm"
>Select other names</button
>
@@ -1,4 +1,6 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from 'svelte';
import { ToolsHeader } from '$lib/components';
@@ -12,15 +14,15 @@
isVisible: boolean;
}
let addInput: HTMLInputElement;
let addInput: HTMLInputElement = $state();
let buzzSound: HTMLAudioElement;
let todoList: TodoItem[] = [];
let todoList: TodoItem[] = $state([]);
let countdownTime: number = 25 * 60;
let countdownDisplay: string = '25:00';
let timerState: string = 'pomodoro';
let countdownDisplay: string = $state('25:00');
let timerState: string = $state('pomodoro');
let isPaused: boolean = true;
let isPaused: boolean = $state(true);
let timerInterval: any = null;
let isVisible: boolean = false;
@@ -174,7 +176,7 @@
class="mx-auto w-[15%] rounded-full border md:w-[5%]"
alt=""
/>
<h1 class="my-3 text-4xl font-bold text-[#040F2D] md:text-6xl">Pomodoro Timer</h1>
<h1 class="my-3 text-3xl text-[#040F2D] md:text-5xl">Pomodoro Timer</h1>
<p class="mx-auto mt-10 text-sm font-light text-[#656565] md:w-[45%] md:font-normal">
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
@@ -193,7 +195,7 @@
<div class="item-center flex justify-evenly">
<button
type="button"
on:click={() => setTimerState('pomodoro')}
onclick={() => setTimerState('pomodoro')}
class:bg-[#3ADFECED]={timerState === 'pomodoro'}
class:bg-[#0233BD]={timerState !== 'pomodoro'}
class="w-[25%] rounded-md py-1.5 text-xs italic transition-all duration-500 hover:bg-[#3ADFECED] md:py-2 md:text-sm md:font-medium"
@@ -201,7 +203,7 @@
>
<button
type="button"
on:click={() => setTimerState('long-break')}
onclick={() => setTimerState('long-break')}
class:bg-[#3ADFECED]={timerState === 'long-break'}
class:bg-[#0233BD]={timerState !== 'long-break'}
class="w-[25%] rounded-md py-1.5 text-xs italic transition-all duration-500 hover:bg-[#3ADFECED] md:py-2 md:text-sm md:font-medium"
@@ -209,7 +211,7 @@
</button>
<button
type="button"
on:click={() => setTimerState('short-break')}
onclick={() => setTimerState('short-break')}
class:bg-[#3ADFECED]={timerState === 'short-break'}
class:bg-[#0233BD]={timerState !== 'short-break'}
class="w-[25%] rounded-md py-1.5 text-xs italic transition-all duration-500 hover:bg-[#3ADFECED] md:py-2 md:text-sm md:font-medium"
@@ -223,7 +225,7 @@
<!-- controls -->
<div class="flex items-center justify-center gap-7">
<!-- reset -->
<button type="button" on:click={resetCountdown}>
<button type="button" onclick={resetCountdown}>
<img
src="/free-tools/pomodoro/restart-icon.svg"
alt="Restart Icon"
@@ -234,14 +236,14 @@
<!-- 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="rounded-md border bg-white px-14 py-3 text-base font-bold uppercase text-[#0542CC] transition-all duration-300 hover:border hover:border-white hover:bg-transparent hover:text-white"
onclick={isPaused ? startCountdown : pauseCountdown}
class="rounded-md border bg-white px-14 py-3 text-base uppercase text-[#0542CC] transition-all duration-300 hover:border hover:border-white hover:bg-transparent hover:text-white"
>
{isPaused ? 'Start' : 'Pause'}
</button>
<!-- next -->
<button type="button" on:click={nextTimerState}>
<button type="button" onclick={nextTimerState}>
<img
src="/free-tools/pomodoro/timer-play-icon.svg"
alt="Play Icon"
@@ -266,7 +268,7 @@
{#each todoList as todo, i}
<div class="border p-5 text-black">
{#if todo.isEditing}
<form on:submit|preventDefault={() => setEditing(i, false)}>
<form onsubmit={preventDefault(() => setEditing(i, false))}>
<p class="text-left text-xs font-semibold text-[#656565]">Pomodoro name</p>
<input
type="text"
@@ -277,7 +279,7 @@
<div class="mt-5 flex items-center justify-end gap-5">
<button
type="button"
on:click={() => deleteTodo(i)}
onclick={() => deleteTodo(i)}
class="rounded-sm bg-[#F7F7F7] px-5 py-2 text-xs font-semibold"
>Discard</button
>
@@ -295,11 +297,11 @@
<!-- pen and menu icon -->
<div class="relative flex w-[15%] justify-between">
<button type="button" on:click={() => setEditing(i, true)}>
<button type="button" onclick={() => 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)}>
<button type="button" onclick={() => (todo.isVisible = !todo.isVisible)}>
<img src="/free-tools/pomodoro/menu-icon.svg" alt="Menu icon" class="w-5" />
</button>
@@ -309,14 +311,14 @@
>
<button
type="button"
on:click={() => markTodoAsDone(i)}
onclick={() => markTodoAsDone(i)}
class="w-full px-5 py-3 text-left font-medium"
>
Mark as done
</button>
<button
type="button"
on:click={() => deleteTodo(i)}
onclick={() => deleteTodo(i)}
class="w-full rounded-b-[5px] bg-red-600 px-5 py-3 text-left font-medium text-white"
>
Delete
@@ -335,7 +337,7 @@
<button
type="button"
on:click={() => {
onclick={() => {
if (todo.isPaused) {
startCountdown();
} else {
@@ -373,7 +375,7 @@
<!-- todo button -->
<button
type="button"
on:click={addTodo}
onclick={addTodo}
class="mt-10 flex w-full items-center justify-center gap-3 rounded-md bg-[#1D4ED8] py-3 text-center font-medium"
>
Add new item
@@ -16,11 +16,11 @@
import DownloadButton from '$lib/features/tools/progress/download-button.svelte';
const MAX_CHARS = 160;
let remainingChars = MAX_CHARS;
let showReport: boolean = false;
let showSetter: boolean = true;
let remainingChars = $state(MAX_CHARS);
let showReport: boolean = $state(false);
let showSetter: boolean = $state(true);
let isDownloading: boolean = false;
let isDisabled: boolean;
let isDisabled: boolean = $state();
// sets the result of the mini validation for the htmlBody store
const unsubscribe = isFormComplete.subscribe((value) => {
@@ -88,7 +88,7 @@
<section class=" w-full px-1 md:w-full md:px-0">
<ToolsHeader>
<img src="/free-tools/progress-report.svg" class="mx-auto w-[15%] rounded-full border md:w-[5%]" alt="" />
<h1 class="my-3 text-4xl font-bold text-[#040F2D] md:text-6xl">Progress Report</h1>
<h1 class="my-3 text-3xl text-[#040F2D] md:text-5xl">Progress Report</h1>
<p class="text-md mx-auto font-light text-[#656565] md:w-[45%] md:font-normal">
Generate cool reports of your learning progress. Share reports with your network for collaborative learning
</p>
@@ -113,7 +113,7 @@
<div class="mb-5 flex justify-end md:hidden">
<button
type="button"
on:click={() => {
onclick={() => {
showSetter = false;
showReport = true;
}}
@@ -130,7 +130,7 @@
<input
type="text"
bind:value={$htmlBody.name}
on:input={(event) => handleInputChange(event, 'name')}
oninput={(event) => handleInputChange(event, 'name')}
placeholder="Enter your name here"
class="my-3 w-full rounded-sm border bg-[#F1F2F4] px-3 py-2 text-xs outline-none placeholder:text-sm placeholder:text-[#ADADAD]"
/>
@@ -141,7 +141,7 @@
<p class="text-sm text-[#656565]">Select your mood</p>
<button
type="button"
on:click={() => ($openModal.mood = true)}
onclick={() => ($openModal.mood = true)}
class="my-3 flex w-full items-center justify-between rounded-sm border bg-[#F1F2F4] px-3 py-2 text-sm text-gray-400 outline-none"
>
{#if $htmlBody.mood.text}
@@ -172,7 +172,7 @@
<textarea
bind:value={$htmlBody.learning}
maxlength={MAX_CHARS}
on:input={(event) => handleInputChange(event, 'learning')}
oninput={(event) => handleInputChange(event, 'learning')}
placeholder="Tell us what you are learning"
class="my-3 h-[17vh] w-full rounded-sm border bg-[#F1F2F4] px-3 py-2 text-sm outline-none placeholder:text-sm placeholder:text-[#ADADAD]"
></textarea>
@@ -189,7 +189,7 @@
max="100"
bind:value={$htmlBody.progress}
class="range-input"
on:input={(event) => handleInputChange(event, 'progress')}
oninput={(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>
@@ -202,7 +202,7 @@
<p class="text-[12px] text-[#656565] md:text-sm">Choose your avatar</p>
<button
type="button"
on:click={() => ($openModal.avatar = true)}
onclick={() => ($openModal.avatar = true)}
class="mt-3 flex w-full items-center justify-between gap-2 bg-[#F7F7F7] px-5 py-1.5 md:w-[65%] md:justify-center md:px-0"
>
<img
@@ -223,7 +223,7 @@
<p class="text-[12px] text-[#656565] md:text-sm">Choose your background</p>
<button
type="button"
on:click={() => ($openModal.background = true)}
onclick={() => ($openModal.background = true)}
class="mt-3 flex w-full items-center justify-between gap-2 bg-[#F7F7F7] px-5 py-1.5 md:w-[65%] md:justify-center md:px-0"
>
<img
@@ -265,19 +265,19 @@
<div class="mx-auto mt-2 flex w-[60%] justify-evenly gap-5 rounded-xl border px-5 py-2 md:w-[80%]">
<button
type="button"
on:click={shareOnFacebook}
onclick={shareOnFacebook}
class="w-10 transition-all duration-300 hover:scale-[1.2]"
>
<Facebook size={32} />
</button>
<button
type="button"
on:click={shareOnLinkedIn}
onclick={shareOnLinkedIn}
class="w-10 transition-all duration-300 hover:scale-[1.2]"
>
<Linkedin size={32} />
</button>
<button type="button" on:click={shareOnTwitter} class="w-5 transition-all duration-300 hover:scale-[1.2]">
<button type="button" onclick={shareOnTwitter} class="w-5 transition-all duration-300 hover:scale-[1.2]">
<img src="/twitter_logo.png" alt="X.com" />
</button>
</div>
@@ -290,7 +290,7 @@
<div class="mb-5 flex justify-end">
<button
type="button"
on:click={() => {
onclick={() => {
showReport = false;
showSetter = true;
}}
@@ -313,19 +313,19 @@
<div class="mx-auto mt-2 flex w-[60%] justify-evenly gap-5 rounded-xl border px-5 py-2 md:w-[80%]">
<button
type="button"
on:click={shareOnFacebook}
onclick={shareOnFacebook}
class="w-10 transition-all duration-300 hover:scale-[1.2]"
>
<Facebook size={32} />
</button>
<button
type="button"
on:click={shareOnLinkedIn}
onclick={shareOnLinkedIn}
class="w-10 transition-all duration-300 hover:scale-[1.2]"
>
<Linkedin size={32} />
</button>
<button type="button" on:click={shareOnTwitter} class="w-5 transition-all duration-300 hover:scale-[1.2]">
<button type="button" onclick={shareOnTwitter} class="w-5 transition-all duration-300 hover:scale-[1.2]">
<img src="/twitter_logo.png" alt="X.com" />
</button>
</div>
@@ -1,4 +1,6 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { sineInOut } from 'svelte/easing';
@@ -7,18 +9,18 @@
let timer: NodeJS.Timeout;
let buzzSound: HTMLAudioElement;
let isNextStep: boolean = false;
let isPaused: boolean = false;
let soundOn: boolean = true;
let isNextStep: boolean = $state(false);
let isPaused: boolean = $state(false);
let soundOn: boolean = $state(true);
let hours: number = 0;
let minutes: number = 0;
let seconds: number = 0;
let hours: number = $state(0);
let minutes: number = $state(0);
let seconds: number = $state(0);
let activityName: string = '';
let displayTime: string = '00:00:00';
let activityName: string = $state('');
let displayTime: string = $state('00:00:00');
let totalSeconds: number = 0;
let formattedTime: string = '00:00';
let formattedTime: string = $state('00:00');
export function toggleTimer() {
if (isPaused) {
@@ -146,7 +148,7 @@
<!-- countdown setter -->
{#if !isNextStep}
<div transition:fly={{ y: -300, delay: 0, easing: sineInOut }} class="bg-white px-6 py-8">
<form on:submit|preventDefault={startTimer}>
<form onsubmit={preventDefault(startTimer)}>
<div>
<h1 class="text-sm font-bold">Name of activity</h1>
<input
@@ -6,10 +6,10 @@
import { scores, changeToComputer } from '$lib/features/tools/tic-tac-toe/store';
import { ToolsHeader } from '$lib/components';
let startGame = false;
let message: string | null = null;
let winner: Winner | null = null;
let game = createGame();
let startGame = $state(false);
let message: string | null = $state(null);
let winner: Winner | null = $state(null);
let game = $state(createGame());
interface Winner {
symbol?: string;
@@ -114,7 +114,7 @@
class="absolute top-[43%] z-[150] flex w-full flex-col items-center justify-center"
>
<div
class="flex h-[8vh] w-full items-center justify-center gap-10 bg-white py-1 font-bold text-[#0233BD] md:h-[13vh]"
class="flex h-[8vh] w-full items-center justify-center gap-10 bg-white py-1 text-[#0233BD] md:h-[13vh]"
>
{#if message != 'Tie game!'}
<img src="/free-tools/tic-tac/winner-star-icon.svg" alt="Star icon" class="h-full" />
@@ -124,8 +124,8 @@
<button
type="button"
on:click={playAgain}
class="mt-10 rounded-md border border-white bg-[#0F62FE] px-16 pb-1.5 pt-1 text-xs font-bold md:text-base"
onclick={playAgain}
class="mt-10 rounded-md border border-white bg-[#0F62FE] px-16 pb-1.5 pt-1 text-xs md:text-base"
>Play Again</button
>
</div>
@@ -136,7 +136,7 @@
<div class="flex h-full w-full flex-col items-center justify-between border p-5">
<header class="flex w-full items-center justify-between">
<button
on:click={changePlayer}
onclick={changePlayer}
class="flex w-[35%] flex-row items-center gap-2 text-[10px] font-medium leading-3 md:w-28 md:text-xs"
>
<img src="/free-tools/tic-tac/computer-icon.svg" alt="Computer icon" class="w-4 md:w-6" />
@@ -147,7 +147,7 @@
{/if}
</button>
<button
on:click={newGame}
onclick={newGame}
class="flex w-24 flex-col items-center gap-1 rounded-md border bg-white p-2 text-[10px] font-medium leading-3 text-[#0542CC] shadow-md"
>
<img src="/free-tools/tic-tac/restart-icon.svg" alt="Restart icon" class="w-3 md:w-4" />
@@ -182,7 +182,7 @@
<div class="flex flex-col gap-3">
<button
type="button"
on:click={() => {
onclick={() => {
changePlayer();
startTicTac();
}}
@@ -192,7 +192,7 @@
>
<button
type="button"
on:click={() => {
onclick={() => {
startTicTac();
}}
class="rounded-md border border-white bg-[#0F62FE] px-16 pb-1.5 pt-1 text-sm font-bold"
+7 -2
View File
@@ -1,11 +1,16 @@
<script lang="ts">
import { fade } from 'svelte/transition';
export let url: string;
interface Props {
url: string;
children?: import('svelte').Snippet;
}
let { url, children }: Props = $props();
</script>
{#key url}
<div class="transition" in:fade>
<slot />
{@render children?.()}
</div>
{/key}