feat: introducing @classroomio/course-app (#490)

* feat: v1 templates

* update nvm

* working on the displaying the course content

* working on the displaying the course content

* displayed the content

* finished with the logic for rendering the content for all the courses and lessons

* post hog template UI is good to go

* classic and minimalistic template UI is good to go

* some refactoring

* some refactoring

* layout adjustments

* update tsconfig

* trying to get the filter to work well

* this is working now

* cleanup

* cleanup

* UI updates

* course page UI for posthog

* navbar and sidebar for course page in all templates complete

* all templates course page showing lesson content from md file with mdsvex
configured

* wokring on sections

* posthog lesson content done

* all templates fetches the lesson content from the section

* all templates properly typed

* scaffolding the new template

* finished the webflow-saas

* done with the webflow saas UI

* new calcom template

* new calcom template

* masonry layout for testitomonail section layout done

* calcom UI done

* UI all done

* cleanup

* trying to fix dark mode for the course page and home page

* done with this

* done with this

* working on tests

* migrated minimalistic template to svelte 5

* updated the syntax to svelte 5 for webflow

* migrated classic to svelte 5

* migrated posthog to svelte 5

* used svelte 5 syntax in calcom

* cli tool woks locally

* rename to course-sites

* use pnpm in root

* calcom refactoring

* remove unused files

* chore: biggest refactoring in my life

* add basic readme

* refactoring on course page

* restructure how courses are ordered

* proper state propagation

* add cf adapter

* fix pnpm i issue

* fix folder icon size

* use 18.17 version

* update npmrc

* add engine to packagejson

* pnpm install

* add engine to cio-com

* make it 18.x

* remove sequence

* move mdsvex to dependencies

* update lock

* mdsvex please work

* replace fs with import from vite

* add lock

* styling improvements

* fix styling

* sidebar style improvements

* fix: mobile concerns

* fix spacing of course header on desktop

* update header

* top nav fix

* feat: make cal template

* setup another template

* classic basic UI

* fixed the /courses fetching wrong page content

* fix breaking template cause of logo

* adding some updates

* improve styles theming

* finished updates from reviews

* updated the custom styling css

* clean up posthog template

* cli tool done, needs some cleaning though

* moved components to home

* improved the UI of the minimal template

* Add CLI tool and course app site

* prettier lint

* improve CLI

* ignore sveltekit folder

* publish course app

* fix tailwing theme + improve README

* finish up course-app site

* update README.md

---------

Co-authored-by: emmanuel <[email protected]>
This commit is contained in:
Rotimi Best
2024-12-14 03:40:47 +01:00
committed by GitHub
co-authored by emmanuel
parent 7e239da7d2
commit 26700dccd0
343 changed files with 17993 additions and 350 deletions
+22
View File
@@ -0,0 +1,22 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
dist
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+27
View File
@@ -0,0 +1,27 @@
# @classroomio/course-app
Create a university site for your SAAS in seconds, ready to go. Just add your content.
## Usage
To create a template run:
```bash
npx @cio/course-app@latest name-of-your-project -t name-of-template --course
```
### Commands
The CLI accepts the following arguments
- argument 1 : `name-of-project` this argument is the name of your project
- argument 2 : `--template name-of-template` this argument is the name of the template you wish to use
- argument 3 : `--courses` an option to use our demo courses
## Available Templates
- cal
- classic
- minimalistic
- posthog
- webflow
@@ -0,0 +1,84 @@
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import path from 'path';
import TEMPLATES from '../templates.json';
import { scaffoldTemplate } from '../lib/scaffoldTemplate';
const spinner = ora();
const templates = TEMPLATES.map((template) => template.name);
program.version('0.0.1').description('Courseapp CLI');
program
.argument('[projectName]', 'name of the project')
.option('-t, --template <template>', 'name of the template')
.option('-c, --courses', 'use demo courses')
.action(async (projectName, option) => {
let { template, courses } = option;
if (!projectName) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'Enter the project name',
default: '.',
when: !projectName
}
]);
projectName = answer.projectName;
}
const projectPath =
projectName === '.' ? process.cwd() : path.resolve(process.cwd(), projectName);
if (!template) {
const answer = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: 'Select a template',
choices: templates,
default: templates[0]
}
]);
template = answer.template;
}
if (courses === undefined) {
const answer = await inquirer.prompt([
{
type: 'confirm',
name: 'course',
message: 'Would you like to use our demo courses?',
default: true
}
]);
courses = answer.course;
}
spinner.start('Scaffolding project....');
try {
await scaffoldTemplate({ template, courses, projectPath, projectName });
spinner.succeed(`\n\n${chalk.green('Done! Your project is ready.')}\n\n`);
console.log(
`To get started:\n\n${chalk.blue(`cd ${projectName}`)}\n${chalk.blue(
'pnpm i'
)}\n${chalk.blue('pnpm run dev')}`
);
console.log('\n\n', chalk.blue('Happy hacking ;)'));
} catch (error) {
spinner.fail(chalk.red(`Failed to scaffold the project. ${error}`));
process.exit(1);
}
});
program.parse(process.argv);
@@ -0,0 +1,91 @@
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { copyCoursesFolder } from '../utils/copyCoursesFolder';
import { copyFolderSync } from '../utils/copyFolder';
// Define the interface for function parameters
interface ScaffoldTemplateParams {
template: string; // Template name
courses: boolean; // Whether to include demo courses
projectPath: string; // Path to the project
projectName: string; // Name of the project
}
// Using require.resolve() instead of import.meta.url for better compatibility
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRoot = path.join(__dirname, '..');
const spinner = ora();
export async function scaffoldTemplate({
template,
courses,
projectPath,
projectName
}: ScaffoldTemplateParams): Promise<void> {
spinner.text = chalk.yellow('Getting project...');
// Path to the entire template directory
const templateDirPath: string = path.resolve(packageRoot, 'src', 'template');
const coursePathToExclude: string = path.join(templateDirPath, 'src', 'courses');
// Check if the template directory exists
if (!fs.existsSync(templateDirPath)) {
throw new Error(`Template directory does not exist at "${templateDirPath}".`);
}
// Path to the specific template within the components directory
const specificTemplatePath: string = path.resolve(
templateDirPath,
'src',
'lib',
'components',
template
);
// Check if the specific template exists
if (!fs.existsSync(specificTemplatePath)) {
throw new Error(`Template "${template}" does not exist at "${specificTemplatePath}".`);
}
// Get all templates in the components directory
const allTemplates = fs
.readdirSync(path.join(templateDirPath, 'src', 'lib', 'components'))
.filter((file) =>
fs.statSync(path.join(templateDirPath, 'src', 'lib', 'components', file)).isDirectory()
);
// Exclude all templates except the selected one and the ui folder
const excludeTemplates = allTemplates.filter((temp) => temp !== template && temp !== 'ui');
// Copy the selected template files to the project path, excluding others
await copyFolderSync({
from: templateDirPath,
to: projectPath,
excludeNames: ['node_modules', '.svelte-kit', ...excludeTemplates], // Exclude node_modules if present
excludePath: coursePathToExclude
});
// If the user wants demo courses, copy the courses folder
spinner.text = chalk.yellow('Setting up courses folder...');
await copyCoursesFolder({ templatePath: templateDirPath, projectPath, courses });
console.log(
chalk.yellow(
`Project ${projectName} created using template "${template}" ${
courses ? 'with' : 'without'
} demo courses.`
)
);
console.log(
chalk.yellow(
`You can ${
courses
? 'edit or create new courses in the courses folder'
: 'create new courses in the courses folder'
}`
)
);
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@classroomio/course-app",
"version": "0.0.1",
"private": false,
"main": "dist/create-template.js",
"scripts": {
"create": "node dist/create-template.js",
"build": "vite build && node scripts/postbuild.js"
},
"bin": {
"@classroomio/course-app": "./dist/create-template.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/classroomio/classroomio.git"
},
"bugs": {
"url": "https://github.com/classroomio/classroomio/issues"
},
"homepage": "https://github.com/classroomio/classroomio/blob/main/packages/course-app#readme",
"author": "ClassroomIO <[email protected]>",
"license": "MIT",
"files": [
"dist",
"src"
],
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/fs-extra": "^11.0.4",
"svelte": "^4.2.7",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.2.0",
"inquirer": "^11.1.0",
"ora": "^8.1.1"
}
}
+15
View File
@@ -0,0 +1,15 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const cliPath = path.resolve(__dirname, '../dist/create-template.js');
fs.readFile(cliPath, 'utf8', (err, data) => {
if (err) throw err;
const newContent = `${data}`;
fs.writeFile(cliPath, newContent, (err) => {
if (err) throw err;
fs.chmodSync(cliPath, '755');
});
});
@@ -0,0 +1,25 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
@@ -0,0 +1,48 @@
# course-app
`course-app` allows you create beautiful looking university sites in minutes for your SAAS. Companies like Webflow, Jira, Framer, Hubspot, Google all have university sites that helps them educate their customers and create loyal communities.
## Features
- Pages can be managed from json files
- Courses can be managed in markdown
- Sections and Lessons broken into folders and automatically rendered
## Folder Structure
```bash
/src
/courses
/course-1-slug
/section-slug
lesson-1-slug.md
lesson-2-slug.md
...
metadata.json
metadata.json
metadata.json
...
/lib
/components
/ui
Navigation.svelte
...
/data
pages.json
...
/utils
/constants
page.ts
...
/helpers
page.ts
...
/stores
page.ts
...
/types
page.ts
...
/routes
...
```
@@ -0,0 +1,17 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}
@@ -0,0 +1,47 @@
{
"name": "calcom",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.4",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.9.0",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"jsdom": "^25.0.1",
"mdsvex": "^0.11.2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^3.4.9",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.5.0",
"vite": "^5.4.4",
"vitest": "^2.1.4"
},
"dependencies": {
"@sveltejs/adapter-cloudflare": "^4.7.4",
"carbon-icons-svelte": "^12.13.0",
"rehype-slug": "^6.0.0",
"rehype-unwrap-images": "^1.0.0",
"remark-toc": "^9.0.0",
"shiki": "^1.23.1",
"svelte-meta-tags": "^4.0.4"
}
}
@@ -0,0 +1,108 @@
// import { defineConfig, devices } from '@playwright/test';
// /**
// * Read environment variables from file.
// * https://github.com/motdotla/dotenv
// */
// // import dotenv from 'dotenv';
// // import path from 'path';
// // dotenv.config({ path: path.resolve(__dirname, '.env') });
// /**
// * See https://playwright.dev/docs/test-configuration.
// */
// export default defineConfig({
// testDir: './tests',
// testMatch: ['**/*.spec.ts'],
// /* Run tests in files in parallel */
// fullyParallel: true,
// /* Fail the build on CI if you accidentally left test.only in the source code. */
// forbidOnly: !!process.env.CI,
// /* Retry on CI only */
// retries: process.env.CI ? 2 : 0,
// /* Opt out of parallel tests on CI. */
// workers: process.env.CI ? 1 : undefined,
// /* Reporter to use. See https://playwright.dev/docs/test-reporters */
// reporter: 'html',
// /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
// use: {
// /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:5173',
// /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
// trace: 'on-first-retry'
// },
// /* Configure projects for major browsers */
// projects: [
// {
// name: 'chromium',
// use: { ...devices['Desktop Chrome'] }
// },
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] }
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] }
// }
// /* Test against mobile viewports. */
// // {
// // name: 'Mobile Chrome',
// // use: { ...devices['Pixel 5'] },
// // },
// // {
// // name: 'Mobile Safari',
// // use: { ...devices['iPhone 12'] },
// // },
// /* Test against branded browsers. */
// // {
// // name: 'Microsoft Edge',
// // use: { ...devices['Desktop Edge'], channel: 'msedge' },
// // },
// // {
// // name: 'Google Chrome',
// // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// // },
// ],
// /* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run dev',
// url: 'http://127.0.0.1:5173',
// reuseExistingServer: !process.env.CI,
// timeout: 12000000, // Add longer timeout
// stdout: 'pipe', // Add this to see server output
// stderr: 'pipe' // Add this to see server errors
// }
// });
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
testMatch: ['**/*.spec.ts'],
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
// Remove baseURL from here as we'll detect it dynamically
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
// Remove webServer config since we're running the server manually
});
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
@@ -0,0 +1,144 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--posthog-primary: 36 83% 54%;
--posthog-secondary: 19 100% 48%;
--posthog-ring: 37 78% 39%;
--posthog-background: 70 16% 93%;
--posthog-border: 68 8% 80%;
--minimal-primary: 214 33% 9%;
--minimal-secondary: 212 34% 15% 1;
--classic-primary: 300 98% 41%;
--classic-secondary: 300 100% 95%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.prose :is(h1, h2, h3, h4, h5, h6) {
font-weight: 700;
margin-top: 20px;
}
.prose :is(h1) {
font-size: 2em;
}
.prose :is(h2) {
font-size: 1.5em;
}
.prose :is(h3) {
font-size: 1.17em;
}
.prose :is(h4) {
font-size: 1em;
}
.prose :is(h5) {
font-size: 0.83em;
}
.prose :is(h6) {
font-size: 0.67em;
}
.prose :is(p, li) {
margin-top: 0.7em;
margin-bottom: 0em;
color: #374151;
}
.prose :is(ul, ol) {
list-style-type: disc;
padding-left: 3rem;
}
.prose pre {
max-inline-size: 100%;
padding: 1rem;
border-radius: 8px;
tab-size: 2;
}
.prose blockquote {
border-left: 4px solid gray;
font-style: italic;
font-weight: 500;
padding-left: 16px;
margin: 20px 0;
}
.prose figure + em {
display: block;
font-size: 14px;
margin-bottom: 40px;
border: none;
padding: none;
text-align: center;
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
@@ -0,0 +1,44 @@
---
title: 'Why Analytics Matters'
---
<script>
let count = $state(0);
const increment = () => count++;
</script>
## Code
```ts
let count = $state(0);
const increment = () => count++;
```
## Images
![classroomio-brand-customisations](https://assets.cdn.clsrio.com/blog/brand-customisations.png)
_ClassroomIO Brand Customisations_
Time is our most valuable and non-renewable resource. In today's fast-paced world, effective time management isn't just a nice-to-have skill—it's essential for success in both professional and personal life.
### What You'll Learn
In this comprehensive course, you'll discover:
- Core principles of effective time management
- Practical strategies for prioritization
- Digital tools and techniques for better scheduling
- How to use Cal.com to streamline your meeting scheduling
### Course Structure
This course is divided into focused sections that build upon each other:
1. Time Management Fundamentals
2. Prioritization Techniques
3. Digital Tools & Automation
4. Advanced Scheduling with Cal.com
5. Maintaining Work-Life Balance
Let's begin your journey to mastering time management!
@@ -0,0 +1,38 @@
---
title: 'Core Concepts of Analytics'
---
### The Four Quadrants of Time Management
Based on Stephen Covey's framework, activities can be categorized into:
1. **Urgent & Important** (Crises, pressing problems)
2. **Not Urgent but Important** (Planning, prevention, relationship building)
3. **Urgent but Not Important** (Interruptions, some calls)
4. **Not Urgent & Not Important** (Time wasters, pleasant activities)
### The 80/20 Rule (Pareto Principle)
The principle states that 80% of results come from 20% of efforts. In time management:
- Focus on high-impact activities
- Identify your most productive hours
- Eliminate or delegate low-value tasks
### Time Blocking
Time blocking is a method where you schedule specific blocks of time for different activities:
- Deep work sessions
- Meeting blocks
- Administrative tasks
- Breaks and buffer time
### Common Time Management Mistakes
- Multitasking
- Poor planning
- Saying yes to everything
- Not taking breaks
- Perfectionism
-
@@ -0,0 +1,4 @@
{
"title": "Fundamentals of Analytics",
"unlocked": true
}
@@ -0,0 +1,29 @@
---
title: 'Effective Prioritization Methods'
---
### The Eisenhower Matrix
A powerful tool for deciding on and prioritizing tasks by urgency and importance:
- **Do First**: Urgent and important
- **Schedule**: Important but not urgent
- **Delegate**: Urgent but not important
- **Don't Do**: Neither urgent nor important
### The ABCDE Method
Prioritize tasks by assigning letters:
- A: Must do - very important
- B: Should do - important
- C: Nice to do - not very important
- D: Delegate
- E: Eliminate
### Practical Implementation
1. Create a master task list
2. Apply prioritization method
3. Schedule tasks accordingly
4. Review and adjust regularly
@@ -0,0 +1,4 @@
{
"title": "Prioritization Techniques",
"unlocked": true
}
@@ -0,0 +1,39 @@
---
title: 'Modern Scheduling Tools'
---
### Why Use Digital Scheduling?
Digital scheduling tools offer numerous advantages:
- Eliminate email back-and-forth
- Prevent double-booking
- Automatic timezone conversion
- Calendar integration
- Professional appearance
### Introduction to Cal.com
Cal.com is a modern scheduling infrastructure that helps you:
- Create customizable booking pages
- Set availability rules
- Integrate with existing calendars
- Automate meeting workflows
### Key Features of Cal.com
1. **Event Types**
- One-on-one meetings
- Group sessions
- Recurring appointments
2. **Smart Scheduling**
- Buffer times
- Minimum notice period
- Custom availability
3. **Integration Capabilities**
- Calendar systems
- Video conferencing
- CRM systems
@@ -0,0 +1,4 @@
{
"title": "Digital Tools & Automation",
"unlocked": true
}
@@ -0,0 +1,37 @@
---
title: 'Mastering PostHog for Efficient Analytics'
---
### Setting Up Your PostHog Profile
1. **Profile Optimization**
- Professional photo
- Clear description
- Customized booking link
2. **Event Types Configuration**
- Meeting durations
- Booking questions
- Confirmation messages
### Advanced Features
#### Workflows
Automate your meeting-related tasks:
- Email reminders
- Follow-up messages
- Calendar blocks
#### Team Scheduling
- Round-robin assignments
- Collective availability
- Shared booking pages
### Best Practices
1. Use buffer times between meetings
2. Set appropriate minimum notice periods
3. Customize your availability for different time zones
4. Integrate with your preferred tools
@@ -0,0 +1,36 @@
---
title: 'Maintaining Work-Life Balance'
---
### Setting Boundaries
1. **Time Boundaries**
- Define working hours
- Create meeting-free days
- Set aside personal time
2. **Digital Boundaries**
- Use Cal.com's availability settings
- Implement 'Do Not Disturb' periods
- Create booking rules
### Sustainable Practices
#### Daily Habits
- Morning routine
- Regular breaks
- End-of-day ritual
#### Weekly Planning
- Review and adjust schedules
- Block personal time
- Schedule buffer days
### Long-term Success
1. Regular schedule audits
2. Adjustment of boundaries as needed
3. Continuous improvement of systems
4. Regular reflection and assessment
@@ -0,0 +1,4 @@
{
"title": "Advanced Cal.com Usage",
"unlocked": true
}
@@ -0,0 +1,9 @@
{
"title": "Analytics 101: Mastering Your Data",
"description": "Learn essential analytics skills and leverage modern analytics tools to maximize your productivity and achieve better work-life balance.",
"banner": "https://images.unsplash.com/photo-1501139083538-0139583c060f?q=80&w=640&auto=format&fit=crop",
"created_at": "24-11-2024",
"cost": 0,
"currency": "USD",
"type": "paced"
}
@@ -0,0 +1,26 @@
---
title: 'Why Time Management Matters'
---
Time is our most valuable and non-renewable resource. In today's fast-paced world, effective time management isn't just a nice-to-have skill—it's essential for success in both professional and personal life.
### What You'll Learn
In this comprehensive course, you'll discover:
- Core principles of effective time management
- Practical strategies for prioritization
- Digital tools and techniques for better scheduling
- How to use Cal.com to streamline your meeting scheduling
### Course Structure
This course is divided into focused sections that build upon each other:
1. Time Management Fundamentals
2. Prioritization Techniques
3. Digital Tools & Automation
4. Advanced Scheduling with Cal.com
5. Maintaining Work-Life Balance
Let's begin your journey to mastering time management!
@@ -0,0 +1,38 @@
---
title: 'Core Concepts of Time Management'
---
### The Four Quadrants of Time Management
Based on Stephen Covey's framework, activities can be categorized into:
1. **Urgent & Important** (Crises, pressing problems)
2. **Not Urgent but Important** (Planning, prevention, relationship building)
3. **Urgent but Not Important** (Interruptions, some calls)
4. **Not Urgent & Not Important** (Time wasters, pleasant activities)
### The 80/20 Rule (Pareto Principle)
The principle states that 80% of results come from 20% of efforts. In time management:
- Focus on high-impact activities
- Identify your most productive hours
- Eliminate or delegate low-value tasks
### Time Blocking
Time blocking is a method where you schedule specific blocks of time for different activities:
- Deep work sessions
- Meeting blocks
- Administrative tasks
- Breaks and buffer time
### Common Time Management Mistakes
- Multitasking
- Poor planning
- Saying yes to everything
- Not taking breaks
- Perfectionism
-
@@ -0,0 +1,4 @@
{
"title": "Understanding Time Management Fundamentals",
"unlocked": true
}
@@ -0,0 +1,29 @@
---
title: 'Effective Prioritization Methods'
---
### The Eisenhower Matrix
A powerful tool for deciding on and prioritizing tasks by urgency and importance:
- **Do First**: Urgent and important
- **Schedule**: Important but not urgent
- **Delegate**: Urgent but not important
- **Don't Do**: Neither urgent nor important
### The ABCDE Method
Prioritize tasks by assigning letters:
- A: Must do - very important
- B: Should do - important
- C: Nice to do - not very important
- D: Delegate
- E: Eliminate
### Practical Implementation
1. Create a master task list
2. Apply prioritization method
3. Schedule tasks accordingly
4. Review and adjust regularly
@@ -0,0 +1,4 @@
{
"title": "Prioritization Techniques",
"unlocked": true
}
@@ -0,0 +1,39 @@
---
title: 'Modern Scheduling Tools'
---
### Why Use Digital Scheduling?
Digital scheduling tools offer numerous advantages:
- Eliminate email back-and-forth
- Prevent double-booking
- Automatic timezone conversion
- Calendar integration
- Professional appearance
### Introduction to Cal.com
Cal.com is a modern scheduling infrastructure that helps you:
- Create customizable booking pages
- Set availability rules
- Integrate with existing calendars
- Automate meeting workflows
### Key Features of Cal.com
1. **Event Types**
- One-on-one meetings
- Group sessions
- Recurring appointments
2. **Smart Scheduling**
- Buffer times
- Minimum notice period
- Custom availability
3. **Integration Capabilities**
- Calendar systems
- Video conferencing
- CRM systems
@@ -0,0 +1,4 @@
{
"title": "Digital Tools & Automation",
"unlocked": true
}
@@ -0,0 +1,37 @@
---
title: 'Mastering Cal.com for Efficient Scheduling'
---
### Setting Up Your Cal.com Profile
1. **Profile Optimization**
- Professional photo
- Clear description
- Customized booking link
2. **Event Types Configuration**
- Meeting durations
- Booking questions
- Confirmation messages
### Advanced Features
#### Workflows
Automate your meeting-related tasks:
- Email reminders
- Follow-up messages
- Calendar blocks
#### Team Scheduling
- Round-robin assignments
- Collective availability
- Shared booking pages
### Best Practices
1. Use buffer times between meetings
2. Set appropriate minimum notice periods
3. Customize your availability for different time zones
4. Integrate with your preferred tools
@@ -0,0 +1,36 @@
---
title: 'Maintaining Work-Life Balance'
---
### Setting Boundaries
1. **Time Boundaries**
- Define working hours
- Create meeting-free days
- Set aside personal time
2. **Digital Boundaries**
- Use Cal.com's availability settings
- Implement 'Do Not Disturb' periods
- Create booking rules
### Sustainable Practices
#### Daily Habits
- Morning routine
- Regular breaks
- End-of-day ritual
#### Weekly Planning
- Review and adjust schedules
- Block personal time
- Schedule buffer days
### Long-term Success
1. Regular schedule audits
2. Adjustment of boundaries as needed
3. Continuous improvement of systems
4. Regular reflection and assessment
@@ -0,0 +1,4 @@
{
"title": "Advanced Cal.com Usage",
"unlocked": true
}
@@ -0,0 +1,9 @@
{
"title": "Tracking and Testing 101: Mastering Your Data",
"description": "Learn essential tracking and testing skills and leverage modern analytics tools to maximize your productivity and achieve better work-life balance.",
"banner": "https://images.unsplash.com/photo-1501139083538-0139583c060f?q=80&w=640&auto=format&fit=crop",
"created_at": "24-11-2024",
"cost": 0,
"currency": "USD",
"type": "paced"
}
@@ -0,0 +1,35 @@
<script lang="ts">
import { CheckmarkFilled } from 'carbon-icons-svelte';
import { getPageSection } from '$lib/utils/helpers/page';
import { homePage } from '$lib/utils/stores/pages';
const content = $derived(getPageSection($homePage, 'about'));
</script>
{#if content?.show}
<section id="about">
<div class="relative px-4 md:px-10 py-10 h-full w-full mx-auto xl:w-[80%]">
<h1 class="text-center text-5xl mb-2 font-bold md:w-[60%] mx-auto">
{content.settings.title}
</h1>
<p class="text-center dark:text-white text-lg font-semibold md:w-[70%] mb-8 mx-auto">
{content.settings.subtitle}
</p>
<div>
<section class="flex flex-wrap items-start justify-center w-full gap-6 py-4">
{#each content.settings.benefits.list as item}
<div
class="flex flex-col gap-2 p-4 bg-white border border-[#282828] h-[130px] max-h-[180px] w-full max-w-full md:max-w-[320px] lg:max-w-[450px] rounded-lg overflow-hidden"
>
<span class="flex items-center gap-2">
<CheckmarkFilled size={24} />
<p class="text-lg capitalize line-clamp-1">{item.title}</p>
</span>
<p class="text-sm text-[#878787] line-clamp-3">{item.subtitle}</p>
</div>
{/each}
</section>
</div>
</div>
</section>
{/if}
@@ -0,0 +1,70 @@
<script lang="ts">
import { Add, Subtract } from 'carbon-icons-svelte';
interface Props {
title?: string;
content?: string;
}
let { title = '', content = '' }: Props = $props();
let isOpen = $state(false);
let contentHeight = $state('0px'); // Track the height of the content
let contentRef: HTMLDivElement | undefined = $state(); // Reference to the content div
function toggleAccordion() {
isOpen = !isOpen;
if (isOpen && contentRef) {
// Measure and set the height when opening
contentHeight = `${contentRef.scrollHeight}px`;
} else {
// Set to 0 height when closing
contentHeight = '0px';
}
}
</script>
<div
class="mx-6 shadow-[0px_3px_#282828] transition bg-white text-black overflow-hidden rounded-3xl"
>
<button
class="relative flex justify-between items-center w-full cursor-pointer overflow-hidden p-6 bg-inherit"
onclick={toggleAccordion}
>
<p class="font-semibold text-lg">{title}</p>
<div class="flex items-center justify-center p-[2px] bg-white text-black">
{#if isOpen}
<Subtract class="text-xl transition-transform duration-200 ease-in" />
{:else}
<Add class="text-xl transition-transform duration-200 ease-in" />
{/if}
</div>
{#if isOpen}
<div class="flex items-center justify-center absolute left-[2%] bottom-0 w-[96%] mx-auto">
<svg class="w-full h-2" viewBox="0 0 360 10" preserveAspectRatio="none">
<line
x1="0"
y1="5"
x2="360"
y2="5"
stroke="#656565"
stroke-width="2"
stroke-dasharray="2, 5"
/>
</svg>
</div>
{/if}
</button>
<div
bind:this={contentRef}
style="max-height: {contentHeight}; transition: max-height 0.3s ease-in-out; overflow: hidden;"
class="rounded-3xl"
>
<div class=" p-4 bg-inherit rounded-b rounded-3xl">
<p>
{content}
</p>
</div>
</div>
</div>
@@ -0,0 +1,47 @@
<script lang="ts">
import PrimaryButton from './PrimaryButton.svelte';
interface Props {
className?: string;
bannerImage: string | undefined;
slug: string;
title: string;
description: string;
}
let {
className = '',
bannerImage = '',
slug = '',
title = '',
description = ''
}: Props = $props();
</script>
<div
class="bg-white border-2 border-black rounded-xl overflow-hidden w-full h-full pb-2 min-w-[80%] md:max-w-[350px] md:min-w-[300px] max-w-full space-y-4"
>
<div class="relative space-y-2 text-black {className}">
<div class="border-b-2 border-black border-dashed">
<img
src={bannerImage ? bannerImage : '/classroomio-course-img-template.jpg'}
alt=""
class="w-full h-44"
/>
</div>
<div>
<div class="px-3 py-2 overflow-hidden">
<p class="text-lg font-semibold overflow-ellipsis line-clamp-1">
{title}
</p>
</div>
<p class="text-sm text-[#656565] px-3 overflow-ellipsis line-clamp-3 text-justify">
{description}
</p>
</div>
<div class=" w-full py-4 px-6">
<PrimaryButton href={`/course/${slug}`} label="Go to Course" />
</div>
</div>
</div>
@@ -0,0 +1,38 @@
<script lang="ts">
interface Props {
type?: string;
className?: string;
headerClassName?: string;
subtitleClassName?: string;
}
let {
type = 'course',
className = '',
headerClassName = '',
subtitleClassName = ''
}: Props = $props();
</script>
<section
class="{className} w-full border rounded-md py-4 flex items-center justify-center p-2 my-2"
>
<div
class="flex flex-col md:flex-row justify-center items-center text-center md:text-start gap-6 p-2"
>
<img src="/expert-empty.svg" alt="empty" />
<div>
{#if type == 'course'}
<h3 class="font-semibold {headerClassName}">"No Course yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like courses have not been added yet.<br /> Kindly check back later
</p>
{:else}
<h3 class="font-semibold {headerClassName}">"No Pathways yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like pathways have not been added yet.<br /> Kindly check back later
</p>
{/if}
</div>
</div>
</section>
@@ -0,0 +1,27 @@
<script lang="ts">
import { getPageSection } from '$lib/utils/helpers/page';
import { homePage } from '$lib/utils/stores/pages';
import Accordion from './Accordion.svelte';
const content = $derived(getPageSection($homePage, 'faq'));
</script>
{#if content?.show}
<section id="faq" class="relative px-2 pt-4 pb-20 h-full">
<div class="absolute inset-0 bg-[#E5E7E0] opacity-60"></div>
<div class="relative">
<h1 class="text-center text-5xl font-bold mb-4">
{content.settings.title}
</h1>
<h1 class="text-center text-lg font-semibold mb-4 w-full md:w-[80%] lg:w-[60%] mx-auto">
{content.settings.subtitle}
</h1>
<section class="py-4 px-2 space-y-10 w-full md:w-[80%] mx-auto">
{#each content.settings.questions as faq}
<Accordion title={faq.title} content={faq.content} />
{/each}
</section>
</div>
</section>
{/if}
@@ -0,0 +1,47 @@
<script lang="ts">
import { getPageSection } from '$lib/utils/helpers/page';
import { sharedPage } from '$lib/utils/stores/pages';
const content = $derived(getPageSection($sharedPage, 'footer'));
const seo = $derived(getPageSection($sharedPage, 'seo'));
</script>
{#if content?.show}
<nav class="w-full py-4 px-4 lg:px-14 bg-[#F4F4F4]">
<div
class="bg-[#2E2E2F] rounded-lg p-4 text-white w-full flex flex-col md:flex-row items-start md:items-center gap-4 md:justify-between"
>
<a href="/#" class="p-2 bg-white rounded-lg">
<img
src={seo?.settings.logo}
alt={`${seo?.settings.title} logo`}
class="rounded bg-white inline-block mx-auto"
data-atf="1"
/>
</a>
<ul
class="flex flex-col md:flex-row items-start md:items-center gap-4 -md:gap-8 font-semibold capitalize underline"
>
{#if content.settings.twitter}
<a href={content.settings.twitter} target="_blank" title="twitter">Twitter</a>
{/if}
{#if content.settings.youtube}
<a href={content.settings.youtube} target="_blank">Youtube</a>{/if}
{#if content.settings.linkedin}
<a href={content.settings.linkedin} target="_blank" title="linkedin">LinkedIn</a>
{/if}
{#if content.settings.facebook}
<a href={content.settings.facebook} target="_blank" title="facebook">facebook</a>
{/if}
</ul>
<a
href="https://git.new/cio-template"
target="_blank noopener noreferrer"
class="flex w-full md:w-fit items-center justify-center lg:justify-start hover:underline text-white gap-1"
>
<p class="text-base font-bold">Built with ❤️ by ClassroomIO</p>
</a>
</div>
</nav>
{/if}
@@ -0,0 +1,70 @@
<script lang="ts">
import { goto } from '$app/navigation';
import PrimaryButton from './PrimaryButton.svelte';
import { getPageSection } from '$lib/utils/helpers/page';
import { homePage } from '$lib/utils/stores/pages';
</script>
{#if $homePage}
{@const content = getPageSection($homePage, 'header')}
{#if content?.show}
<section
class="font-matter flex items-start md:items-center justify-center py-10 lg:py-20 px-5 lg:px-20 h-full bg-[#F4F4F4]"
>
<section
class="flex flex-col-reverse lg:flex-row items-center justify-center lg:justify-between"
>
<div class="text-center lg:text-start space-y-6 w-full mt-4 lg:mt-0">
<div
class="bg-[#E5E7E0] py-1 px-4 md:border-b border-black rounded-sm w-fit mx-auto lg:mx-0"
>
<p class="uppercase font-bold text-xs lg:text-base text-[#0F163F]">
{content.settings.title}
</p>
</div>
<p
class="text-4xl md:text-6xl font-black w-full mx-auto lg:mx-0 md:w-[90%] lg:w-[80%] capitalize"
>
{content.settings.titleHighlight}
</p>
<p class="font-semibold text-xl w-full md:w-[80%] mx-auto lg:mx-0 lg:w-[70%]">
{content.settings.subtitle}
</p>
<PrimaryButton
onClick={() => {
goto(content.settings.action.link);
}}
label={content.settings.action.label}
/>
</div>
<div
class="relative w-[250px] h-[250px] lg:w-[500px] lg:h-[350px] flex items-center justify-center"
>
<svg
class="absolute -left-[15px] lg:-left-[60px] xl:-left-[100px] w-[270px] h-[270px] lg:w-[400px] xl:w-[520px] lg:h-[370px]"
viewBox="0 0 360 360"
>
<circle
cx="180"
cy="180"
r="170"
fill="none"
stroke="#282828"
stroke-width="2"
stroke-dasharray="15, 10"
/>
</svg>
<img
alt="landing page banner"
src={content.settings.banner?.image
? content.settings.banner?.image
: '/calcom-banner-img.jpeg'}
class="object-cover rounded-full w-[250px] h-[250px] lg:w-[500px] lg:h-[350px]"
/>
</div>
</section>
</section>
{/if}
{/if}
@@ -0,0 +1,97 @@
<script lang="ts">
import { ArrowUpRight, Close } from 'carbon-icons-svelte';
import Menu from 'carbon-icons-svelte/lib/Menu.svelte';
import { sharedPage } from '$lib/utils/stores/pages';
import { getPageSection } from '$lib/utils/helpers/page';
import { Button } from '$lib/components/ui/button';
import type { Section } from '$lib/utils/types/page';
let open = $state(false);
const content = $derived(getPageSection($sharedPage, 'navigation'));
const seo: Section | undefined = $derived(getPageSection($sharedPage, 'seo'));
function toggleMenu() {
open = !open;
}
</script>
<nav
class="relative md:sticky top-0 z-[3000] w-full flex items-center justify-between bg-[#F4F4F4] border-b border-[#D0D1C9] py-4 px-6"
>
<!-- Logo Section -->
<a href="/" class="flex items-center flex-col" title={`${seo?.settings?.title}`} id="logo">
{#if seo?.settings?.logo}
<img
src={seo?.settings?.logo || ''}
alt={`${seo?.settings?.title || ''} logo`}
class="rounded inline-block mx-auto"
/>
{/if}
<h1 class="text-xl font-semibold">University</h1>
</a>
<ul
class="hidden lg:flex items-center gap-14 text-base font-semibold text-[#1F2937] list-none hover:no-underline"
>
{#each content?.settings.navItems as navItem}
<li>
<a
href={navItem.link}
target={navItem.redirect ? '_blank' : undefined}
class="flex items-center gap-1 hover:underline"
>
{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={16} />
{/if}
</a>
</li>
{/each}
</ul>
<Button
href="/courses"
class="bg-[#141414] rounded-full hidden lg:flex items-center gap-1 font-semibold text-white text-base"
>
Explore Courses
</Button>
<!-- Mobile Menu Button (Visible only on mobile and when logged in) -->
<button onclick={toggleMenu} class="lg:hidden">
<Menu size={24} />
</button>
<!-- Mobile Sidebar Menu (Visible only on mobile) -->
<ul
class={`fixed top-0 left-0 pt-10 h-full bg-white w-full transform hover:no-underline ${
open ? 'translate-y-0 ' : '-translate-y-full'
} transition-transform duration-300 ease-in-out lg:hidden text-base font-semibold text-[#1F2937] list-none cursor-pointer`}
>
<button onclick={toggleMenu} class="absolute right-4 top-4 lg:hidden z-20">
<Close size={24} />
</button>
<div class="divide-y divide-solid border-b">
{#each content?.settings.navItems as navItem}
<li class="py-4 px-6">
<a href={navItem.link} onclick={toggleMenu}>{navItem.title}</a>
</li>
{/each}
</div>
<Button
href="/courses"
class="flex items-center mx-auto justify-center gap-1 py-4 px-6 bg-[#141414] w-fit my-4 font-semibold rounded-full text-white text-base"
on:click={toggleMenu}
>
Explore Courses
</Button>
</ul>
</nav>
<style>
ul {
z-index: 10;
}
</style>
@@ -0,0 +1,15 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
export let onClick = () => {};
export let label: string;
export let href: string | undefined = undefined;
</script>
<Button
{href}
class="uppercase rounded-2xl py-6 bg-white hover:bg-white hover:text-black border-[1.5px] border-[#141414] text-[#141414] shadow-[0px_3px_#141414]"
onclick={onClick}
>
{label}
</Button>
@@ -0,0 +1,15 @@
import type { Component } from 'svelte';
import CoursesPage from './pages/Courses.svelte';
import HomePage from './pages/Home.svelte';
import Navigation from './Navigation.svelte';
import Footer from './Footer.svelte';
import Button from './PrimaryButton.svelte';
import type { TemplateComponents } from '$lib/utils/types/template';
export const components: TemplateComponents = {
navigation: Navigation as unknown as Component,
footer: Footer as unknown as Component,
button: Button as unknown as Component,
coursespage: CoursesPage as unknown as Component,
homepage: HomePage as unknown as Component
};
@@ -0,0 +1,158 @@
<script lang="ts">
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import { Button } from '$lib/components/ui/button';
import { getPageSection } from '$lib/utils/helpers/page';
import { COURSE_TYPE } from '$lib/utils/constants/course';
import type { CourseFilterItem, Course } from '$lib/utils/types/course';
import type { Page } from '$lib/utils/types/page';
interface Props {
data: {
page: Page;
sharedPage: Page;
courses: Course[];
};
}
let { data }: Props = $props();
/**
* State
*/
let viewAll = $state(false);
let filter: CourseFilterItem[] = $state([
{
title: 'Self Paced',
type: COURSE_TYPE.PACED,
checked: false
},
{
title: 'Live Sessions',
type: COURSE_TYPE.LIVE,
checked: false
}
]);
let filteredCourses = $state([...data.courses]);
/**
* Constants
*/
const section = $derived({
header: getPageSection(data.page, 'header'),
courses: getPageSection(data.page, 'courses')
});
/**
* Functions
*/
function applyFilter() {
const activeFilters = new Set(
filter
.filter((filterItem) => filterItem.checked)
.map((filterItem) => filterItem.type.toLowerCase())
);
filteredCourses =
activeFilters.size === 0
? data.courses
: data.courses.filter((course) => activeFilters.has(course.type.toLowerCase()));
}
</script>
<!-- Header -->
<div class="w-full">
{#if section.header?.show}
<section
style="background-image: url('calcom-background.svg');"
class="bg-cover bg-center flex items-center justify-center py-10 px-2 lg:px-14 min-h-[30vh]"
>
<section class="flex flex-col text-center items-center gap-5 justify-center">
<div class="space-y-6 w-full mb-4">
<div class="bg-[#E5E7E0] py-1 px-4 md:border-b border-black rounded-sm w-fit mx-auto">
<p class="uppercase font-bold text-xs lg:text-base text-[#0F163F]">
{section.header.settings.title}
</p>
</div>
<p class="w-full text-5xl font-bold md:w-[70%] mx-auto">
{section.header.settings.titleHighlight}
</p>
<p class="w-full font-semibold text-lg md:w-[70%] mx-auto">
{section.header.settings.subtitle}
</p>
</div>
</section>
</section>
{/if}
{#if section.courses?.show}
<section id="course" class="px-2 pt-4 pb-20 h-full lg:px-16">
<div class="relative w-full flex flex-row items-start justify-start lg:gap-2 mt-10">
<!-- filter start -->
<div class="hidden lg:block sticky top-0 min-w-max">
<p class="font-semibold mb-4">Filter by</p>
<div class="w-full space-y-2">
{#each filter as item}
<form
class="space-x-2 text-[#3C4043] font-medium border border-[#D0D1C9] bg-[#F4F4F4] rounded-md pl-4 pr-8 py-4"
>
<input
type="checkbox"
bind:checked={item.checked}
name={item.title}
onchange={applyFilter}
class="focus:ring-0"
/>
<label for={item.title} class="whitespace-nowrap">{item.title}</label>
</form>
{/each}
</div>
</div>
<!-- filter end -->
<div class="text-white rounded-3xl w-full h-full mx-auto">
<!-- courses start -->
<div class="w-full max-h-screen p-4 md:p-8">
{#if data.courses.length > 0}
<section class="flex flex-wrap items-center justify-center gap-4 w-full">
{#each filteredCourses.slice(0, viewAll ? filteredCourses.length : 3) as course}
<CourseCard
slug={course.slug}
title={course.title}
bannerImage={course.banner}
description={course.description}
/>
{/each}
</section>
{#if filteredCourses.length == 0}
<div class="px-4 w-full mx-auto">
<EmptyState />
</div>
{/if}
{:else}
<div class="px-4 w-full mx-auto">
<EmptyState />
</div>
{/if}
</div>
<!-- courses end -->
{#if filteredCourses.length > 2}
<div class="w-full flex items-center justify-center py-8">
<Button
on:click={() => (viewAll = !viewAll)}
class="uppercase rounded-2xl py-6 hover:bg-white hover:text-black border-[1.5px] border-white text-white shadow-[0px_3px_white]"
>
{viewAll === true ? 'Show less' : 'View more courses'}
</Button>
</div>
{/if}
</div>
</div>
</section>
{/if}
<!-- course end -->
</div>
@@ -0,0 +1,306 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getPageSection } from '@/utils/helpers/page';
import { homePage } from '@/utils/stores/pages';
import PrimaryButton from '../PrimaryButton.svelte';
import Button from '@/components/ui/button/button.svelte';
import ArrowRight from 'carbon-icons-svelte/lib/ArrowRight.svelte';
import ArrowLeft from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import { courses } from '$lib/utils/stores/course';
import { CheckmarkFilled } from 'carbon-icons-svelte';
import Accordion from '../Accordion.svelte';
/**
* State
*/
// let coursesList = [1, 2, 3, 4, 5, 6, 7];
let scrollContainer: HTMLElement | undefined = $state();
/**
* Constants
*/
const coursesSection = $derived(getPageSection($homePage, 'courses'));
const aboutSection = $derived(getPageSection($homePage, 'about'));
const faqSection = $derived(getPageSection($homePage, 'faq'));
const testimonialSection = $derived(getPageSection($homePage, 'testimonial'));
const footerNoteSection = $derived(getPageSection($homePage, 'cta'));
/**
* Functions
*/
function scrollLeft() {
scrollContainer?.scrollBy({ left: -300, behavior: 'smooth' });
}
function scrollRight() {
scrollContainer?.scrollBy({ left: 300, behavior: 'smooth' });
}
</script>
<!-- hero -->
{#if $homePage}
{@const content = getPageSection($homePage, 'header')}
{#if content?.show}
<section
class="font-matter flex items-start md:items-center justify-center py-10 lg:py-20 px-5 lg:px-20 h-full bg-[#F4F4F4]"
>
<section
class="flex flex-col-reverse lg:flex-row items-center justify-center lg:justify-between"
>
<div class="text-center lg:text-start space-y-6 w-full mt-4 lg:mt-0">
<div
class="bg-[#E5E7E0] py-1 px-4 md:border-b border-black rounded-sm w-fit mx-auto lg:mx-0"
>
<p class="uppercase font-bold text-xs lg:text-base text-[#0F163F]">
{content.settings.title}
</p>
</div>
<p
class="text-4xl md:text-6xl font-black w-full mx-auto lg:mx-0 md:w-[90%] lg:w-[80%] capitalize"
>
{content.settings.titleHighlight}
</p>
<p class="font-semibold text-xl w-full md:w-[80%] mx-auto lg:mx-0 lg:w-[70%]">
{content.settings.subtitle}
</p>
<PrimaryButton
onClick={() => {
goto(content.settings.action.link);
}}
label={content.settings.action.label}
/>
</div>
<div
class="relative w-[250px] h-[250px] lg:w-[500px] lg:h-[350px] flex items-center justify-center"
>
<svg
class="absolute -left-[15px] lg:-left-[60px] xl:-left-[100px] w-[270px] h-[270px] lg:w-[400px] xl:w-[520px] lg:h-[370px]"
viewBox="0 0 360 360"
>
<circle
cx="180"
cy="180"
r="170"
fill="none"
stroke="#282828"
stroke-width="2"
stroke-dasharray="15, 10"
/>
</svg>
<img
alt="landing page banner"
src={content.settings.banner?.image
? content.settings.banner?.image
: '/calcom-banner-img.jpeg'}
class="object-cover rounded-full w-[250px] h-[250px] lg:w-[500px] lg:h-[350px]"
/>
</div>
</section>
</section>
{/if}
{/if}
<!-- courses -->
{#if coursesSection?.show}
<section id="course" class="flex items-center px-2 md:px-8 py-12 h-full bg-[#E5E7E0]">
<div class="text-white py-8 bg-black rounded-3xl w-full lg:w-[90%] mx-auto">
<div
class="flex flex-col md:flex-row items-center justify-center md:items-start gap-2 md:justify-between px-4 lg:px-8"
>
<h1
class="text-white text-start text-2xl md:text-3xl lg:text-5xl mb-2 font-bold w-full md:w-[80%] lg:w-[60%]"
>
{coursesSection.settings.title}
</h1>
<div class="hidden md:flex gap-2 items-center">
<button
class="w-fit flex items-center justify-center border border-white rounded-full p-2 bg-transparent hover:bg-white hover:text-black"
onclick={scrollLeft}
>
<ArrowLeft />
</button>
<button
class="w-fit flex items-center justify-center border border-white rounded-full p-2 bg-transparent hover:bg-white hover:text-black"
onclick={scrollRight}
>
<ArrowRight />
</button>
</div>
</div>
<div class="w-full pl-4 md:pl-8 overflow-x-hidden">
{#if $courses.length > 0}
<section
bind:this={scrollContainer}
class="flex items-center overflow-x-auto gap-4 py-4 pr-2 w-full scrollbar-hide"
>
{#each $courses as course}
<CourseCard
slug={course.slug}
title={course.title}
bannerImage={course.banner}
description={course.description}
/>
{/each}
<!-- uncomment this to see try the course carousel -->
<!-- {#each coursesList as courseData}
<CourseCard
slug="Patterns"
title="Introduction to patterns"
bannerImage=""
description="This is an introduction to patterns in UIUX designs, it teaches the fundamental concepts about patterns and other intrinsic details"
/>
{/each} -->
</section>
{:else}
<div class="w-full mx-auto">
<EmptyState className="dark:bg-[#232429] dark:border-[#EAEAEA]" />
</div>
{/if}
</div>
<div class="flex items-center justify-center w-full pt-4">
<Button
class="uppercase rounded-2xl py-6 px-10 bg-transparent border-[1.5px] border-white text-white shadow-[-1px_3px_#FFFFFF]"
href="/courses"
>
Explore courses
</Button>
</div>
</div>
</section>
{/if}
<!-- about -->
{#if aboutSection?.show}
<section id="about">
<div class="relative px-4 md:px-10 py-10 h-full w-full mx-auto xl:w-[80%]">
<h1 class="text-center text-5xl mb-2 font-bold md:w-[60%] mx-auto">
{aboutSection.settings.title}
</h1>
<p class="text-center dark:text-white text-lg font-semibold md:w-[70%] mb-8 mx-auto">
{aboutSection.settings.subtitle}
</p>
<div>
<section class="flex flex-wrap items-start justify-center w-full gap-6 py-4">
{#each aboutSection.settings.benefits.list as item}
<div
class="flex flex-col gap-2 p-4 bg-white border border-[#282828] h-[130px] max-h-[180px] w-full max-w-full md:max-w-[320px] lg:max-w-[450px] rounded-lg overflow-hidden"
>
<span class="flex items-center gap-2">
<CheckmarkFilled size={24} />
<p class="text-lg capitalize line-clamp-1">{item.title}</p>
</span>
<p class="text-sm text-[#878787] line-clamp-3">{item.subtitle}</p>
</div>
{/each}
</section>
</div>
</div>
</section>
{/if}
<!-- faq -->
{#if faqSection?.show}
<section id="faq" class="relative px-2 pt-4 pb-20 h-full">
<div class="absolute inset-0 bg-[#E5E7E0] opacity-60"></div>
<div class="relative">
<h1 class="text-center text-5xl font-bold mb-4">
{faqSection.settings.title}
</h1>
<h1 class="text-center text-lg font-semibold mb-4 w-full md:w-[80%] lg:w-[60%] mx-auto">
{faqSection.settings.subtitle}
</h1>
<section class="py-4 px-2 space-y-10 w-full md:w-[80%] mx-auto">
{#each faqSection.settings.questions as faq}
<Accordion title={faq.title} content={faq.content} />
{/each}
</section>
</div>
</section>
{/if}
<!-- testimonial -->
{#if testimonialSection?.show}
<section id="testimonial" class="bg-[#F4F4F4] px-4 lg:p-14 h-fit">
<section class="coolumns-1 md:columns-3 space-y-6 overflow-visible">
{#each testimonialSection.settings.list as item}
<div class="break-inside-avoid overflow-visible">
<section
class="cursor-pointer flex flex-col relative overflow-visible px-6 pt-6 gap-4 p-4 bg-white border border-[#D7D7D7] rounded-3xl w-full h-fit max-w-full sm:max-w-[320px] xl:max-w-[400px]"
>
<div class="flex items-center gap-2">
<img
src={item.banner ?? '/classroomio-course-img-template.jpg'}
alt=""
class="w-10 h-10 rounded-full"
/>
<span class="flex flex-col items-start">
<p class="text-sm font-bold">{item.name}</p>
<p class="text-sm text-[#4C5A70]">{item.role}</p>
</span>
</div>
<div>
<p class=" text-[#282828] dark:text-[#ABABAB]">
{item.description}
</p>
</div>
</section>
</div>
{/each}
</section>
</section>
{/if}
<!-- footernote -->
{#if footerNoteSection?.show}
<section class="w-full bg-[#F4F4F4] py-16 px-4 lg:px-10">
<div
class="relative flex flex-col md:flex-row min-h-40 justify-center border-2 border-[#141414] rounded-xl divide-x divide-transparent w-full lg:w-[80%] mx-auto"
>
<div
class="bg-[#E5E7E0] text-center md:text-start rounded-t-xl md:rounded-l-xl flex justify-center capitalize items-center py-20 px-4 lg:px-10 w-full md:w-[65%] min-h-40 text-4xl font-bold"
>
<h1 class="leading-normal">
{footerNoteSection.settings.title}<br />{footerNoteSection.settings.titleHighlight}
</h1>
</div>
<div
class="rounded-b-xl md:rounded-r-xl flex items-center justify-center py-16 px-4 bg-white w-full md:w-[35%] min-h-40 text-lg font-semibold"
>
<p>
{footerNoteSection.settings.subtitle}
</p>
<Button
class="absolute -bottom-5 uppercase rounded-2xl py-6 bg-white hover:bg-white hover:text-black border-[1.5px] border-[#141414] text-[#141414] shadow-[0px_3px_#141414]"
>
{footerNoteSection.settings.buttonLabel}
</Button>
</div>
</div>
</section>
{/if}
<style>
/* Hide scrollbar for Webkit browsers */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for other browsers */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.break-inside-avoid {
break-inside: avoid;
}
</style>
@@ -0,0 +1,53 @@
<script lang="ts">
import { Add, Subtract } from 'carbon-icons-svelte';
interface Props {
title?: string;
content?: string;
}
let { title = '', content = '' }: Props = $props();
let isOpen = $state(false);
let contentHeight = $state('0px'); // Track the height of the content
let contentRef: HTMLDivElement = $state(); // Reference to the content div
function toggleAccordion() {
isOpen = !isOpen;
if (isOpen) {
// Measure and set the height when opening
contentHeight = `${contentRef?.scrollHeight}px`;
} else {
// Set to 0 height when closing
contentHeight = '0px';
}
}
</script>
<div class="space-y-2 shadow-md bg-white">
<!-- Accordion Header -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex justify-between items-center cursor-pointer p-4 bg-white rounded"
onclick={toggleAccordion}
>
<p class="font-semibold text-lg max-w-[90%]">{title}</p>
<div>
{#if isOpen}
<Subtract size={20} class="text-xl transition-transform duration-200 ease-in" />
{:else}
<Add size={20} class="text-xl transition-transform duration-200 ease-in" />
{/if}
</div>
</div>
<!-- Accordion Content -->
<div
bind:this={contentRef}
style="max-height: {contentHeight}; transition: max-height 0.3s ease-in-out; overflow: hidden;"
>
<div class="p-4 bg-white rounded-b">
<p>{content}</p>
</div>
</div>
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import Calendar from 'carbon-icons-svelte/lib/Calendar.svelte';
</script>
<div
class="hover:scale-95 border border-[#EAEAEA] bg-[#FCFCFC] dark:border-neutral-600 rounded w-full max-w-[300px] space-y-4 p-4"
>
<img src="/classroomio-course-img-template.jpg" alt="tutor" class="object-cover w-full h-full" />
<div class="w-full space-y-2">
<p class="text-lg font-semibold line-clamp-2">
How to Choose the Right Online Course to Boost Your Career
</p>
<span class="inline-flex items-center gap-1 text-[#282828]">
<Calendar class="fill-black" />
<p class="text-xs">Mon Jun 05, 2023</p>
</span>
</div>
</div>
@@ -0,0 +1,131 @@
<script lang="ts">
import RadioButtonChecked from 'carbon-icons-svelte/lib/RadioButtonChecked.svelte';
import GrowthIcon from 'carbon-icons-svelte/lib/Growth.svelte';
import UserProfileIcon from 'carbon-icons-svelte/lib/UserProfile.svelte';
const COURSE_TYPE = {
LIVE_CLASS: 'live',
SELF_PACED: 'paced'
};
interface Props {
bannerImage: string | undefined;
slug?: string;
title?: string;
description?: string;
cost?: number;
totalLessons?: number;
currency?: string;
type: any;
}
let {
bannerImage,
slug = '',
title = '',
description = '',
cost = 0,
totalLessons = 0,
currency = 'USD',
type
}: Props = $props();
let target: any = $state();
function getCourseUrl() {
return `/course/${slug}`;
}
const COURSE_TAG: Record<
string,
{
style: string;
label: string;
icon: any;
iconStyle?: string;
}
> = {
[COURSE_TYPE.LIVE_CLASS]: {
style: '',
label: 'live',
icon: RadioButtonChecked,
iconStyle: 'text-red-700'
},
[COURSE_TYPE.SELF_PACED]: {
style: '',
label: 'paced',
icon: UserProfileIcon,
iconStyle: 'text-primary-700'
},
SPECIALIZATION: {
style: '',
label: 'specialized',
icon: GrowthIcon
}
};
</script>
<a
rel="prefetch"
bind:this={target}
href={getCourseUrl()}
class="text-black border border-gray dark:border-neutral-600 rounded w-full h-fit md:h-[350px] min-w-[250px] max-w-[300px] relative hover:scale-95 transition-all ease-in-out"
>
<div class="p-4">
<div class="relative mb-5">
<img
src={bannerImage || '/classroomio-course-img-template.jpg'}
alt="Course Logo"
class="h-[170px] w-full rounded dark:border dark:border-neutral-600 relative"
/>
{#if type}
{@const tag = COURSE_TAG[type]}
<span
class="absolute bottom-2 left-2 z-10 text-xs capitalize bg-primary-50 rounded-sm p-1 flex items-center gap-1 font-mono"
>
<tag.icon size={16} class={tag.iconStyle} />
{tag.label}
</span>
{/if}
</div>
<h3 class="text-xl dark:text-white font-semibold line-clamp-1">{title}</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-300 description">
{description}
</p>
</div>
<div class="py-4 mx-2 border-t border-gray dark:border-neutral-600 flex justify-between">
<div>
<p class="text-xs pl-2 dark:text-white">
{totalLessons}
lessons
</p>
</div>
<p class="text-xs">
<span class="px-2 font-bold text-[#944499]"
>{!cost ? 'Free' : currency == 'USD' ? `$ ${cost}` : `N ${cost}`}</span
>
</p>
</div>
</a>
<style>
a,
a:hover {
text-decoration: none;
}
.description {
height: 42px;
line-height: 20px;
display: -webkit-box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
-moz-line-clamp: 2;
-ms-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
</style>
@@ -0,0 +1,25 @@
<script lang="ts">
interface Props {
className?: string;
headerClassName?: string;
subtitleClassName?: string;
}
let { className = '', headerClassName = '', subtitleClassName = '' }: Props = $props();
</script>
<section
class="{className} w-full border rounded-md py-4 flex items-center justify-center p-2 my-2"
>
<div
class="flex flex-col md:flex-row justify-center items-center text-center md:text-start gap-6 p-2"
>
<img src="/organization-empty.svg" alt="empty" />
<div>
<h3 class="font-semibold {headerClassName}">"No Course yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like courses have not been added yet.<br /> Kindly check back later
</p>
</div>
</div>
</section>
@@ -0,0 +1,69 @@
<script lang="ts">
import LogoX from 'carbon-icons-svelte/lib/LogoX.svelte';
import LogoDiscord from 'carbon-icons-svelte/lib/LogoDiscord.svelte';
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
import LogoLinkedin from 'carbon-icons-svelte/lib/LogoLinkedin.svelte';
import LogoInstagram from 'carbon-icons-svelte/lib/LogoInstagram.svelte';
import { LogoYoutube } from 'carbon-icons-svelte';
import { getPageSection } from '@/utils/helpers/page';
import { sharedPage } from '@/utils/stores/pages';
const content = $derived(getPageSection($sharedPage, 'footer'));
const seo = $derived(getPageSection($sharedPage, 'seo'));
</script>
{#if content?.show}
<nav
class="bg-white w-full flex flex-col md:flex-row items-start md:items-center gap-4 md:justify-between py-4 px-6"
>
<div class="logo w-full md:w-fit flex items-center justify-between">
<a href="/" title={`Go to ${seo?.settings.title} Home`} id="logo" data-hveid="8">
<img
src={seo?.settings.logo}
alt={`${seo?.settings.title} logo`}
class="rounded w-9 inline-block mx-auto"
data-atf="1"
/>
</a>
<a href="/#" class="md:hidden flex items-center gap-1">
<p class=" text-[#0233BD] text-base underline font-semibold">Built on ClassroomIO</p>
</a>
</div>
<ul
class="my-4 md:my-0 mx-auto flex flex-row items-center justify-center gap-8 underline border rounded-full px-6 py-2"
>
{#if content.settings.instagram}
<a href={content.settings.instagram} target="_blank" title="instragram"
><LogoInstagram class="fill-red-500 " size={24} /></a
>
{/if}
{#if content.settings.twitter}
<a href={content.settings.twitter} target="_blank" title="twitter"><LogoX /></a>
{/if}
{#if content.settings?.youtube}
<a href={content.settings?.youtube} target="_blank"
><LogoYoutube class="fill-red-700" size={24} /></a
>{/if}
{#if content.settings?.discord}
<a href={content.settings.discord} target="_blank" title="discord"
><LogoDiscord class="fill-blue-800" size={24} /></a
>
{/if}
{#if content.settings.linkedin}
<a href={content.settings.linkedin} target="_blank" title="linkedin"
><LogoLinkedin class="fill-blue-800" size={24} /></a
>
{/if}
{#if content.settings.facebook}
<a href={content.settings.facebook} target="_blank" title="facebook"
><LogoFacebook class="fill-blue-800" size={24} /></a
>
{/if}
</ul>
<a href="/#" class="hidden md:flex items-center gap-1">
<p class=" text-[#0233BD] text-base underline font-semibold">Built on ClassroomIO</p>
</a>
</nav>
{/if}
@@ -0,0 +1,31 @@
<script lang="ts">
import RatingComponent from './RatingComponent.svelte';
interface Props {
name?: string;
description?: string;
rating?: number;
banner?: string;
}
let { name = '', description = '', rating = 0, banner = '' }: Props = $props();
</script>
<div
class="flex flex-col md:flex-row items-start gap-3 border max-h-full max-w-[300px] md:max-h-[250px] md:max-w-[500px] lg:max-w-[400px] xl:max-w-[550px] rounded-md p-3"
>
<div class="bg-[#FFE8FF] w-full h-full rounded-md p-2">
<img
src={banner ? banner : './classroomio-course-img-template.jpg'}
alt="tutor"
class="object-cover w-full h-full rounded"
/>
</div>
<div class="w-full space-y-1">
<p class="text-xl font-semibold">{name}</p>
<RatingComponent {rating} />
<p class="text-[#656565] text-sm line-clamp-5 md:line-clamp-4 xl:line-clamp-6">
{description}
</p>
</div>
</div>
@@ -0,0 +1,99 @@
<script lang="ts">
import { ArrowRight, ArrowUpRight, Close } from 'carbon-icons-svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Menu from 'carbon-icons-svelte/lib/Menu.svelte';
import { getPageSection } from '@/utils/helpers/page';
import { sharedPage } from '@/utils/stores/pages';
import type { Section } from '@/utils/types/page';
const content = $derived(getPageSection($sharedPage, 'navigation'));
const seo: Section | undefined = $derived(getPageSection($sharedPage, 'seo'));
let open = $state(false); // State for mobile menu
// Function to toggle the mobile menu
function toggleMenu() {
open = !open;
}
</script>
<nav class={`relative w-full flex items-center justify-between py-4 px-6 bg-white dark:bg-black`}>
<!-- Logo Section -->
<div class="logo">
<a href="/" title={`${seo?.settings?.title}`} id="logo">
{#if seo?.settings?.logo}
<img
src={seo?.settings?.logo}
alt={`${seo.settings.title} logo`}
class="rounded w-9 inline-block mx-auto"
/>
{/if}
</a>
</div>
<!-- Mobile Menu Button (Visible only on mobile and when logged in) -->
<button onclick={toggleMenu} class="lg:hidden">
<Menu size={24} />
</button>
<!-- Desktop Navigation Menu (Hidden on mobile) -->
<ul class="hidden lg:flex items-center space-x-8 text-base font-bold text-[#1F2937] list-none">
{#each content?.settings.navItems as navItem}
<li class="hover:text-[#CE02CE]">
<a
href={navItem.link}
target={navItem.redirect ? '_blank' : undefined}
class="flex items-center gap-1"
>{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={12} />
{/if}
</a>
</li>
{/each}
</ul>
<!-- Mobile Sidebar Menu (Visible only on mobile) -->
<ul
class={`fixed top-0 left-0 pt-10 h-full bg-white w-full transform hover:no-underline ${
open ? 'translate-y-0' : '-translate-y-full'
} transition-transform duration-300 ease-in-out lg:hidden text-base font-bold text-[#1F2937] list-none cursor-pointer`}
>
<button onclick={toggleMenu} class="absolute right-4 top-4 lg:hidden z-20">
<Close size={24} />
</button>
{#each content?.settings.navItems as navItem}
<li class="py-4 px-6 border-b">
<a href={navItem.link} onclick={toggleMenu} class="flex items-center gap-1"
>{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={12} />
{/if}</a
>
</li>
{/each}
<a href="/courses" class="flex items-center gap-1 py-4 px-6 border-b" onclick={toggleMenu}>
<p class="font-bold text-[#1F2937] text-base">Start Learning</p>
<ArrowRight size={16} class="fill-[#1F2937] font-bold" />
</a>
</ul>
<!-- Learn with Me Button (Visible on desktop when logged in) -->
<a href="/courses" class="hidden lg:flex items-center gap-1">
<p class="font-bold text-[#1F2937] text-base">Start Learning</p>
<ArrowRight size={16} class="fill-[#1F2937] font-bold" />
</a>
</nav>
<style>
nav {
position: relative;
}
ul {
z-index: 10;
}
</style>
@@ -0,0 +1,28 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
interface Props {
onClick?: () => void;
label: string;
href?: string | undefined;
class?: string | undefined;
children?: any;
}
const { onClick, children, label, href, class: className, ...restProps }: Props = $props();
</script>
<Button
{href}
class={cn(
'bg-classic rounded text-white font-semibold p-2 hover:scale-90 hover:bg-classic',
className
)}
onclick={onClick}
{...restProps}
>
{label}
{#if children}
{@render children?.()}
{/if}
</Button>
@@ -0,0 +1,32 @@
<script lang="ts">
import Star from 'carbon-icons-svelte/lib/Star.svelte';
import StarFilled from 'carbon-icons-svelte/lib/StarFilled.svelte';
import StarHalf from 'carbon-icons-svelte/lib/StarHalf.svelte';
interface Props {
rating?: number;
}
let { rating = 0 }: Props = $props();
const maxRating = 5;
const getStars = (rating: number) => {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 !== 0;
const emptyStars = maxRating - Math.ceil(rating);
return { fullStars, hasHalfStar, emptyStars };
};
let { fullStars, hasHalfStar, emptyStars } = getStars(rating);
</script>
<div class="flex items-center">
{#each Array(fullStars) as _}
<StarFilled class="mr-1 w-4 h-4 fill-[#FFC107]" aria-label="full star" />
{/each}
{#if hasHalfStar}
<StarHalf class="mr-1 w-4 h-4 fill-[#FFC107]" aria-label="half star" />
{/if}
{#each Array(emptyStars) as _}
<Star class="mr-1 w-4 h-4" aria-label="empty star" />
{/each}
</div>
@@ -0,0 +1,30 @@
<script lang="ts">
interface Props {
description?: string;
name?: string;
banner?: string;
}
let { description = '', name = '', banner = '' }: Props = $props();
</script>
<section
class="flex flex-col justify-between relative border border-[#EAEAEA] px-6 pt-6 space-y-4 bg-[#FDFDFD] rounded w-full h-full lg:h-[280px] max-w-[400px] xl:max-w-[500px]"
>
<img src="/quote.svg" alt="" class="w-6 h-6 rounded-full absolute top-6 left-8" />
<div>
<p class="my-6 font-bold line-clamp-5">
{description}
</p>
</div>
<div class="flex items-center p-4 gap-4 border-t border-[#EAEAEA]">
<img
src={banner ? banner : '/classroomio-course-img-template.jpg'}
alt=""
class="w-6 h-6 rounded-full"
/>
<span>
<p class="text-sm font-bold">{name}</p>
</span>
</div>
</section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

@@ -0,0 +1,14 @@
<svg width="168" height="191" viewBox="0 0 168 191" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M146.745 71.4805H83.6116L78.5517 59.1652C77.773 57.396 76.4882 55.8967 74.859 54.8562C73.2299 53.8157 71.3293 53.2806 69.3966 53.3183H33.7112C31.7785 53.2806 29.8779 53.8157 28.2487 54.8562C26.6196 55.8967 25.3348 57.396 24.5561 59.1652L21.1118 70.3265C20.4737 72.4035 20.1447 74.563 20.1353 76.7357L19.508 158.404C19.4907 160.913 20.4701 163.327 22.2311 165.114C23.9921 166.902 26.3906 167.917 28.8999 167.938H156.136V81.0203C156.155 78.51 155.177 76.095 153.415 74.3061C151.654 72.5172 149.255 71.5009 146.745 71.4805Z" fill="#990199"/>
<path d="M146.744 71.4805H83.6108L78.5509 59.1652C77.7723 57.396 76.4874 55.8967 74.8583 54.8562C73.2291 53.8157 71.3285 53.2806 69.3958 53.3183H33.7105C32.4474 53.3178 31.1946 53.5443 30.0117 53.987H64.7561C68.8218 53.987 74.3847 56.3187 75.8287 59.834L80.8886 72.1552H142.104C147.288 72.1552 153.413 76.422 153.413 81.689V167.92H156.136V81.0203C156.155 78.51 155.176 76.095 153.415 74.3061C151.654 72.5172 149.254 71.5009 146.744 71.4805Z" fill="white"/>
<path d="M146.745 71.4805H83.6116L78.5517 59.1652C77.773 57.396 76.4882 55.8967 74.859 54.8562C73.2299 53.8157 71.3293 53.2806 69.3966 53.3183H33.7112C31.7785 53.2806 29.8779 53.8157 28.2487 54.8562C26.6196 55.8967 25.3348 57.396 24.5561 59.1652L21.1118 70.3265C20.4737 72.4035 20.1447 74.563 20.1353 76.7357L19.508 158.404C19.4907 160.913 20.4701 163.327 22.2311 165.114C23.9921 166.902 26.3906 167.917 28.8999 167.938H156.136V81.0203C156.155 78.51 155.177 76.095 153.415 74.3061C151.654 72.5172 149.255 71.5009 146.745 71.4805Z" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M29.6983 167.917H156.136L134.464 96.9016C133.911 95.0497 132.777 93.4248 131.23 92.2665C129.683 91.1082 127.804 90.478 125.871 90.4688H10.0092C8.58813 90.485 7.19062 90.8334 5.92823 91.4861C4.66584 92.1387 3.57356 93.0776 2.73865 94.2276C1.90375 95.3776 1.34935 96.7069 1.11974 98.1093C0.890134 99.5118 0.991673 100.948 1.41626 102.305L18.9927 159.898C19.6833 162.205 21.0962 164.229 23.0234 165.673C24.9507 167.116 27.2904 167.903 29.6983 167.917Z" fill="#CE02CE"/>
<path d="M7.42288 93.6171H123.291C125.223 93.6273 127.102 94.2579 128.649 95.416C130.196 96.5742 131.33 98.1986 131.884 100.05L152.597 167.917H156.148L134.476 96.9016C133.923 95.0497 132.789 93.4248 131.242 92.2665C129.694 91.1082 127.816 90.478 125.883 90.4688H10.009C8.33774 90.4722 6.70093 90.9443 5.2845 91.8314C3.86807 92.7185 2.7288 93.9851 1.99609 95.4872C3.5445 94.2736 5.45555 93.6151 7.42288 93.6171Z" fill="white"/>
<path d="M29.6983 167.917H156.136L134.464 96.9016C133.911 95.0497 132.777 93.4248 131.23 92.2665C129.683 91.1082 127.804 90.478 125.871 90.4688H10.0092C8.58813 90.485 7.19062 90.8334 5.92823 91.4861C4.66584 92.1387 3.57356 93.0776 2.73865 94.2276C1.90375 95.3776 1.34935 96.7069 1.11974 98.1093C0.890134 99.5118 0.991673 100.948 1.41626 102.305L18.9927 159.898C19.6833 162.205 21.0962 164.229 23.0234 165.673C24.9507 167.116 27.2904 167.903 29.6983 167.917Z" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M71.3737 145.165C79.0348 145.165 85.2454 138.955 85.2454 131.294C85.2454 123.632 79.0348 117.422 71.3737 117.422C63.7125 117.422 57.502 123.632 57.502 131.294C57.502 138.955 63.7125 145.165 71.3737 145.165Z" fill="#263238" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M64.6855 137.982L78.0661 124.578" stroke="#F2F2F2" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M64.6855 124.578L78.0661 137.959" stroke="#F2F2F2" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M163.982 9.00001H126.415C125.774 9.00001 125.16 9.2544 124.707 9.70721C124.254 10.16 124 10.7742 124 11.4145V40.1286C124 40.7689 124.254 41.3831 124.707 41.8359C125.16 42.2887 125.774 42.5431 126.415 42.5431H135.735V51.7396C135.724 51.9767 135.785 52.2116 135.909 52.4139C136.033 52.6162 136.214 52.7765 136.431 52.8741C136.647 52.9718 136.888 53.0021 137.121 52.9613C137.355 52.9204 137.571 52.8103 137.742 52.6451L149.128 42.5431H163.982C164.622 42.5431 165.236 42.2887 165.689 41.8359C166.142 41.3831 166.396 40.7689 166.396 40.1286V11.4145C166.397 11.0972 166.335 10.7829 166.214 10.4896C166.093 10.1963 165.915 9.92983 165.691 9.70547C165.467 9.4811 165.2 9.30328 164.907 9.18221C164.613 9.06115 164.299 8.99923 163.982 9.00001Z" fill="white" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M144.719 29.8483C144.477 29.8525 144.239 29.7971 144.024 29.687C143.809 29.5768 143.624 29.4154 143.487 29.217C143.349 29.0186 143.263 28.7893 143.235 28.5495C143.207 28.3096 143.239 28.0667 143.328 27.8421C143.577 27.2749 143.911 26.7482 144.316 26.2798C144.706 25.8248 145.127 25.3977 145.577 25.0015C146.021 24.6109 146.441 24.2203 146.837 23.8179C147.212 23.4683 147.538 23.0702 147.808 22.6343C148.118 22.0995 148.237 21.4754 148.147 20.8638C148.057 20.2523 147.762 19.6894 147.311 19.267C146.44 18.5913 145.351 18.2586 144.251 18.3319C143.243 18.3061 142.241 18.5084 141.322 18.9237C140.861 19.1388 140.432 19.4177 140.049 19.7522C139.763 19.9972 139.404 20.1414 139.028 20.1628C138.651 20.1842 138.279 20.0815 137.966 19.8706C137.738 19.7172 137.549 19.5131 137.414 19.2743C137.278 19.0356 137.2 18.7686 137.185 18.4945C137.17 18.2203 137.219 17.9465 137.328 17.6944C137.437 17.4424 137.603 17.219 137.812 17.0418C138.359 16.6036 138.955 16.2302 139.588 15.9292C141.154 15.2226 142.859 14.8768 144.577 14.9172C145.948 14.8869 147.313 15.1137 148.601 15.586C149.677 15.9803 150.619 16.6719 151.317 17.5803C151.97 18.4556 152.31 19.5248 152.282 20.6163C152.302 21.377 152.176 22.1345 151.909 22.8473C151.673 23.4382 151.341 23.986 150.927 24.4689C150.522 24.9353 150.085 25.3723 149.619 25.7767C149.145 26.1851 148.713 26.5816 148.305 26.9603C147.912 27.3411 147.577 27.7778 147.311 28.2564C147.209 28.4443 147.127 28.6427 147.068 28.8482C146.975 29.1491 146.788 29.4125 146.535 29.5998C146.281 29.7871 145.975 29.8887 145.66 29.8897L144.719 29.8483ZM144.943 37.299C144.621 37.3054 144.3 37.247 144.001 37.1274C143.701 37.0078 143.429 36.8293 143.199 36.6026C142.97 36.3759 142.788 36.1054 142.665 35.8073C142.541 35.5091 142.479 35.1893 142.482 34.8667C142.476 34.551 142.535 34.2374 142.655 33.9453C142.775 33.6532 142.954 33.3887 143.18 33.1683C143.665 32.7241 144.298 32.4777 144.955 32.4777C145.613 32.4777 146.246 32.7241 146.731 33.1683C146.954 33.3902 147.13 33.6551 147.248 33.9471C147.366 34.239 147.424 34.5519 147.417 34.8667C147.422 35.1926 147.361 35.5161 147.237 35.8174C147.113 36.1187 146.928 36.3915 146.695 36.6192C146.462 36.8468 146.185 37.0245 145.88 37.1414C145.576 37.2583 145.251 37.3119 144.926 37.299H144.943Z" fill="#EC644E" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M89.0291 190.495C131.502 190.495 165.933 188.569 165.933 186.193C165.933 183.817 131.502 181.891 89.0291 181.891C46.5561 181.891 12.125 183.817 12.125 186.193C12.125 188.569 46.5561 190.495 89.0291 190.495Z" fill="#8192C3" fill-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -0,0 +1,3 @@
<svg width="28" height="22" viewBox="0 0 28 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 22V16.2105C28 14.4522 27.6966 12.5867 27.0897 10.614C26.4619 8.61988 25.562 6.70078 24.3901 4.85673C23.1973 2.99123 21.7638 1.37232 20.0897 0L16.0717 3.34503C17.3901 5.27485 18.5411 7.29045 19.5247 9.39181C20.4873 11.4717 20.9686 13.7018 20.9686 16.0819V22H28ZM11.9283 22V16.2105C11.9283 14.4522 11.6248 12.5867 11.0179 10.614C10.3901 8.61988 9.49029 6.70078 8.31839 4.85673C7.12556 2.99123 5.69208 1.37232 4.01794 0L0 3.34503C1.31839 5.27485 2.46936 7.29045 3.45292 9.39181C4.41555 11.4717 4.89686 13.7018 4.89686 16.0819V22H11.9283Z" fill="#CE02CE"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

@@ -0,0 +1,15 @@
import type { Component } from 'svelte';
import CoursesPage from './pages/Courses.svelte';
import HomePage from './pages/Home.svelte';
import Navigation from './Navigation.svelte';
import Footer from './Footer.svelte';
import Button from './PrimaryButton.svelte';
import type { TemplateComponents } from '$lib/utils/types/template';
export const components: TemplateComponents = {
navigation: Navigation as unknown as Component,
footer: Footer as unknown as Component,
button: Button as unknown as Component,
coursespage: CoursesPage as unknown as Component,
homepage: HomePage as unknown as Component
};
@@ -0,0 +1,144 @@
<script lang="ts">
import { Button } from '@/components/ui/button';
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import { COURSE_TYPE } from '@/utils/constants/course';
import { getPageSection } from '@/utils/helpers/page';
import type { Course, CourseFilterItem } from '@/utils/types/course';
import type { Page } from '$lib/utils/types/page';
interface Props {
data: {
page: Page;
sharedPage: Page;
courses: Course[];
};
}
let { data }: Props = $props();
/**
* State
*/
let viewAll = $state(false);
let filter: CourseFilterItem[] = $state([
{
title: 'Self Paced',
type: COURSE_TYPE.PACED,
checked: false
},
{
title: 'Live Sessions',
type: COURSE_TYPE.LIVE,
checked: false
}
]);
let filteredCourses = $state([...data.courses]);
/**
* Constants
*/
const section = $derived({
header: getPageSection(data.page, 'header'),
courses: getPageSection(data.page, 'courses')
});
/**
* Functions
*/
function applyFilter() {
const activeFilters = new Set(
filter
.filter((filterItem) => filterItem.checked)
.map((filterItem) => filterItem.type.toLowerCase())
);
filteredCourses =
activeFilters.size === 0
? data.courses
: data.courses.filter((course) => activeFilters.has(course.type.toLowerCase()));
}
</script>
<section class="overflow-x-hidden">
{#if section.header?.show}
<div
class="h-full pt-20 pb-36 flex flex-col items-center justify-center text-center w-full bg-gradient-to-r from-[#f0a5f11a] via-white to-[#f6e7f61a]"
>
<h1 class="font-bold text-5xl mb-4 w-full md:w-[50%] text-center">
{section.header.settings.title}
<span class="text-[#CE02CE]/80 font-bold text-5xl">
{section.header.settings.titleHighlight}</span
>
</h1>
<p class="md:text-xl text-[#878787] w-full md:w-[60%] lg:w-[40%] text-center">
{section.header.settings.subtitle}
</p>
</div>
{/if}
{#if section.courses?.show}
<section id="course" class="px-4 pt-4 pb-20 h-full bg-white">
<h1 class="text-center text-3xl text-[#3F3F3F] font-bold mb-6 w-full md:w-[60%] mx-auto">
{section.courses.settings.title}
</h1>
<div class="w-full md:w-[90%] mx-auto">
{#if data.courses.length > 0}
<div class="w-full flex gap-4 lg:ml-[5%]">
<div class="hidden lg:block w-fit space-y-2">
<p class="font-medium text-[#3C4043] uppercase">Filter</p>
<div class="w-max border border-[#EAEAEA] p-6 space-y-8">
{#each filter as item}
<form class="space-x-2 text-[#3C4043]">
<input
type="checkbox"
name={item.title}
checked={item.checked}
onchange={applyFilter}
/>
<label for={item.title}>{item.title}</label>
</form>
{/each}
</div>
</div>
<section
class="grid place-items-center grid-cols-1 md:grid-cols-2 xl:grid-cols-3 w-full"
>
{#each filteredCourses.slice(0, viewAll ? filteredCourses.length : 3) as courseData}
<CourseCard
slug={courseData.slug}
bannerImage={courseData.banner ?? './classroomio-courseData-img-template.jpg'}
title={courseData.title}
type={courseData.type}
description={courseData.description}
cost={courseData.cost}
currency={courseData.currency}
totalLessons={courseData.lessonsCount}
/>
{/each}
{#if filteredCourses.length == 0}
<div class="px-4 w-full mx-auto">
<EmptyState headerClassName="text-[#CE02CE]" />
</div>
{/if}
</section>
{#if filteredCourses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<Button
class="text-lg font-semibold text-white !bg-[#CE02CE]"
on:click={() => (viewAll = !viewAll)}>View more programs</Button
>
</div>
{/if}
</div>
{:else}
<div class="px-4 w-full lg:w-[70%] mx-auto">
<EmptyState headerClassName="text-[#CE02CE]" />
</div>
{/if}
</div>
</section>
{/if}
</section>
@@ -0,0 +1,242 @@
<script lang="ts">
import { getPageSection } from '@/utils/helpers/page';
import { homePage } from '@/utils/stores/pages';
import PrimaryButton from '../PrimaryButton.svelte';
import { goto } from '$app/navigation';
import banner from '../assets/org-banner.png';
import { courses } from '@/utils/stores/course';
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import InstructorCard from '../InstructorsCard.svelte';
import Accordion from '../Accordion.svelte';
import TestimonialCard from '../TestimonialCard.svelte';
import BlogCard from '../BlogCard.svelte';
const faqSection = $derived(getPageSection($homePage, 'faq'));
const testimonialSection = $derived(getPageSection($homePage, 'testimonial'));
const aboutSection = $derived(getPageSection($homePage, 'about'));
const courseSection = $derived(getPageSection($homePage, 'courses'));
const instructorSection = $derived(getPageSection($homePage, 'instructors'));
const footerNoteSection = $derived(getPageSection($homePage, 'cta'));
const DISPLAY_COURSE = {
ALL: 'all',
COURSE: 'course',
PATH: 'path'
};
let viewAll = $state(false);
let type = DISPLAY_COURSE.ALL;
</script>
<main>
<!-- <Hero /> -->
{#if $homePage}
{@const content = getPageSection($homePage, 'header')}
{#if content?.show}
<section class="flex items-start justify-center pb-20 lg:pt-10 pt-4 px-6 md:px-14 h-full">
<section class="flex flex-col-reverse md:flex-row items-start gap-10 md:justify-between">
<div class="space-y-6 w-full">
<p class="text-4xl xl:text-6xl font-bold w-full xl:w-[90%]">
{content.settings.title}
<span class="text-[#CE02CE]">
{content.settings.titleHighlight}
</span>
</p>
<p class="w-full lg:w-[70%] text-[#878787] xl:text-lg">
{content.settings.subtitle}
</p>
<PrimaryButton
onClick={() => {
goto(content.settings.action.link);
}}
label={content.settings.action.label}
/>
</div>
<div
class="rounded-md md:h-[300px] xl:h-[500px] max-h-full w-full md:w-1/2 lg:w-4/5 xl:w-[800px] md:max-w-[800px] flex"
>
<img
style="min-width:280px; min-height:200px"
alt="landing page banner"
src={content.settings.banner.image ? content.settings.banner.image : banner}
class="object-cover mt-2 h-full w-full rounded-md md:mt-0"
/>
</div>
</section>
</section>
{/if}
{/if}
<!-- about -->
{#if aboutSection?.show}
<section
id="about"
class="flex items-start justify-center px-6 md:px-10 lg:px-14 h-full bg-white border-b-2 pb-20 lg:pt-20 pt-4"
>
<section class="flex flex-col lg:flex-row gap-4 items-center lg:items-start justify-center">
<div class="w-full lg:w-[60%] space-y-4">
<p class="text-4xl font-bold text-[#3F3F3F]">{aboutSection.settings.title}</p>
<p class="w-full lg:w-[80%] text-base leading-7 text-[#878787]">
{aboutSection.settings.content}
</p>
</div>
{#if aboutSection.settings.benefits}
<div class="max-w-[400px] min-h-fit">
{#each aboutSection.settings.benefits.list as item, index}
<div
class="benefit-card w-full text-center max-w-[200px] p-4 font-semibold mb-9 border-b-4 border-[#CE02CE] bg-white rounded-b-lg shadow-lg"
class:left={index % 2 === 0}
class:right={index % 2 !== 0}
>
{item.title}
</div>
{/each}
</div>
{:else}
<img
src={aboutSection.settings.imageUrl}
alt="Our Story"
class="rounded-2xl max-h-[450px]"
/>
{/if}
</section>
</section>
{/if}
<!-- courses -->
{#if courseSection?.show}
<section id="course" class="px-4 py-6 pb-20 h-full bg-white">
<h1 class="text-center text-3xl text-[#3F3F3F] font-bold mb-4">
{courseSection.settings.title}
</h1>
<div class="w-full md:w-[90%] mx-auto">
{#if $courses.length > 0}
<section class="flex flex-wrap items-center mx-auto w-fit gap-4">
{#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData}
<CourseCard
slug={courseData.slug}
bannerImage={courseData.banner || '/classroomio-course-img-template.jpg'}
title={courseData.title}
type={courseData.type}
description={courseData.description}
cost={courseData.cost}
currency={courseData.currency}
totalLessons={courseData.lessonsCount}
/>
{/each}
</section>
{#if $courses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<PrimaryButton
class="text-lg font-semibold text-white bg-[#CE02CE]"
onClick={() => (viewAll = !viewAll)}
label="View more programs"
/>
</div>
{/if}
{:else}
<div class="px-4 w-full lg:w-[70%] mx-auto">
<EmptyState headerClassName="text-[#CE02CE]" />
</div>
{/if}
</div>
</section>
{/if}
<!-- instructors -->
{#if instructorSection?.show}
<section class="px-4 lg:px-14 pt-4 pb-20 h-full bg-white">
<div class="w-full xl:w-[90%] mx-auto">
<h1 class=" text-center lg:text-start text-3xl text-[#3F3F3F] font-bold mb-4">
Meet some of our Instructors
</h1>
<section class="grid place-items-center grid-cols-1 md:grid-cols-2 gap-2 w-full">
{#each instructorSection.settings.list as item}
<InstructorCard name={item.name} description={item.description} rating={item.rating} />
{/each}
</section>
<div class="w-full flex justify-center md:justify-end px-4 mt-6">
<PrimaryButton
class="text-lg font-semibold text-white p-6 rounded"
label="Start learning & Explore courses"
/>
</div>
</div>
</section>
{/if}
<!-- faq -->
{#if faqSection?.show}
<section class="px-4 py-6 pb-20 h-full bg-[#F9F9F9]">
<h1 class="text-center text-3xl text-[#3F3F3F] font-bold mb-4">
{faqSection.settings.title}
</h1>
<section class="p-2 space-y-10 w-full md:w-[80%] mx-auto">
{#each faqSection.settings.questions as faq}
<Accordion title={faq.title} content={faq.content} />
{/each}
</section>
</section>
{/if}
<!-- testimonial -->
{#if testimonialSection?.show}
<section id="testimonial" class="px-4 lg:px-14 pt-4 pb-20 h-full bg-white">
<h1 class="text-center text-3xl text-[#3F3F3F] font-bold mb-4">
Words from our past learners
</h1>
<section class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 w-full">
{#each testimonialSection.settings.list as item}
<TestimonialCard description={item.description} name={item.name} />
{/each}
</section>
</section>
{/if}
<!-- footerNote -->
{#if footerNoteSection?.show}
<section class="flex flex-col items-center justify-between px-6 lg:px-10 py-20 bg-classic">
<div class="flex items-center justify-center w-full">
<p class="text-4xl text-white text-center font-bold w-full">
{footerNoteSection.settings.title}
</p>
</div>
<div class="w-full flex items-center justify-center my-5">
<PrimaryButton
onClick={() => goto('/courses')}
class="rounded-sm font-bold text-lg bg-classic-secondary border hover:scale-95 hover:bg-classic-secondary border-gray-100"
label={footerNoteSection.settings.buttonLabel}
/>
</div>
</section>
{/if}
<!-- blog -->
<section id="course" class="px-4 py-6 pb-20 h-full bg-white">
<h1 class="text-center text-3xl text-[#3F3F3F] font-bold mb-4">Latest blog Post</h1>
<section class="flex flex-wrap items-center mx-auto gap-2 w-full">
<BlogCard />
<BlogCard />
<BlogCard />
<BlogCard />
</section>
</section>
</main>
<style>
.benefit-card {
transition: transform 0.3s ease-in-out;
position: relative;
}
.benefit-card.left {
float: left;
margin-left: 6%;
z-index: 2;
}
.benefit-card.right {
float: right;
margin-right: 6%;
z-index: 1;
}
/* Make sure the cards overlap slightly */
.benefit-card:not(:first-child) {
margin-top: -40px; /* Adjust this for more overlap */
}
</style>
@@ -0,0 +1,21 @@
import { components as CalComponents } from './cal';
import { components as PosthogComponents } from './posthog';
import { components as ClassicComponents } from './classic';
import { components as MinimalComponents } from './minimal';
function getComponents() {
switch (import.meta.env.VITE_TEMPLATE) {
case 'posthog':
return PosthogComponents;
case 'classic':
return ClassicComponents;
case 'minimal':
return MinimalComponents;
default:
return CalComponents;
}
}
export const components = {
...getComponents()
};
@@ -0,0 +1,53 @@
<script lang="ts">
import { Add, Subtract } from 'carbon-icons-svelte';
interface Props {
title?: string;
content?: string;
}
let { title = '', content = '' }: Props = $props();
let isOpen = $state(false);
let contentHeight = $state('0px'); // Track the height of the content
let contentRef: HTMLDivElement = $state(); // Reference to the content div
function toggleAccordion() {
isOpen = !isOpen;
if (isOpen) {
// Measure and set the height when opening
contentHeight = `${contentRef?.scrollHeight}px`;
} else {
// Set to 0 height when closing
contentHeight = '0px';
}
}
</script>
<div class="space-y-2 shadow-md bg-white">
<!-- Accordion Header -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex justify-between items-center cursor-pointer p-4 bg-white rounded"
onclick={toggleAccordion}
>
<p class="font-semibold text-lg">{title}</p>
{#if isOpen}
<Subtract class="text-xl transition-transform duration-200 ease-in" />
{:else}
<Add class="text-xl transition-transform duration-200 ease-in" />
{/if}
</div>
<!-- Accordion Content -->
<div
bind:this={contentRef}
style="max-height: {contentHeight}; transition: max-height 0.3s ease-in-out; overflow: hidden;"
>
<div class="p-4 bg-white rounded-b">
<p>{content}</p>
</div>
</div>
</div>
@@ -0,0 +1,56 @@
<script lang="ts">
import { ArrowRight } from 'carbon-icons-svelte';
interface Props {
className?: string;
buttonClass?: string;
slug?: string;
title?: string;
description?: string;
cost?: number;
currency?: string;
lessons?: number;
}
let {
className = '',
buttonClass = '',
slug = '',
title = '',
description = '',
cost = 0,
currency = 'USD',
lessons = 0
}: Props = $props();
function getCourseUrl() {
return `/course/${slug}`;
}
</script>
<div class="h-fit w-full min-w-[250px] max-w-[300px] space-y-4">
<div class="border border-[#EAEAEA] rounded-sm space-y-4 px-4 {className}">
<div class="py-4 border-b overflow-hidden">
<p class="text-xl font-semibold p border-[#EAEAEA] overflow-ellipsis line-clamp-1">
{title}
</p>
</div>
<div class="rounded-md overflow-hidden">
<p class=" text-[#878787] overflow-ellipsis line-clamp-3 text-justify">
{description}
</p>
</div>
<span class="flex justify-between items-center py-4">
<p>{lessons} Lessons</p>
<p class="font-bold text-[#0233BD]">
{!cost ? 'Free' : currency == 'USD' ? `$ ${cost}` : `N ${cost}`}
</p>
</span>
</div>
<a
href={getCourseUrl()}
class="flex items-center justify-between w-full border border-[#EAEAEA] rounded-sm p-3 cursor-pointer hover:no-underline hover:scale-90 transition-all duration-200 {buttonClass}"
>
<p class="font-bold text-[#0233BD] uppercase">Learn More</p>
<ArrowRight class="text-[#0233BD]" />
</a>
</div>
@@ -0,0 +1,26 @@
<script lang="ts">
import emptyBanner from './assets/expert-empty.svg';
interface Props {
className?: string;
headerClassName?: string;
subtitleClassName?: string;
}
let { className = '', headerClassName = '', subtitleClassName = '' }: Props = $props();
</script>
<section
class="{className} w-full border rounded-md py-4 flex items-center justify-center p-2 my-2"
>
<div
class="flex flex-col md:flex-row justify-center items-center text-center md:text-start gap-6 p-2"
>
<img src={emptyBanner} alt="empty" />
<div>
<h3 class="font-semibold {headerClassName}">"No Course yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like courses have not been added yet.<br /> Kindly check back later
</p>
</div>
</div>
</section>
@@ -0,0 +1,43 @@
<script lang="ts">
import { getPageSection } from '@/utils/helpers/page';
import { sharedPage } from '@/utils/stores/pages';
const content = $derived(getPageSection($sharedPage, 'footer'));
const seo = $derived(getPageSection($sharedPage, 'seo'));
</script>
{#if content?.show}
<nav
class="bg-white w-full flex flex-col md:flex-row items-start md:items-center gap-4 md:justify-between py-4 px-6"
>
<div class="logo">
<a href="/" title={`Go to ${seo?.settings.title} Home`} id="logo" data-hveid="8">
<img
src={seo?.settings.logo}
alt={`${seo?.settings.title} logo`}
class="rounded w-9 inline-block mx-auto"
data-atf="1"
/>
</a>
</div>
<ul
class="flex flex-col md:flex-row items-start md:items-center gap-4 -md:gap-8 font-semibold capitalize underline"
>
{#if content.settings.twitter}
<a href={content.settings.twitter} target="_blank" title="twitter">Twitter</a>
{/if}
{#if content.settings?.youtube}
<a href={content.settings?.youtube} target="_blank">Youtube</a>{/if}
{#if content.settings.linkedin}
<a href={content.settings.linkedin} target="_blank" title="linkedin">LinkedIn</a>
{/if}
{#if content.settings.facebook}
<a href={content.settings.facebook} target="_blank" title="facebook">facebook</a>
{/if}
</ul>
<a href="/#" class="flex items-center hover:underline text-[#0233BD] dark:text-white gap-1">
<p class="text-base font-semibold">Built on ClassroomIO</p>
</a>
</nav>
{/if}
@@ -0,0 +1,95 @@
<script lang="ts">
import { ArrowRight, ArrowUpRight, Close } from 'carbon-icons-svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { getPageSection } from '@/utils/helpers/page';
import { sharedPage } from '@/utils/stores/pages';
import Menu from 'carbon-icons-svelte/lib/Menu.svelte';
let open = $state(false);
function toggleMenu() {
open = !open;
}
const seo = $derived(getPageSection($sharedPage, 'seo'));
const content = $derived(getPageSection($sharedPage, 'navigation'));
</script>
<nav class={`relative w-full flex items-center justify-between py-4 px-6 bg-white dark:bg-black`}>
<!-- Logo Section -->
<div class="logo">
<a href="/" title={`${seo?.settings.title} home`} id="logo">
<img
src={seo?.settings.logo}
alt={`${seo?.settings.title} logo`}
class="rounded w-9 inline-block mx-auto"
/>
</a>
</div>
<!-- Mobile Menu Button (Visible only on mobile and when logged in) -->
<button onclick={toggleMenu} class="lg:hidden">
<Menu size={24} />
</button>
<!-- Desktop Navigation Menu (Hidden on mobile) -->
<ul
class="hidden lg:flex items-center space-x-8 text-base font-bold text-[#1F2937] list-none hover:no-underline"
>
{#each content?.settings.navItems as navItem}
<li>
<a href={navItem.link} class="flex items-center gap-1">
{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={12} />
{/if}
</a>
</li>
{/each}
</ul>
<!-- Mobile Sidebar Menu (Visible only on mobile) -->
<ul
class={`fixed top-0 left-0 pt-10 h-full bg-white w-full transform hover:no-underline ${
open ? 'translate-y-0' : '-translate-y-full'
} transition-transform duration-300 ease-in-out lg:hidden text-base font-bold text-[#1F2937] list-none cursor-pointer`}
>
<button onclick={toggleMenu} class="absolute right-4 top-4 lg:hidden z-20">
<Close size={24} />
</button>
{#each content?.settings.navItem as navItem}
<li class="py-4 px-6 border-b">
<a href={navItem.link} onclick={toggleMenu} class="flex item-center gap-1">
{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={12} />
{/if}
</a>
</li>
{/each}
<a href="/courses" class="flex items-center gap-1 py-4 px-6 border-b" onclick={toggleMenu}>
<p class="font-bold text-[#1F2937] text-base">Learn with me</p>
<ArrowRight size={16} class="fill-[#1F2937] font-bold" />
</a>
</ul>
<!-- Learn with Me Button (Visible on desktop when logged in) -->
<a href="/courses" class="hidden lg:flex items-center gap-1">
<p class="font-bold text-[#1F2937] text-base">Learn with me</p>
<ArrowRight size={16} class="fill-[#1F2937] font-bold" />
</a>
</nav>
<style>
nav {
position: relative;
}
ul {
z-index: 10;
}
</style>
@@ -0,0 +1,28 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
interface Props {
onClick?: () => void;
label?: string;
href?: string;
class?: string;
children?: any;
}
const { onClick, children, label, href, class: className, ...restProps }: Props = $props();
</script>
<Button
{href}
onclick={onClick}
class={cn(
'rounded-none uppercase bg-minimal hover:bg-minimal p-3 hover:scale-95 font-bold',
className
)}
{...restProps}
>
{label}
{#if children}
{@render children?.()}
{/if}
</Button>
@@ -0,0 +1,28 @@
<script lang="ts">
import { getPageSection } from '@/utils/helpers/page';
import { sharedPage } from '@/utils/stores/pages';
interface Props {
name?: string;
description?: string;
role?: string;
}
let { name = '', description = '', role = '' }: Props = $props();
const seo = $derived(getPageSection($sharedPage, 'seo'));
</script>
<section
class="border bg-[#192533] text-white border-[#D7D7D7] px-4 py-6 space-y-4 rounded h-full w-full max-w-[350px] md:max-w-[300px]"
>
<div class="flex items-center gap-2">
<img src={seo?.settings.logo} alt="" class="w-12 h-12 rounded-full" />
<span>
<p class="text-sm font-bold">{name}</p>
<p class="text-xs text-white">{role}</p>
</span>
</div>
<p class="text-white text-justify line-clamp-5">
{description}
</p>
</section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -0,0 +1,81 @@
<svg
width="168"
height="191"
viewBox="0 0 168 191"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M146.744 71.4805H83.6106L78.5507 59.1652C77.772 57.396 76.4872 55.8967 74.858 54.8562C73.2289 53.8157 71.3283 53.2806 69.3956 53.3183H33.7102C31.7775 53.2806 29.8769 53.8157 28.2478 54.8562C26.6186 55.8967 25.3338 57.396 24.5551 59.1652L21.1108 70.3265C20.4728 72.4035 20.1438 74.563 20.1344 76.7357L19.5071 158.404C19.4897 160.913 20.4691 163.327 22.2301 165.114C23.9911 166.902 26.3897 167.917 28.8989 167.938H156.135V81.0203C156.154 78.51 155.176 76.095 153.414 74.3061C151.653 72.5172 149.254 71.5009 146.744 71.4805Z"
fill="#032587"
/>
<path
d="M146.744 71.4805H83.6108L78.5509 59.1652C77.7723 57.396 76.4874 55.8967 74.8583 54.8562C73.2291 53.8157 71.3285 53.2806 69.3958 53.3183H33.7105C32.4474 53.3178 31.1946 53.5443 30.0117 53.987H64.7561C68.8218 53.987 74.3847 56.3187 75.8287 59.834L80.8886 72.1552H142.104C147.288 72.1552 153.413 76.422 153.413 81.689V167.92H156.136V81.0203C156.155 78.51 155.176 76.095 153.415 74.3061C151.654 72.5172 149.254 71.5009 146.744 71.4805Z"
fill="white"
/>
<path
d="M146.744 71.4805H83.6106L78.5507 59.1652C77.772 57.396 76.4872 55.8967 74.858 54.8562C73.2289 53.8157 71.3283 53.2806 69.3956 53.3183H33.7102C31.7775 53.2806 29.8769 53.8157 28.2478 54.8562C26.6186 55.8967 25.3338 57.396 24.5551 59.1652L21.1108 70.3265C20.4728 72.4035 20.1438 74.563 20.1344 76.7357L19.5071 158.404C19.4897 160.913 20.4691 163.327 22.2301 165.114C23.9911 166.902 26.3897 167.917 28.8989 167.938H156.135V81.0203C156.154 78.51 155.176 76.095 153.414 74.3061C151.653 72.5172 149.254 71.5009 146.744 71.4805Z"
stroke="#263238"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M29.6983 167.917H156.136L134.464 96.9016C133.911 95.0497 132.777 93.4248 131.23 92.2665C129.683 91.1082 127.804 90.478 125.871 90.4688H10.0092C8.58813 90.485 7.19062 90.8334 5.92823 91.4861C4.66584 92.1387 3.57356 93.0776 2.73865 94.2276C1.90375 95.3776 1.34935 96.7069 1.11974 98.1093C0.890134 99.5118 0.991673 100.948 1.41626 102.305L18.9927 159.898C19.6833 162.205 21.0962 164.229 23.0234 165.673C24.9507 167.116 27.2904 167.903 29.6983 167.917Z"
fill="#0233BD"
/>
<path
d="M7.42288 93.6171H123.291C125.223 93.6273 127.102 94.2579 128.649 95.416C130.196 96.5742 131.33 98.1986 131.884 100.05L152.597 167.917H156.148L134.476 96.9016C133.923 95.0497 132.789 93.4248 131.242 92.2665C129.694 91.1082 127.816 90.478 125.883 90.4688H10.009C8.33774 90.4722 6.70093 90.9443 5.2845 91.8314C3.86807 92.7185 2.7288 93.9851 1.99609 95.4872C3.5445 94.2736 5.45555 93.6151 7.42288 93.6171Z"
fill="white"
/>
<path
d="M29.6983 167.917H156.136L134.464 96.9016C133.911 95.0497 132.777 93.4248 131.23 92.2665C129.683 91.1082 127.804 90.478 125.871 90.4688H10.0092C8.58813 90.485 7.19062 90.8334 5.92823 91.4861C4.66584 92.1387 3.57356 93.0776 2.73865 94.2276C1.90375 95.3776 1.34935 96.7069 1.11974 98.1093C0.890134 99.5118 0.991673 100.948 1.41626 102.305L18.9927 159.898C19.6833 162.205 21.0962 164.229 23.0234 165.673C24.9507 167.116 27.2904 167.903 29.6983 167.917Z"
stroke="#263238"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M71.3727 145.165C79.0338 145.165 85.2444 138.955 85.2444 131.294C85.2444 123.632 79.0338 117.422 71.3727 117.422C63.7115 117.422 57.501 123.632 57.501 131.294C57.501 138.955 63.7115 145.165 71.3727 145.165Z"
fill="#263238"
stroke="#263238"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M64.6855 137.982L78.0661 124.578"
stroke="#F2F2F2"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M64.6855 124.578L78.0661 137.959"
stroke="#F2F2F2"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M163.982 9.00001H126.415C125.774 9.00001 125.16 9.2544 124.707 9.70721C124.254 10.16 124 10.7742 124 11.4145V40.1286C124 40.7689 124.254 41.3831 124.707 41.8359C125.16 42.2887 125.774 42.5431 126.415 42.5431H135.735V51.7396C135.724 51.9767 135.785 52.2116 135.909 52.4139C136.033 52.6162 136.214 52.7765 136.431 52.8741C136.647 52.9718 136.888 53.0021 137.121 52.9613C137.355 52.9204 137.571 52.8103 137.742 52.6451L149.128 42.5431H163.982C164.622 42.5431 165.236 42.2887 165.689 41.8359C166.142 41.3831 166.396 40.7689 166.396 40.1286V11.4145C166.397 11.0972 166.335 10.7829 166.214 10.4896C166.093 10.1963 165.915 9.92983 165.691 9.70547C165.467 9.4811 165.2 9.30328 164.907 9.18221C164.613 9.06115 164.299 8.99923 163.982 9.00001Z"
fill="white"
stroke="#263238"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M144.719 29.8483C144.477 29.8525 144.239 29.7971 144.024 29.687C143.809 29.5768 143.624 29.4154 143.487 29.217C143.349 29.0186 143.263 28.7893 143.235 28.5495C143.207 28.3096 143.239 28.0667 143.328 27.8421C143.577 27.2749 143.911 26.7482 144.316 26.2798C144.706 25.8248 145.127 25.3977 145.577 25.0015C146.021 24.6109 146.441 24.2203 146.837 23.8179C147.212 23.4683 147.538 23.0702 147.808 22.6343C148.118 22.0995 148.237 21.4754 148.147 20.8638C148.057 20.2523 147.762 19.6894 147.311 19.267C146.44 18.5913 145.351 18.2586 144.251 18.3319C143.243 18.3061 142.241 18.5084 141.322 18.9237C140.861 19.1388 140.432 19.4177 140.049 19.7522C139.763 19.9972 139.404 20.1414 139.028 20.1628C138.651 20.1842 138.279 20.0815 137.966 19.8706C137.738 19.7172 137.549 19.5131 137.414 19.2743C137.278 19.0356 137.2 18.7686 137.185 18.4945C137.17 18.2203 137.219 17.9465 137.328 17.6944C137.437 17.4424 137.603 17.219 137.812 17.0418C138.359 16.6036 138.955 16.2302 139.588 15.9292C141.154 15.2226 142.859 14.8768 144.577 14.9172C145.948 14.8869 147.313 15.1137 148.601 15.586C149.677 15.9803 150.619 16.6719 151.317 17.5803C151.97 18.4556 152.31 19.5248 152.282 20.6163C152.302 21.377 152.176 22.1345 151.909 22.8473C151.673 23.4382 151.341 23.986 150.927 24.4689C150.522 24.9353 150.085 25.3723 149.619 25.7767C149.145 26.1851 148.713 26.5816 148.305 26.9603C147.912 27.3411 147.577 27.7778 147.311 28.2564C147.209 28.4443 147.127 28.6427 147.068 28.8482C146.975 29.1491 146.788 29.4125 146.535 29.5998C146.281 29.7871 145.975 29.8887 145.66 29.8897L144.719 29.8483ZM144.943 37.299C144.621 37.3054 144.3 37.247 144.001 37.1274C143.701 37.0078 143.429 36.8293 143.199 36.6026C142.97 36.3759 142.788 36.1054 142.665 35.8073C142.541 35.5091 142.479 35.1893 142.482 34.8667C142.476 34.551 142.535 34.2374 142.655 33.9453C142.775 33.6532 142.954 33.3887 143.18 33.1683C143.665 32.7241 144.298 32.4777 144.955 32.4777C145.613 32.4777 146.246 32.7241 146.731 33.1683C146.954 33.3902 147.13 33.6551 147.248 33.9471C147.366 34.239 147.424 34.5519 147.417 34.8667C147.422 35.1926 147.361 35.5161 147.237 35.8174C147.113 36.1187 146.928 36.3915 146.695 36.6192C146.462 36.8468 146.185 37.0245 145.88 37.1414C145.576 37.2583 145.251 37.3119 144.926 37.299H144.943Z"
fill="#EC644E"
stroke="#263238"
stroke-width="1.47949"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M89.0291 190.495C131.502 190.495 165.933 188.569 165.933 186.193C165.933 183.817 131.502 181.891 89.0291 181.891C46.5561 181.891 12.125 183.817 12.125 186.193C12.125 188.569 46.5561 190.495 89.0291 190.495Z"
fill="#8192C3"
fill-opacity="0.2"
/>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.0 MiB

@@ -0,0 +1,15 @@
import type { Component } from 'svelte';
import CoursesPage from './pages/Courses.svelte';
import HomePage from './pages/Home.svelte';
import Navigation from './Navigation.svelte';
import Footer from './Footer.svelte';
import Button from './PrimaryButton.svelte';
import type { TemplateComponents } from '$lib/utils/types/template';
export const components: TemplateComponents = {
navigation: Navigation as unknown as Component,
footer: Footer as unknown as Component,
button: Button as unknown as Component,
coursespage: CoursesPage as unknown as Component,
homepage: HomePage as unknown as Component
};
@@ -0,0 +1,161 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { Course, CourseFilterItem } from '@/utils/types/course';
import EmptyState from '../EmptyState.svelte';
import { COURSE_TYPE } from '@/utils/constants/course';
import type { Page } from '@/utils/types/page';
import { getPageSection } from '@/utils/helpers/page';
import PrimaryButton from '../PrimaryButton.svelte';
import { courses } from '@/utils/stores/course';
import CourseCard from '../CourseCard.svelte';
interface Props {
data: {
page: Page;
sharedPage: Page;
courses: Course[];
};
}
let { data }: Props = $props();
/**
* State
*/
let viewAllCourses = $state(false);
let filter: CourseFilterItem[] = $state([
{
title: 'Self Paced',
type: COURSE_TYPE.PACED,
checked: false
},
{
title: 'Live Sessions',
type: COURSE_TYPE.LIVE,
checked: false
}
]);
let filteredCourses = $state([...data.courses]);
/**
* Constants
*/
const section = $derived({
header: getPageSection(data.page, 'header'),
courses: getPageSection(data.page, 'courses')
});
/**
* Functions
*/
function applyFilter() {
const activeFilters = new Set(
filter
.filter((filterItem) => filterItem.checked)
.map((filterItem) => filterItem.type.toLowerCase())
);
filteredCourses =
activeFilters.size === 0
? data.courses
: data.courses.filter((course) => activeFilters.has(course.type.toLowerCase()));
}
</script>
<main class="bg-[#101720] font-ibm overflow-hidden">
{#if section.header?.show}
<div class="relative h-full p-2 md:p-10">
<div
class="absolute inset-0 bg-cover bg-center"
style="background-image: url('/classroomio-course-img-template.jpg');"
></div>
<div
class="absolute inset-0 bg-gradient-to-r from-[#0233BD99] via-[#00000033] to-[#FFFFFF00]"
></div>
<div class="relative flex items-center justify-center h-full text-white px-4 py-20 md:px-10">
<div class="text-white space-y-6 w-full">
<p class="text-2xl md:text-4xl font-mono font-semibold w-full md:w-[60%] lg:w-[40%]">
{section.header.settings.title}
</p>
<p class="w-full md:w-[60%] lg:w-[40%]">{section.header.settings.subtitle}</p>
<div class="flex items-center gap-2 md:gap-4">
<PrimaryButton
onClick={() => goto(section.header?.settings.action.link)}
class="uppercase bg-blue-900 px-6 py-3 font-semibold hover:bg-blue-900 hover:scale-95 rounded-none"
label={section.header.settings.action.label}
/>
</div>
</div>
</div>
</div>
{/if}
{#if section.courses?.show}
<div class="bg-white py-10 px-2 md:px-8">
<p class="dark:text-white text-3xl text-center font-semibold pb-10 w-full md:w-[70%] mx-auto">
{section.courses.settings.title}
</p>
<div>
{#if $courses.length > 0}
<div class="w-full lg:flex items-start gap-4 lg:ml-[5%]">
<div class="hidden lg:block w-max">
<p class="font-medium mb-2">Filter by</p>
<div class="w-max space-y-2">
{#each filter as item}
<form
class="space-x-2 text-[#3C4043] font-medium border border-[#EAEAEA] bg-[#FDFDFD] rounded-md px-4 py-4"
>
<input
type="checkbox"
checked={item.checked}
name={item.title}
onchange={applyFilter}
/>
<label for={item.title}>{item.title}</label>
</form>
{/each}
</div>
</div>
<section
class="grid place-items-center grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
{#each filteredCourses.slice(0, viewAllCourses ? filteredCourses.length : 3) as courseData}
<CourseCard
className="bg-[#FDFDFD]"
slug={courseData.slug}
title={courseData.title}
description={courseData.description}
cost={courseData.cost}
lessons={courseData.lessonsCount}
currency={courseData.currency}
/>
{/each}
</section>
{#if filteredCourses.length == 0}
<div class="px-4 w-full mx-auto">
<EmptyState />
</div>
{/if}
</div>
{#if filteredCourses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<PrimaryButton
class="uppercase bg-blue-900 p-2"
onClick={() => (viewAllCourses = !viewAllCourses)}
label="VIEW COURSES"
/>
</div>
{/if}
{:else}
<div class="px-4 w-full lg:w-[70%] mx-auto">
<EmptyState />
</div>
{/if}
</div>
</div>
{/if}
</main>
@@ -0,0 +1,195 @@
<script lang="ts">
import { homePage } from '@/utils/stores/pages';
import { getPageSection } from '@/utils/helpers/page';
import PrimaryButton from '../PrimaryButton.svelte';
import { goto } from '$app/navigation';
import defaultBanner from '../assets/classroomio-course-img-template.jpg';
import { courses } from '@/utils/stores/course';
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import TestimonialCard from '../TestimonialCard.svelte';
import Accordion from '../Accordion.svelte';
import testimonial from '../assets/testimonial.svg';
const courseSection = $derived(getPageSection($homePage, 'about'));
const learnSection = $derived(getPageSection($homePage, 'about'));
const testimonialSection = $derived(getPageSection($homePage, 'testimonial'));
const faqSection = $derived(getPageSection($homePage, 'faq'));
const footerNoteSection = $derived(getPageSection($homePage, 'cta'));
let viewAll = $state(false);
</script>
<!-- for some reason the custom color doesnt work for this template -->
<main class="bg-[#101720]">
<!-- hero -->
{#if $homePage}
{@const content = getPageSection($homePage, 'header')}
{#if content?.show}
<section class="flex items-center justify-center py-5 px-5 md:py-14 md:px-14 max-h-full">
<section class="flex items-center justify-between gap-4">
<div class="text-white space-y-6 w-full">
<div class="bg-[#DCFCFFED] py-1 px-3 md:border rounded-sm w-[90%]">
<p class="text-center uppercase font-bold text-sm text-[#0F163F]">
{content.settings?.title}
</p>
</div>
<p class="text-4xl font-bold w-full md:w-[70%] capitalize">
{content.settings?.titleHighlight}
</p>
<p class="w-full md:w-[70%]">
{content.settings?.subtitle}
</p>
<PrimaryButton
class="bg-[#0542CC]"
onClick={() => {
goto(content.settings?.action.link);
}}
label={content.settings?.action.label}
/>
</div>
<div class="hidden rounded-md h-[280px] max-h-[400px] w-5/6 md:w-full lg:block">
<img
style="min-width:280px; min-height:200px"
alt="landing page banner"
src={content.settings?.banner?.image
? content.settings?.banner?.image
: defaultBanner}
class="mt-2 h-full max-h-[400px] w-full max-w-[500px] rounded-md md:mt-0 object-cover"
/>
</div>
</section>
</section>
{/if}
{/if}
<!-- learn -->
{#if learnSection?.show}
<section
id="about"
class="flex items-start justify-center px-5 md:px-14 h-full bg-white py-20 mx-auto"
>
<section class="flex flex-col lg:flex-row gap-8 items-start justify-center">
<div class="w-full lg:w-[60%] space-y-4">
<p class="text-4xl font-bold">{learnSection.settings.title}</p>
<p class="w-full lg:w-[80%] text-base leading-7 text-[#878787]">
{learnSection.settings.subtitle}
</p>
</div>
{#if learnSection.settings.benefits}
<div class="w-full lg:w-fit space-y-8">
<span
class="bg-[#DCFCFF] py-1 px-3 uppercase border border-[#0233BD] rounded-sm text-center font-bold text-base text-[#0233BD]"
>{learnSection.settings.benefits.title}</span
>
<ul class="space-y-6 text-base text-[#696969] font-medium">
{#each learnSection.settings.benefits.list as items}
<li class="border-l border-[#0233BD] pl-3">{items.title}</li>
{/each}
</ul>
</div>
{:else}
<img
src={learnSection.settings.imageUrl}
alt="Our Story"
class="rounded-2xl max-h-[450px]"
/>
{/if}
</section>
</section>
{/if}
<!-- courses -->
{#if courseSection?.show}
<section id="course" class="px-5 pt-10 space-y-10 py-36 h-full bg-white">
<h1 class="text-center text-3xl font-bold mb-4 w-full md:w-[70%] mx-auto">
{courseSection.settings.title}
</h1>
<div class="w-full md:w-[90%] mx-auto">
{#if $courses.length > 0}
<section class="flex flex-wrap items-center w-fit mx-auto gap-4 p-4">
{#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData}
<CourseCard
className="bg-[#FDFDFD]"
slug={courseData.slug}
title={courseData.title}
description={courseData.description}
cost={courseData.cost}
lessons={courseData.lessonsCount}
currency={courseData.currency}
/>
{/each}
</section>
{#if $courses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<PrimaryButton
class="p-2"
onClick={() => (viewAll = !viewAll)}
label="VIEW MORE COURSES"
/>
</div>
{/if}
{:else}
<div class="px-4 w-full lg:w-[70%] mx-auto">
<EmptyState />
</div>
{/if}
</div>
</section>
{/if}
<!-- testimonial -->
{#if testimonialSection?.show}
<section id="testimonial" class="relative pt-10 h-full text-white">
<div class="absolute -top-20 md:-top-32 left-[5%] w-[90%]">
<img src={testimonial} alt="testimonial" class="w-full max-h-[500px] md:max-h-[300px]" />
</div>
<div class="px-5 py-20 md:py-40 space-y-10">
<h1 class="text-center text-3xl font-bold mb-4">Student testimonial</h1>
<div class="w-full md:w-[90%] mx-auto">
<section class="grid place-items-center grid-cols-1 md:grid-cols-3 w-fit mx-auto gap-4">
{#each testimonialSection.settings.list as item}
<TestimonialCard name={item.name} description={item.description} role={item.role} />
{/each}
</section>
</div>
</div>
</section>
{/if}
<!-- faq -->
{#if faqSection?.show}
<section class="px-5 py-20 space-y-10 h-full bg-[#F9F9F9]">
<h1 class="text-center text-3xl font-bold mb-4">{faqSection.settings.title}</h1>
<section class="p-4 space-y-10 w-full md:w-[80%] mx-auto">
{#each faqSection.settings.questions as faq}
<Accordion title={faq.title} content={faq.content} />
{/each}
</section>
</section>
{/if}
<!-- footerNote -->
{#if footerNoteSection?.show}
<section
class="flex flex-col md:flex-row items-center justify-between px-6 lg:px-10 py-20 bg-blue-800"
>
<div class="flex items-center justify-center w-full">
<p
class="text-4xl text-white text-center md:text-start font-bold w-full lg:w-[70%] capitalize"
>
{footerNoteSection.settings.title}
</p>
</div>
<div class="w-full flex items-center justify-center my-5">
<PrimaryButton
onClick={() => goto('/courses')}
class="py-2 bg-white text-blue-700 hover:bg-white"
label={footerNoteSection.settings.buttonLabel}
/>
</div>
</section>
{/if}
</main>
@@ -0,0 +1,43 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import PrimaryButton from './PrimaryButton.svelte';
interface Props {
className?: string;
buttonClass?: string;
slug?: string;
title?: string;
description?: string;
}
let {
className = '',
buttonClass = '',
slug = '',
title = '',
description = ''
}: Props = $props();
function getCourseUrl() {
return `/course/${slug}`;
}
</script>
<div
class="rounded-lg h-[250px] w-full max-w-full md:max-w-[280px] xl:max-w-[300px] p-4 bg-[#E5E7E0] dark:bg-[#232429] border border-[#D0D1C9] {className}"
>
<div class="flex flex-col justify-between gap-4 rounded-lg w-full h-full">
<div class="flex-1 space-y-2">
<p class="text-lg font-bold capitalize">{title}</p>
<p class="text-sm line-clamp-4">
{description}
</p>
</div>
<div class="w-fit">
<PrimaryButton
href={getCourseUrl()}
class="px-8 transition bg-white hover:bg-white text-black"
label="Get started"
/>
</div>
</div>
</div>
@@ -0,0 +1,38 @@
<script lang="ts">
interface Props {
type?: string;
className?: string;
headerClassName?: string;
subtitleClassName?: string;
}
let {
type = 'course',
className = '',
headerClassName = '',
subtitleClassName = ''
}: Props = $props();
</script>
<section
class="{className} w-full border rounded-md py-4 flex items-center justify-center p-2 my-2"
>
<div
class="flex flex-col md:flex-row justify-center items-center text-center md:text-start gap-6 p-2"
>
<img src="/hogempty.svg" alt="empty" />
<div>
{#if type == 'course'}
<h3 class="font-semibold {headerClassName}">"No Course yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like courses have not been added yet.<br /> Kindly check back later
</p>
{:else}
<h3 class="font-semibold {headerClassName}">"No Pathways yet"</h3>
<p class="text-sm md:text-base {subtitleClassName}">
Looks like pathways have not been added yet.<br /> Kindly check back later
</p>
{/if}
</div>
</div>
</section>
@@ -0,0 +1,92 @@
<script lang="ts">
import FaqMark from './static/icons/FaqMark.svelte';
interface Props {
faq: any;
index: any;
}
let { faq, index }: Props = $props();
const background = 'bg-[#FDBAF2]';
const iconColor = 'bg-[#3B82F6]';
let card: HTMLDivElement | undefined = $state();
const handleMouseMove = (event: { clientX: number; clientY: number }) => {
if (card) {
const cardRect = card.getBoundingClientRect();
const cardX = event.clientX - cardRect.left;
const cardY = event.clientY - cardRect.top;
const centerX = cardRect.width / 2;
const centerY = cardRect.height / 2;
const rotateX = (centerY - cardY) / 10;
const rotateY = (cardX - centerX) / 10;
card.style.transform = `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`;
}
};
const handleMouseLeave = () => {
if (card) {
card.style.transform = '';
}
};
</script>
<div
class="z-10 min-w-60 max-w-72 h-72 perspective group"
bind:this={card}
onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
role="none"
>
<div class={`inner-card ${index % 2 === 0 ? 'rotate-3 ' : '-rotate-3 '} `}>
<span class="z-10 absolute w-2 h-2 rounded-full {iconColor} top-4 left-4"></span>
<span class="z-10 absolute w-[2px] h-[2px] rounded-full {iconColor} top-8 left-8"></span>
<span class="z-10 absolute w-2 h-2 rounded-full {iconColor} bottom-4 right-4"></span>
<span class="z-10 absolute w-[2px] h-[2px] rounded-full {iconColor} bottom-8 right-8"></span>
<div
class="front text-black shadow-md flex flex-col items-center justify-center p-4 {background}"
>
<FaqMark color="#3B82F6" />
<h3 class="text-lg font-bold text-center">{faq.title}</h3>
</div>
<div
class="back text-black text-center text-sm shadow-md flex items-center justify-center p-4 {background}"
>
<p class="line-clamp-5">{faq.content}</p>
</div>
</div>
</div>
<style>
.perspective {
perspective: 1000px;
}
.inner-card {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.4s ease;
transform-style: preserve-3d;
}
.group:hover .inner-card {
transform: rotateY(180deg);
}
.front,
.back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.back {
transform: rotateY(180deg);
}
.group:hover .rotate-3 {
transform: rotateY(180deg) rotateZ(3deg);
}
.group:hover .-rotate-3 {
transform: rotateY(180deg) rotateZ(-3deg);
}
</style>
@@ -0,0 +1,52 @@
<script lang="ts">
import { getPageSection } from '$lib/utils/helpers/page';
import { sharedPage } from '$lib/utils/stores/pages';
const content = $derived(getPageSection($sharedPage, 'footer'));
const seo = $derived(getPageSection($sharedPage, 'seo'));
/**
* Constants
*/
</script>
{#if content?.show}
<nav
class="w-full h-40 bg-posthog-background border-t border-posthog-border flex flex-col md:flex-row items-start md:items-center gap-4 md:justify-between py-4 px-6"
>
<div class="logo">
<a href="/" title={`Go to ${seo?.settings.title} Home`} id="logo" data-hveid="8">
<img
src={seo?.settings.logo}
alt={`${seo?.settings.title} logo`}
class="w-24"
data-atf="1"
/>
</a>
</div>
<ul
class="flex flex-col md:flex-row items-start md:items-center gap-4 -md:gap-8 font-semibold capitalize underline"
>
{#if content.settings.twitter}
<a href={content.settings.twitter} target="_blank" title="twitter">Twitter</a>
{/if}
{#if content.settings?.youtube}
<a href={content.settings?.youtube} target="_blank">Youtube</a>{/if}
{#if content.settings.linkedin}
<a href={content.settings.linkedin} target="_blank" title="linkedin">LinkedIn</a>
{/if}
{#if content.settings.facebook}
<a href={content.settings.facebook} target="_blank" title="facebook">Facebook</a>
{/if}
</ul>
<a
href="https://classroomio.com"
class="flex items-center hover:underline text-[#0233BD] dark:text-white gap-1"
target="_blank"
rel="noopener noreferrer"
>
<p class="text-base font-semibold">Built on ClassroomIO</p>
</a>
</nav>
{/if}
@@ -0,0 +1,110 @@
<script lang="ts">
import { goto } from '$app/navigation';
import Close from 'carbon-icons-svelte/lib/Close.svelte';
import Menu from 'carbon-icons-svelte/lib/Menu.svelte';
import { sharedPage } from '$lib/utils/stores/pages';
import { getPageSection } from '$lib/utils/helpers/page';
import type { Section } from '@/utils/types/page';
import PrimaryButton from './PrimaryButton.svelte';
import { ArrowUpRight } from 'carbon-icons-svelte';
let open = $state(false);
const content = $derived(getPageSection($sharedPage, 'navigation'));
const seo: Section | undefined = $derived(getPageSection($sharedPage, 'seo'));
function toggleMenu() {
open = !open;
}
</script>
<nav
class={`relative w-full flex items-center justify-between border-b border-posthog-border py-3 px-6`}
>
<!-- Logo Section -->
<a href="/" class="flex items-center flex-col" title={`${seo?.settings?.title}`} id="logo">
{#if seo?.settings?.logo}
<img
src={seo?.settings?.logo || ''}
alt={`${seo?.settings?.title || ''} logo`}
class="w-24"
/>
{/if}
<h1 class="text-lg font-semibold">University</h1>
</a>
<!-- Mobile Menu Button (Visible only on mobile and when logged in) -->
<button onclick={toggleMenu} class="lg:hidden">
<Menu size={24} />
</button>
<!-- Desktop Navigation Menu (Hidden on mobile) -->
<ul class="hidden lg:flex items-center space-x-8 list-none hover:no-underline">
{#each content?.settings.navItems as navItem}
<a
href={navItem.link}
target={navItem.redirect ? '_blank' : undefined}
class="text-sm text-gray-500 hover:text-black"
>
<li class="hover:no-underline flex items-center gap-1">
{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={16} />
{/if}
</li>
</a>
{/each}
</ul>
<!-- PrimaryButtons for login/signup (Visible only when not logged in) -->
<!-- Mobile Sidebar Menu (Visible only on mobile) -->
<ul
class={`fixed top-0 left-0 pt-10 h-full bg-white dark:bg-black w-full transform hover:no-underline ${
open ? 'translate-y-0' : '-translate-y-full'
} transition-transform duration-300 ease-in-out lg:hidden text-base font-bold text-[#1F2937] dark:text-white list-none cursor-pointer`}
>
<button onclick={toggleMenu} class="absolute right-4 top-4 lg:hidden z-20">
<Close size={24} />
</button>
{#each content?.settings.navItems as navItem}
<li class="py-4 px-6 border-b dark:border-gray-200">
<a href={navItem.link} onclick={toggleMenu} class="flex item-center gap-1"
>{navItem.title}
{#if navItem.redirect}
<ArrowUpRight size={16} />
{/if}</a
>
</li>
{/each}
<div class="border-b dark:border-gray-200 py-4 px-6">
<PrimaryButton
onClick={() => {
goto('/courses');
toggleMenu;
}}
class="bg-[#F7A50180] hover:bg-[#f7a501ad] dark:bg-[#F7A501] hover:no-underline rounded-lg flex items-center py-2 px-3 border w-fit border-[#4A4C524D] font-bold text-black text-base"
label="Explore Course"
/>
</div>
</ul>
<!-- Learn with Me Button (Visible on desktop when logged in) -->
<div class="hidden lg:flex items-center gap-2">
<PrimaryButton onClick={() => goto('/courses')} label="Explore Course" />
</div>
</nav>
<style>
nav {
position: relative;
}
ul {
z-index: 10;
}
</style>
@@ -0,0 +1,33 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
interface Props {
onClick?: () => void;
label?: string;
href?: string | undefined;
class?: string | undefined;
children?: any;
}
const { onClick, children, label, href, class: className, ...restProps }: Props = $props();
</script>
<div class="relative inline-block group">
<span
class="absolute inset-0 -bottom-1 ring-1 ring-posthog bg-posthog rounded-md transition-transform duration-200 ease-in-out"
></span>
<Button
{href}
class={cn(
'bg-white group-hover:bg-white text-black ring-1 ring-posthog font-bold p-2 rounded-md relative z-10 transition-transform duration-200 ease-in-out hover:-translate-y-[2px]',
className
)}
onclick={onClick}
{...restProps}
>
{label}
{#if children}
{@render children?.()}
{/if}
</Button>
</div>
@@ -0,0 +1,15 @@
import type { Component } from 'svelte';
import CoursesPage from './pages/Courses.svelte';
import HomePage from './pages/Home.svelte';
import Navigation from './Navigation.svelte';
import Footer from './Footer.svelte';
import Button from './PrimaryButton.svelte';
import type { TemplateComponents } from '$lib/utils/types/template';
export const components: TemplateComponents = {
navigation: Navigation as unknown as Component,
footer: Footer as unknown as Component,
button: Button as unknown as Component,
coursespage: CoursesPage as unknown as Component,
homepage: HomePage as unknown as Component
};
@@ -0,0 +1,151 @@
<script lang="ts">
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import { Button } from '$lib/components/ui/button';
import { getPageSection } from '$lib/utils/helpers/page';
import type { CourseFilterItem, Course } from '$lib/utils/types/course';
import type { Page } from '$lib/utils/types/page';
import { COURSE_TYPE } from '@/utils/constants/course';
interface Props {
data: {
page: Page;
sharedPage: Page;
courses: Course[];
};
}
let { data }: Props = $props();
/**
* State
*/
let viewAll = $state(false);
let filter: CourseFilterItem[] = $state([
{
title: 'Self Paced',
type: COURSE_TYPE.PACED,
checked: false
},
{
title: 'Live Sessions',
type: COURSE_TYPE.LIVE,
checked: false
}
]);
let filteredCourses = $state([...data.courses]);
/**
* Constants
*/
const section = $derived({
header: getPageSection(data.page, 'header'),
courses: getPageSection(data.page, 'courses')
});
/**
* Functions
*/
function applyFilter() {
const activeFilters = new Set(
filter
.filter((filterItem) => filterItem.checked)
.map((filterItem) => filterItem.type.toLowerCase())
);
filteredCourses =
activeFilters.size === 0
? data.courses
: data.courses.filter((course) => activeFilters.has(course.type.toLowerCase()));
console.log('filtered', filteredCourses);
}
</script>
<main class="bg-posthog-background dark:bg-black dark:text-white overflow-x-hidden">
{#if section.header?.show}
<section class="flex items-center justify-center py-10 px-4 md:px-14 min-h-full">
<section
class="mx-auto text-center px-4 lg:px-10 py-20 space-y-4 bg-[#E5E7E0] dark:bg-[#232429] rounded-md w-full lg:w-[70%]"
>
<div class="flex flex-col items-center gap-8 relative rounded-lg">
<span class="absolute w-2 h-2 rounded-full bg-red-500 -top-14 left-3"></span>
<span class="absolute w-2 h-2 rounded-full bg-white top-10 -left-10"></span>
<span class="absolute w-2 h-2 rounded-full bg-yellow-500 -top-10 -right-3"></span>
<span class="absolute w-2 h-2 rounded-full bg-blue-800 top-14 -right-10"></span>
<p class="text-center text-3xl md:text-5xl font-bold w-full md:w-[90%]">
{section.header.settings.title}
<span class="text-[#F54E00]">{section.header.settings.titleHighlight}</span>
</p>
<p class="text-center w-full text-lg text-[#878787] md:w-[70%]">
{section.header.settings.subtitle}
</p>
</div>
</section>
</section>
{/if}
<section id="course" class="px-2 py-20 h-full lg:px-16">
<div class="relative w-full flex flex-row items-start justify-start lg:gap-2 mt-2">
{#if data.courses.length > 0}
<div class="w-full flex gap-2">
<div class="hidden lg:block min-w-max">
<p class="font-semibold mb-4">Filter by</p>
<div class="w-full space-y-2">
{#each filter as item}
<form
class="space-x-2 text-[#3C4043] dark:text-white font-medium border border-[#EAEAEA] dark:border-[#232429] bg-[#FDFDFD] dark:bg-[#232429] rounded-md px-4 py-4"
>
<input
type="checkbox"
checked={item.checked}
name={item.title}
onchange={applyFilter}
class=" dark:accent-[#EB9D2A] accent-[#F54E00] focus:ring-0"
/>
<label for={item.title} class="whitespace-nowrap">{item.title}</label>
</form>
{/each}
</div>
</div>
<div class="w-full max-h-screen p-4 md:p-8">
<section class="flex flex-wrap items-center justify-center gap-4 w-full">
{#each filteredCourses.slice(0, viewAll ? filteredCourses.length : 3) as courseData}
<CourseCard
slug={courseData.slug}
title={courseData.title}
description={courseData.description}
/>
{/each}
</section>
{#if filteredCourses.length == 0}
<div class="px-4 w-full mx-auto">
<EmptyState />
</div>
{/if}
</div>
{#if filteredCourses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<Button
on:click={() => (viewAll = !viewAll)}
class="dark:text-black bg-transparent uppercase text-black ring-1 ring-[#B17816] dark:bg-[#EB9D2A] hover:scale-95 hover:bg-[#EB9D2A] shadow-[0px_3px_#B17816] font-bold mb-4"
>{viewAll === true ? 'Show less' : 'View more courses'}</Button
>
</div>
{/if}
</div>
{:else}
<div class="px-4 w-full lg:w-[70%] mx-auto">
<EmptyState className="dark:bg-[#232429] dark:border-[#EAEAEA]" type="course" />
</div>
{/if}
</div>
</section>
</main>
@@ -0,0 +1,179 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getPageSection } from '@/utils/helpers/page';
import { homePage } from '@/utils/stores/pages';
import PrimaryButton from '../PrimaryButton.svelte';
import analytics from '../static/analytics.png';
import { Button } from '@/components/ui/button';
import { courses } from '@/utils/stores/course';
import CourseCard from '../CourseCard.svelte';
import EmptyState from '../EmptyState.svelte';
import { CheckmarkFilled } from 'carbon-icons-svelte';
import FaqCard from '../FaqCard.svelte';
const coursesSection = $derived(getPageSection($homePage, 'courses'));
const aboutSection = $derived(getPageSection($homePage, 'about'));
const faqSection = $derived(getPageSection($homePage, 'faq'));
const footerNoteSection = $derived(getPageSection($homePage, 'cta'));
function getCourseUrl() {
return '/courses';
}
let viewAll = $state(false);
</script>
<div class="bg-posthog-background">
<!-- hero -->
{#if $homePage}
{@const content = getPageSection($homePage, 'header')}
{#if content?.show}
<section
class="font-matter flex items-center justify-center py-14 px-10 md:px-14 min-h-full overflow-hidden"
>
<section
class="flex flex-col-reverse md:flex-col text-center items-center gap-5 justify-center"
>
<div class="space-y-6 w-full mb-4">
<p class="text-3xl md:text-5xl font-bold w-full md:w-[80%] lg:w-[70%] mx-auto">
{content.settings.title}
</p>
<p class="w-full text-lg md:w-[80%] lg:w-[70%] mx-auto">
{content.settings.subtitle}
</p>
<PrimaryButton
onClick={() => {
goto(content.settings.action.link);
}}
label={content.settings.action.label}
/>
</div>
<div class=" relative rounded-lg w-full md:w-[800px] md:max-w-[80vw] lg:max-w-[80%] flex">
<span class="absolute w-2 h-2 rounded-full bg-red-500 -top-14 left-3"></span>
<span class="absolute w-2 h-2 rounded-full bg-white top-10 -left-10"></span>
<span class="absolute w-2 h-2 rounded-full bg-yellow-500 -top-10 -right-3"></span>
<span class="absolute w-2 h-2 rounded-full bg-blue-800 top-14 -right-10"></span>
<div class="w-full h-full flex items-center justify-center">
<img alt="Hero Banner" src={analytics} class="w-full rounded-lg object-cover" />
</div>
</div>
</section>
</section>
{/if}
{/if}
<!-- courses -->
{#if coursesSection?.show}
<section id="course" class="px-4 pt-4 pb-20 h-full md:px-10">
<h1 class="text-center text-3xl mb-8 font-bold w-full md:w-[90%] lg:w-[70%] mx-auto">
{coursesSection.settings.title}
<span class="text-[#F54E00] dark:text-[#EB9D2A]"
>{coursesSection.settings.titleHighlight}</span
>
</h1>
{#if $courses.length > 0}
<section class="flex flex-wrap items-center justify-center mx-auto gap-4">
{#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData}
<CourseCard
slug={courseData.slug}
title={courseData.title}
description={courseData.description}
/>
{/each}
</section>
{#if $courses.length > 3}
<div class="w-full flex items-center justify-center my-5">
<Button
on:click={() => (viewAll = !viewAll)}
class="dark:text-black bg-white text-black capitalize ring-1 ring-[#B17816] dark:bg-[#EB9D2A] hover:scale-95 hover:bg-[#EB9D2A] shadow-[0px_3px_#B17816] font-bold mb-4"
>
{viewAll === true ? 'Show less' : 'View more courses'}
</Button>
</div>
{/if}
{:else}
<div class="w-full lg:w-[70%] mx-auto">
<EmptyState className="dark:bg-[#232429] dark:border-[#EAEAEA]" />
</div>
{/if}
</section>
{/if}
<!-- about -->
{#if aboutSection?.show}
<section
id="about"
class="px-4 md:px-10 pt-4 pb-20 h-full w-full mx-auto md:w-[90%] lg:w-[80%]"
>
<h1 class="text-start text-3xl mb-2 font-bold md:w-[50%]">{aboutSection.settings.title}</h1>
<p class="text-start text-[#3D3D3D] dark:text-white text-sm font-semibold md:w-[70%] mb-8">
{aboutSection.settings.subtitle}
</p>
<div>
<p class="text-xl font-bold capitalize mb-6">{aboutSection.settings.benefits.title}</p>
<section class="grid place-items-center grid-cols-1 md:grid-cols-2 gap-4">
{#each aboutSection.settings.benefits.list as item}
<div
class="flex flex-col gap-2 p-4 bg-[#E5E7E0] dark:bg-[#232429] border border-[#D0D1C9] h-[150px] max-h-[200px] w-full max-w-full md:max-w-[320px] lg:max-w-[450px] rounded-lg overflow-hidden"
>
<span class="flex items-center gap-2">
<CheckmarkFilled size={24} class=" fill-[#F54E00] dark:fill-[#EB9D2A]" />
<p class="text-lg font-bold capitalize line-clamp-1">{item.title}</p>
</span>
<p class="text-sm line-clamp-4">{item.subtitle}</p>
</div>
{/each}
</section>
</div>
</section>
{/if}
<!-- faq -->
{#if faqSection?.show}
<div id="faq">
<h1 class="text-center text-3xl mb-8 font-bold">{faqSection.settings.title}</h1>
<div class="flex flex-wrap items-center justify-center mx-auto gap-8 p-4">
{#each faqSection.settings.questions as faq, index}
<FaqCard {index} {faq} />
{/each}
</div>
</div>
{/if}
<!-- footerNote -->
{#if footerNoteSection?.show}
<section class="w-full h-full p-4 md:p-20">
<section
class="mx-auto w-fit md:w-[80%] flex flex-col items-center justify-between px-2 lg:px-5 py-20 space-y-4 bg-[#E5E7E0] dark:bg-[#232429] rounded-md"
>
<div class="flex items-center justify-center w-full">
<p
class="text-4xl text-center xl:text-start font-bold w-full mx-auto lg:w-[90%] capitalize"
>
{footerNoteSection.settings.title}
<span class="text-[#F54E00]">{footerNoteSection.settings.titleHighlight}</span>
</p>
</div>
<div
class="w-full lg:w-[90%] flex flex-col lg:flex-row items-center lg:items-end gap-4 lg:gap-0 lg:justify-between"
>
<p class="text-center lg:text-start text-sm font-semibold w-full lg:w-[50%]">
{footerNoteSection.settings.subtitle}
</p>
<div class="w-fit">
<PrimaryButton
href={getCourseUrl()}
class="px-8 transition bg-white hover:bg-white text-black"
label={footerNoteSection.settings.buttonLabel}
/>
</div>
</div>
</section>
</section>
{/if}
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,14 @@
<svg width="168" height="191" viewBox="0 0 168 191" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M147.244 71.4805H84.1107L79.0508 59.1652C78.2721 57.396 76.9873 55.8967 75.3582 54.8562C73.729 53.8157 71.8284 53.2806 69.8957 53.3183H34.2103C32.2776 53.2806 30.377 53.8157 28.7479 54.8562C27.1187 55.8967 25.8339 57.396 25.0552 59.1652L21.611 70.3265C20.9729 72.4035 20.6439 74.563 20.6345 76.7357L20.0072 158.404C19.9899 160.913 20.9692 163.327 22.7302 165.114C24.4912 166.902 26.8898 167.917 29.399 167.938H156.635V81.0203C156.654 78.51 155.676 76.095 153.915 74.3061C152.153 72.5172 149.754 71.5009 147.244 71.4805Z" fill="#F54E00"/>
<path d="M147.244 71.4805H84.1108L79.0509 59.1652C78.2723 57.396 76.9874 55.8967 75.3583 54.8562C73.7291 53.8157 71.8285 53.2806 69.8958 53.3183H34.2105C32.9474 53.3178 31.6946 53.5443 30.5117 53.987H65.2561C69.3218 53.987 74.8847 56.3187 76.3287 59.834L81.3886 72.1552H142.604C147.788 72.1552 153.913 76.422 153.913 81.689V167.92H156.636V81.0203C156.655 78.51 155.676 76.095 153.915 74.3061C152.154 72.5172 149.754 71.5009 147.244 71.4805Z" fill="white"/>
<path d="M147.244 71.4805H84.1107L79.0508 59.1652C78.2721 57.396 76.9873 55.8967 75.3582 54.8562C73.729 53.8157 71.8284 53.2806 69.8957 53.3183H34.2103C32.2776 53.2806 30.377 53.8157 28.7479 54.8562C27.1187 55.8967 25.8339 57.396 25.0552 59.1652L21.611 70.3265C20.9729 72.4035 20.6439 74.563 20.6345 76.7357L20.0072 158.404C19.9899 160.913 20.9692 163.327 22.7302 165.114C24.4912 166.902 26.8898 167.917 29.399 167.938H156.635V81.0203C156.654 78.51 155.676 76.095 153.915 74.3061C152.153 72.5172 149.754 71.5009 147.244 71.4805Z" stroke="#F54E00" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M30.198 167.917H156.636L134.964 96.9016C134.411 95.0497 133.277 93.4248 131.73 92.2665C130.182 91.1082 128.304 90.478 126.371 90.4688H10.5089C9.08789 90.485 7.69037 90.8334 6.42798 91.4861C5.16559 92.1387 4.07331 93.0776 3.23841 94.2276C2.40351 95.3776 1.84911 96.7069 1.6195 98.1093C1.38989 99.5118 1.49143 100.948 1.91601 102.305L19.4924 159.898C20.1831 162.205 21.596 164.229 23.5232 165.673C25.4504 167.116 27.7902 167.903 30.198 167.917Z" fill="#EB9D2A"/>
<path d="M7.92288 93.6171H123.791C125.723 93.6273 127.602 94.2579 129.149 95.416C130.696 96.5742 131.83 98.1986 132.384 100.05L153.097 167.917H156.648L134.976 96.9016C134.423 95.0497 133.289 93.4248 131.742 92.2665C130.194 91.1082 128.316 90.478 126.383 90.4688H10.509C8.83774 90.4722 7.20093 90.9443 5.7845 91.8314C4.36807 92.7185 3.2288 93.9851 2.49609 95.4872C4.0445 94.2736 5.95555 93.6151 7.92288 93.6171Z" fill="white"/>
<path d="M30.198 167.917H156.636L134.964 96.9016C134.411 95.0497 133.277 93.4248 131.73 92.2665C130.182 91.1082 128.304 90.478 126.371 90.4688H10.5089C9.08789 90.485 7.69037 90.8334 6.42798 91.4861C5.16559 92.1387 4.07331 93.0776 3.23841 94.2276C2.40351 95.3776 1.84911 96.7069 1.6195 98.1093C1.38989 99.5118 1.49143 100.948 1.91601 102.305L19.4924 159.898C20.1831 162.205 21.596 164.229 23.5232 165.673C25.4504 167.116 27.7902 167.903 30.198 167.917Z" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M71.8726 145.165C79.5337 145.165 85.7443 138.955 85.7443 131.294C85.7443 123.632 79.5337 117.422 71.8726 117.422C64.2114 117.422 58.0009 123.632 58.0009 131.294C58.0009 138.955 64.2114 145.165 71.8726 145.165Z" fill="#263238" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M65.1851 137.982L78.5656 124.578" stroke="#F2F2F2" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M65.1851 124.578L78.5656 137.959" stroke="#F2F2F2" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M164.482 9.00001H126.915C126.274 9.00001 125.66 9.2544 125.207 9.70721C124.754 10.16 124.5 10.7742 124.5 11.4145V40.1286C124.5 40.7689 124.754 41.3831 125.207 41.8359C125.66 42.2887 126.274 42.5431 126.915 42.5431H136.235V51.7396C136.224 51.9767 136.285 52.2116 136.409 52.4139C136.533 52.6162 136.714 52.7765 136.931 52.8741C137.147 52.9718 137.388 53.0021 137.621 52.9613C137.855 52.9204 138.071 52.8103 138.242 52.6451L149.628 42.5431H164.482C165.122 42.5431 165.736 42.2887 166.189 41.8359C166.642 41.3831 166.896 40.7689 166.896 40.1286V11.4145C166.897 11.0972 166.835 10.7829 166.714 10.4896C166.593 10.1963 166.415 9.92983 166.191 9.70547C165.967 9.4811 165.7 9.30328 165.407 9.18221C165.113 9.06115 164.799 8.99923 164.482 9.00001Z" fill="white" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M145.219 29.8483C144.977 29.8525 144.739 29.7971 144.524 29.687C144.309 29.5768 144.125 29.4154 143.987 29.217C143.85 29.0186 143.763 28.7893 143.735 28.5495C143.708 28.3096 143.74 28.0667 143.828 27.8421C144.078 27.2749 144.411 26.7482 144.816 26.2798C145.206 25.8248 145.627 25.3977 146.077 25.0015C146.521 24.6109 146.941 24.2203 147.337 23.8179C147.712 23.4683 148.039 23.0702 148.308 22.6343C148.618 22.0995 148.738 21.4754 148.647 20.8638C148.557 20.2523 148.262 19.6894 147.811 19.267C146.94 18.5913 145.851 18.2586 144.751 18.3319C143.743 18.3061 142.741 18.5084 141.822 18.9237C141.361 19.1388 140.933 19.4177 140.55 19.7522C140.263 19.9972 139.904 20.1414 139.528 20.1628C139.152 20.1842 138.779 20.0815 138.466 19.8706C138.239 19.7172 138.05 19.5131 137.914 19.2743C137.779 19.0356 137.7 18.7686 137.685 18.4945C137.671 18.2203 137.719 17.9465 137.828 17.6944C137.937 17.4424 138.103 17.219 138.313 17.0418C138.859 16.6036 139.455 16.2302 140.088 15.9292C141.654 15.2226 143.359 14.8768 145.077 14.9172C146.448 14.8869 147.813 15.1137 149.101 15.586C150.177 15.9803 151.119 16.6719 151.817 17.5803C152.47 18.4556 152.81 19.5248 152.782 20.6163C152.802 21.377 152.676 22.1345 152.409 22.8473C152.173 23.4382 151.841 23.986 151.427 24.4689C151.022 24.9353 150.585 25.3723 150.119 25.7767C149.646 26.1851 149.213 26.5816 148.805 26.9603C148.412 27.3411 148.077 27.7778 147.811 28.2564C147.709 28.4443 147.628 28.6427 147.568 28.8482C147.475 29.1491 147.288 29.4125 147.035 29.5998C146.781 29.7871 146.475 29.8887 146.16 29.8897L145.219 29.8483ZM145.444 37.299C145.121 37.3054 144.801 37.247 144.501 37.1274C144.201 37.0078 143.929 36.8293 143.699 36.6026C143.47 36.3759 143.288 36.1054 143.165 35.8073C143.042 35.5091 142.979 35.1893 142.982 34.8667C142.976 34.551 143.035 34.2374 143.155 33.9453C143.275 33.6532 143.454 33.3887 143.68 33.1683C144.165 32.7241 144.798 32.4777 145.456 32.4777C146.113 32.4777 146.746 32.7241 147.231 33.1683C147.454 33.3902 147.63 33.6551 147.748 33.9471C147.866 34.239 147.924 34.5519 147.917 34.8667C147.923 35.1926 147.861 35.5161 147.737 35.8174C147.613 36.1187 147.428 36.3915 147.195 36.6192C146.962 36.8468 146.685 37.0245 146.381 37.1414C146.076 37.2583 145.752 37.3119 145.426 37.299H145.444Z" fill="#EC644E" stroke="#263238" stroke-width="1.47949" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M89.5286 190.495C132.002 190.495 166.433 188.569 166.433 186.193C166.433 183.817 132.002 181.891 89.5286 181.891C47.0557 181.891 12.6245 183.817 12.6245 186.193C12.6245 188.569 47.0557 190.495 89.5286 190.495Z" fill="#8192C3" fill-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 431 KiB

Some files were not shown because too many files have changed in this diff Show More