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 <chifez1@gmail.com>
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
+26
View File
@@ -0,0 +1,26 @@
name: Publish to NPM
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v4
- name: Install dependencies & publish
working-directory: packages/course-app
run: |
pnpm i
pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+1 -1
View File
@@ -1 +1 @@
v18.16.0
v18.17.0
+3 -2
View File
@@ -3,11 +3,12 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": ["*.svelte"],
"files": "*.svelte",
"options": {
"svelteBracketNewLine": true
"parser": "svelte"
}
}
]
+3
View File
@@ -26,6 +26,9 @@
"vite": "^5.0.0"
},
"type": "module",
"engines": {
"node": "^18.17.0"
},
"dependencies": {
"html-to-image": "^1.11.11",
"just-extend": "^6.2.0",
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+4
View File
@@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
+15
View File
@@ -0,0 +1,15 @@
{
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+17
View File
@@ -0,0 +1,17 @@
# Course App
Create a course site for your Product. With our prebuilt templates, you can create a fully functional course site in minutes.
## Tech Stack
- SvelteKit
- TailwindCSS
- Mdx - Courses are written in markdown
## How to use the package
```bash
pnpm dlx @cio/course-app mysaas-university
```
This will create a folder called `mysaas-university` with a fully functional course site.
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "course-app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"clsx": "^2.1.1",
"mode-watcher": "^0.5.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"vite": "^6.0.0",
"vitest": "^2.0.4"
},
"dependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"lucide-svelte": "^0.468.0",
"svelte-inview": "^4.0.4",
"svelte-motion": "^0.12.2"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+78
View File
@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+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 {};
+45
View File
@@ -0,0 +1,45 @@
<!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" />
<title>Courseapp by ClassroomIO –&nbsp;Create your SAAS Academy in Minutes</title>
<meta name="description" content="Courseapp" />
<meta name="application-name" content="Courseapp" />
<meta name="apple-mobile-web-app-title" content="Courseapp" />
<meta name="theme-color" content="#ffffff" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://www.courseapp.oncws.com/" />
<meta property="og:image" itemprop="image" content="https://cdn.courseapp.oncws.com/courseapp-og-image.png" />
<meta property="og:title" content="Courseapp by ClassroomIO –&nbsp;Create your SAAS Academy in Minutes" />
<meta property="og:description"
content="Series of templates available for you to clone, customise and launch your SAAS Academy. Scale your customer education effort today." />
<meta property="og:type" content="website" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1920" />
<meta property="og:image:height" content="1080" />
<meta property="og:image:secure_url" itemprop="image"
content="https://cdn.courseapp.oncws.com/courseapp-og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="courseapp.oncws.com" />
<meta property="twitter:url" content="https://www.courseapp.oncws.com/" />
<meta name="twitter:title" content="Courseapp by ClassroomIO –&nbsp;Create your SAAS Academy in Minutes" />
<meta name="twitter:description"
content="Series of templates available for you to clone, customise and launch your SAAS Academy. Scale your customer education effort today." />
<meta name="twitter:image" content="https://cdn.courseapp.oncws.com/courseapp-og-image.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
@@ -0,0 +1,48 @@
<script lang="ts">
import { Motion, AnimatePresence, useAnimation } from 'svelte-motion';
import { inview } from 'svelte-inview';
import { cn } from '$lib/utils';
let {
duration = 0.4,
delay = 0,
yOffset = 6,
inViewMargin = '-50px',
blur = '6px',
once = false,
class: className = ''
} = $props();
let id = $state(crypto.randomUUID().slice(0, 8));
let isInView = $state('hidden');
let defaultVariants = {
hidden: { opacity: 0, y: yOffset, filter: `blur(${blur})` },
visible: { opacity: 1, y: 0, filter: `blur(0px)` }
};
</script>
<AnimatePresence let:item list={[{ key: id }]}>
<Motion
initial="hidden"
animate={isInView}
exit="hidden"
variants={defaultVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: 'easeOut'
}}
let:motion
>
<div
use:inview={{ rootMargin: inViewMargin, unobserveOnEnter: once }}
use:motion
on:inview_change={({ detail }) => {
isInView = detail.inView ? 'visible' : 'hidden';
}}
class={cn(className)}
>
<slot>Default</slot>
</div>
</Motion>
</AnimatePresence>
@@ -0,0 +1 @@
export { default as BlurFade } from './blurfade.svelte';
@@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from "$lib/utils";
import { Motion } from "svelte-motion";
export let variant: {
hidden: { filter: string; opacity: number };
visible: { filter: string; opacity: number };
} = {
hidden: { filter: "blur(10px)", opacity: 0 },
visible: { filter: "blur(0px)", opacity: 1 },
};
export let duration: number = 1;
let className = "font-display text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]";
export { className as class };
let defaultVariants = {
hidden: { filter: "blur(10px)", opacity: 0 },
visible: { filter: "blur(0px)", opacity: 1 },
};
let combinedVariants = variant || defaultVariants;
</script>
<Motion
initial="hidden"
animate="visible"
transition={{ duration }}
variants={combinedVariants}
let:motion
>
<h1
class={cn(className)}
use:motion
>
<slot />
</h1>
</Motion>
@@ -0,0 +1 @@
export { default as BlurIn } from './blurin.svelte';
@@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from '$lib/utils';
let { children, className } = $props();
// let cardContent = {
// title: 'Svelte is Vibe',
// description: 'Svelte is a fun way to build web applications.'
// };
</script>
<div class={cn('p-4 text-left', className)}>
<!-- <h3 class="mb-1 text-lg font-bold text-gray-900 dark:text-gray-100">
{cardContent.title}
</h3>
<p class="text-gray-700 dark:text-gray-300">
{cardContent.description}
</p> -->
{@render children()}
</div>
@@ -0,0 +1,30 @@
<script lang="ts">
import { cn } from '$lib/utils';
import Ellipses from './ellipses.svelte';
type Props = {
className?: string;
};
let { className }: Props = $props();
</script>
<div
class={cn(
'relative mx-auto w-full border border-dashed border-zinc-300 px-8 shadow-sm sm:px-6 md:px-8 dark:border-zinc-800 dark:bg-[#09090B]',
className
)}
>
<div
class="absolute left-0 top-4 -z-0 h-px w-full bg-zinc-400 sm:top-6 md:top-8 dark:bg-zinc-700"
></div>
<div
class="absolute bottom-4 left-0 z-0 h-px w-full bg-zinc-400 sm:bottom-6 md:bottom-8 dark:bg-zinc-700"
></div>
<div class="relative w-full border-x border-zinc-400 dark:border-zinc-700">
<Ellipses />
<div class="relative z-20 mx-auto py-8">
<slot>Default</slot>
</div>
</div>
</div>
@@ -0,0 +1,13 @@
<script>
let sharedClass =
'rounded-full outline outline-8 dark:outline-gray-950 sm:my-6 md:my-8 size-1 my-4 outline-gray-50 bg-green-400';
</script>
<div class="absolute z-0 grid h-full w-full items-center gap-8 lg:grid-cols-2">
<section class="absolute z-0 grid h-full w-full grid-cols-2 place-content-between">
<div class="{sharedClass} -mx-[2.5px]"></div>
<div class="{sharedClass} -mx-[2px] place-self-end"></div>
<div class="{sharedClass} -mx-[2.5px]"></div>
<div class="{sharedClass} -mx-[2px] place-self-end"></div>
</section>
</div>
@@ -0,0 +1,2 @@
export { default as CardContainer } from './container.svelte';
export { default as CardBody } from './body.svelte';
@@ -0,0 +1,42 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { AnimatePresence, Motion } from 'svelte-motion';
let className: any = 'Gradual Spacing';
export { className as class };
export let words = 'Gradual Spacing';
export let duration = 0.5;
export let delayMultiple = 0.04;
export let framerProps = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
};
let wordsspilit = words.split('');
</script>
<div class="flex justify-center space-x-1">
<AnimatePresence let:item list={[{ key: wordsspilit }]}>
{#each wordsspilit as char, i}
<Motion
initial="hidden"
animate="visible"
exit="hidden"
variants={framerProps}
transition={{
duration: duration,
delay: i * delayMultiple
}}
let:motion
>
<span use:motion class={cn('drop-shadow-sm', className)}>
{#if char === ' '}
<span>&nbsp;</span>
{:else}
{char}
{/if}
</span>
</Motion>
{/each}
</AnimatePresence>
</div>
@@ -0,0 +1 @@
export { default as GradualSpacing } from './gradual-spacing.svelte';
@@ -0,0 +1 @@
export { default as MagicCard } from './magic-card.svelte';
@@ -0,0 +1,72 @@
<script lang="ts">
import { Motion, useMotionValue, useMotionTemplate } from 'svelte-motion';
import { onMount } from 'svelte';
import { cn } from '$lib/utils';
export let gradientSize: number = 200;
export let gradientColor: string = '#262626';
export let gradientOpacity: number = 0.8;
let className: string = '';
export { className as class };
let gradSize = useMotionValue(gradientSize);
let gradColor = useMotionValue(gradientColor);
let mouseX = useMotionValue(-gradientSize);
let mouseY = useMotionValue(-gradientSize);
function handleMouseMove(e: MouseEvent) {
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
mouseX.set(e.clientX - rect.left);
mouseY.set(e.clientY - rect.top);
}
function handleMouseLeave() {
mouseX.set(-gradientSize);
mouseY.set(-gradientSize);
}
onMount(() => {
mouseX.set(-gradientSize);
mouseY.set(-gradientSize);
});
let bg = useMotionTemplate`radial-gradient(${gradSize}px circle at ${mouseX}px ${mouseY}px, ${gradColor}, transparent 100%)`;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- I have added py-4 in below code, you can customize the component as per needs -->
<div
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
class={cn(
'group relative flex size-full justify-center overflow-hidden rounded-xl border bg-neutral-100 py-4 text-black dark:bg-neutral-900 dark:text-white',
className
)}
>
<div class="relative z-10">
<!-- Default -->
<slot>
<div class="flex h-full items-center justify-center text-center">
<p class="text-2xl">Magic Card</p>
</div>
</slot>
</div>
<Motion
style={{
background: bg,
opacity: gradientOpacity
}}
let:motion
>
<div
use:motion
class="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
/>
</Motion>
</div>
<style>
.size-full {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1 @@
export { default as Meteors } from './meteors.svelte';
@@ -0,0 +1,36 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { onMount } from 'svelte';
export let number = 10;
let meteorStyles: any = [];
let changeMeteors = (num: number) => {
meteorStyles = [];
const styles = [...new Array(num)].map(() => ({
top: -20,
left: Math.floor(Math.random() * 700) + 'px',
animationDelay: Math.random() * 1 + 0.2 + 's',
animationDuration: Math.floor(Math.random() * 8 + 2.9) + 's'
}));
meteorStyles = styles;
};
onMount(() => {
changeMeteors(number);
});
// $: changeMeteors(number);
</script>
{#each meteorStyles as style, idx}
<span
id="meteor-{idx + 1}"
class={cn(
'animate-meteor pointer-events-none absolute left-1/2 top-1/2 size-[2.4px] rotate-[215deg] rounded-full bg-slate-500 shadow-[0_0_0_1px_#ffffff10]'
)}
style="top: {style.top}px; left: {style.left}; animation-delay: {style.animationDelay}; animation-duration: {style.animationDuration};"
>
<!-- Meteor Tail -->
<div
class="pointer-events-none absolute top-1/2 -z-10 h-px w-[50px] -translate-y-1/2 bg-gradient-to-r from-slate-500 via-blue-600/30 to-transparent"
/>
</span>
{/each}
@@ -0,0 +1 @@
export { default as ShimmerButton } from './shimmerbutton.svelte';
@@ -0,0 +1,62 @@
<script lang="ts">
import { cn } from '$lib/utils';
export let shimmerColor = '#ffffff';
export let shimmerSize = '0.05em';
export let shimmerDuration = '3s';
export let borderRadius = '100px';
export let background = 'rgba(0, 0, 0, 1)';
export let onClick: () => void;
let className: any = '';
export { className as class };
</script>
<button
style:--spread="90deg"
style:--shimmer-color={shimmerColor}
style:--radius={borderRadius}
style:--speed={shimmerDuration}
style:--cut={shimmerSize}
style:--bg={background}
class={cn(
'group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden whitespace-nowrap border border-white/10 px-6 py-3 text-white [background:var(--bg)] [border-radius:var(--radius)] dark:text-white',
'transform-gpu transition-transform duration-300 ease-in-out active:translate-y-[1px]',
className
)}
on:click={onClick}
>
<div class={cn('-z-30 blur-[2px]', 'absolute inset-0 overflow-visible [container-type:size]')}>
<div
class="animate-magicslide absolute inset-0 h-[100cqh] [aspect-ratio:1] [border-radius:0] [mask:none]"
>
<!-- spark before -->
<div
class="animate-spin-around absolute inset-[-100%] w-auto rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))] [translate:0_0]"
/>
</div>
</div>
<slot>Shimmer Button</slot>
<!-- Highlight -->
<div
class={cn(
'insert-0 absolute h-full w-full',
'rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]',
// transition
'transform-gpu transition-all duration-300 ease-in-out',
// on hover
'group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]',
// on click
'group-active:shadow-[inset_0_-10px_10px_#ffffff3f]'
)}
></div>
<!-- backdrop -->
<div
class={cn(
'absolute -z-20 [background:var(--bg)] [border-radius:var(--radius)] [inset:var(--cut)]'
)}
></div>
</button>
@@ -0,0 +1 @@
export { default as ShinnyText } from './shinny-text.svelte';
@@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from '$lib/utils';
export let shimmerWidth = 100;
let className: any = '';
export { className as class };
</script>
<p
style:--shimmer-width="{shimmerWidth}px"
class={cn(
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50 ',
// Shimmer effect
'animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
// Shimmer gradient
'bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80',
className
)}
>
<slot>Shimmer Animation</slot>
</p>
@@ -0,0 +1,9 @@
<script lang="ts">
import { cn } from '$lib/utils';
let { children, className } = $props();
</script>
<div class={cn('text-left', className)}>
{@render children()}
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { cn } from '$lib/utils';
let className: any = '';
export { className as class };
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
width={24}
height={24}
stroke-width="1"
stroke-linecap="round"
stroke="currentColor"
class={cn('absolute text-black dark:text-white', className)}
{...$$restProps}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
@@ -0,0 +1,2 @@
export { default as SquareCardBody } from './body.svelte';
export { default as SquareCardIcon } from './icon.svelte';
@@ -0,0 +1 @@
export { default as Navigation } from './navigation.svelte';
@@ -0,0 +1,106 @@
<script lang="ts">
import { page } from '$app/stores';
const getClass = (active: boolean) => (active ? 'text-foreground' : 'text-foreground/60');
let activeClass = $derived({
template: getClass($page.url.href.includes('#templates')),
setup: getClass($page.url.href.includes('#setup'))
});
</script>
<header
class="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur"
>
<p class="bg-blue-700 py-1.5 text-center text-sm font-medium text-white">
brought to you by
<a
href="https://git.new/class"
target="_blank"
class="inline-flex items-center font-semibold underline-offset-2 hover:underline"
>classroomio.com
<svg
width="24"
height="24"
stroke="currentColor"
stroke-width="2"
class="lucide-icon lucide lucide-arrow-up-right ml-0.5 h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path>
</svg>
</a>
</p>
<div class="container flex h-14 max-w-screen-2xl items-center">
<div class="mr-4 flex">
<a href="/" class="mr-6 flex items-center space-x-2">
<img src="/logo-512.png" alt="Course App" class="h-8 w-8" />
{' '}
<span class="inline-block font-bold">courseapp</span>
</a>{' '}
<nav class="hidden items-center gap-6 text-sm md:flex">
<a
href="#templates"
class="hover:text-foreground/80 {activeClass.template} transition-colors"
>
Templates
</a>{' '}
<a href="#setup" class="hover:text-foreground/80 {activeClass.setup} transition-colors">
Setup
</a>{' '}
<!-- <a
href="https://github.com/classroomio/classroomio"
class="hover:text-foreground/80 text-foreground/60 transition-colors"
>
Github
</a>{' '} -->
</nav>
</div>
<div class="flex flex-1 items-center justify-end space-x-2">
<nav class="flex items-center">
<a
href="https://github.com/classroomio/classroomio"
target="_blank"
rel="noopener noreferrer"
>
<div
class="focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground inline-flex h-8 w-8 items-center justify-center whitespace-nowrap rounded-md px-0 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50"
>
<svg viewBox="0 0 438.549 438.549" class="h-4 w-4">
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>{' '}
<span class="sr-only" data-svelte-h="svelte-11yow7k"> GitHub </span>
</div>
</a>{' '}
<a href="https://twitter.com/classroomio" target="_blank" rel="noreferrer">
<div
class="focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground inline-flex h-8 w-8 items-center justify-center whitespace-nowrap rounded-md px-0 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50"
>
<svg
height="23"
viewBox="0 0 1200 1227"
width="23"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
class="h-3 w-3 fill-current"
>
<path
d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"
></path>
</svg>{' '}
<span class="sr-only" data-svelte-h="svelte-13ytjzu">
X (formerly known as Twitter)
</span>
</div>
</a>{' '}
</nav>
</div>
</div>
</header>
@@ -0,0 +1 @@
export { default as TemplateCard } from './template-card.svelte';
@@ -0,0 +1,38 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import ArrowUpRight from 'lucide-svelte/icons/arrow-up-right';
interface Props {
template: {
name: string;
image: string;
url: string;
github: string;
};
}
let { template }: Props = $props();
</script>
<div class="mb-5 w-full max-w-[340px]">
<Button
href={template.url}
variant="link"
target="_blank"
rel="noopener noreferrer"
class="border-stroke-primary relative h-80 min-w-60 overflow-hidden rounded-md border border-solid p-0 transition-all duration-500 lg:h-96"
>
<span
class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-all duration-500 hover:opacity-80"
>
<ArrowUpRight class="h-14 w-14 text-white" />
</span>
<img
src={template.image}
class="h-full w-full object-contain hover:opacity-10"
alt={template.name}
/>
</Button>
<div>
<h3 class="text-md font-bold">{template.name}</h3>
</div>
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>
@@ -0,0 +1,50 @@
import type { Button as ButtonPrimitive } from "bits-ui";
import { type VariantProps, tv } from "tailwind-variants";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};
@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};
@@ -0,0 +1,22 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = SeparatorPrimitive.Props;
let className: $$Props["class"] = undefined;
export let orientation: $$Props["orientation"] = "horizontal";
export let decorative: $$Props["decorative"] = undefined;
export { className as class };
</script>
<SeparatorPrimitive.Root
class={cn(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className
)}
{orientation}
{decorative}
{...$$restProps}
/>
@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";
@@ -0,0 +1,20 @@
<script lang="ts">
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher";
type $$Props = SonnerProps;
</script>
<Sonner
theme={$mode}
class="toaster group"
toastOptions={{
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...$$restProps}
/>
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+62
View File
@@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};
+14
View File
@@ -0,0 +1,14 @@
<script lang="ts">
import { Toaster } from '$lib/components/ui/sonner';
import { Navigation } from '$lib/components/navigation';
import '../app.css';
let { children } = $props();
</script>
<Toaster />
<Navigation />
{@render children()}
+185
View File
@@ -0,0 +1,185 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { CardBody, CardContainer } from '$lib/components/animation/card';
import { BlurFade } from '$lib/components/animation/blurfade';
import { BlurIn } from '$lib/components/animation/blurin';
import { Button } from '$lib/components/ui/button';
import { TemplateCard } from '$lib/components/template-card';
import { Meteors } from '$lib/components/animation/meteors';
import { Separator } from '$lib/components/ui/separator';
import { ShinnyText } from '$lib/components/animation/shinny-text';
import { ShimmerButton } from '$lib/components/animation/shimmerbutton';
import Copy from 'lucide-svelte/icons/copy';
import { cn } from '$lib/utils';
const templates = [
{
name: 'Cal',
image: 'https://cdn.courseapp.oncws.com/templates/cal-template.png',
url: 'https://cal.courseapp.oncws.com',
github: 'https://github.com/classroomio/classroomio'
},
{
name: 'PostHog',
image: 'https://cdn.courseapp.oncws.com/templates/posthog-template.png',
url: 'https://posthog.courseapp.oncws.com',
github: 'https://github.com/classroomio/classroomio'
},
{
name: 'Classic',
image: 'https://cdn.courseapp.oncws.com/templates/classic-template.png',
url: 'https://classic.courseapp.oncws.com',
github: 'https://github.com/classroomio/classroomio'
},
{
name: 'Examprep',
image: 'https://cdn.courseapp.oncws.com/templates/examprep-template.png',
url: 'https://examprep.courseapp.oncws.com',
github: 'https://github.com/classroomio/classroomio'
},
{
name: 'Minimal',
image: 'https://cdn.courseapp.oncws.com/templates/minimal.png',
url: 'https://minimal.courseapp.oncws.com',
github: 'https://github.com/classroomio/classroomio'
}
];
</script>
<!-- Hero -->
<section class="my-5 w-full md:mb-20 md:mt-0">
<div class="">
<CardContainer className="md:px-20">
<CardBody
className="max-w-4xl min-h-[50vh] flex flex-col justify-center px-30 gap-2 md:gap-5"
>
<BlurIn
class="pointer-events-none flex max-w-2xl flex-col items-start whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-4xl font-semibold leading-none tracking-tighter text-transparent md:mb-4 md:block md:text-6xl"
>
Create a <span class="font-mono italic text-blue-700">UNIVERSITY</span>
<span>Site for your SAAS in</span>
<span class="ml-1 font-mono italic text-blue-700">MINUTES</span>
</BlurIn>
<BlurFade delay={0.25 * 2}>
<p class="text-md">
With our prebuilt templates, you can setup a fully functional customer educational site
for your product in minutes.<br />
<span>
Similar to the likes of
<Button variant="link" class="h-fit p-0" href="https://university.webflow.com/">
Webflow,
</Button>
<Button variant="link" class="h-fit p-0" href="https://academy.hubspot.com/">
Hubspot,
</Button>
<Button
variant="link"
class="h-fit p-0"
href="https://university.atlassian.com/student/activity/2227111-home#/page/670e8ce7a1887954d13a856a"
>
Atlassian &
</Button>
<Button variant="link" class="h-fit p-0" href="https://framer.university/"
>Framer</Button
>
</span>
</p>
<button
onclick={() => {
navigator.clipboard.writeText('pnpx @classroomio/course-app');
toast('Copied to clipboard 😉');
}}
class={cn(
'group z-10 mt-5 rounded-full border border-white/50 bg-neutral-700 transition-all ease-in hover:cursor-pointer hover:bg-neutral-700 hover:text-white'
)}
>
<ShinnyText
class="animate-gradient inline-flex items-center justify-center bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text px-4 py-1 text-transparent text-white transition ease-out hover:text-neutral-400 hover:duration-300"
>
<span>pnpx @classroomio/course-app</span>
<Copy class="ml-2 size-3" />
</ShinnyText>
</button>
</BlurFade>
</CardBody>
</CardContainer>
</div>
</section>
<!-- Hero End -->
<!-- Templates -->
<section id="templates" class="my-5 w-full pt-0 md:my-10 md:pt-24">
<div class="container">
<h2 class="mb-5 text-center text-3xl font-semibold md:mb-20 md:text-start md:text-5xl">
Browser Templates
</h2>
<div class="grid max-w-6xl grid-cols-1 place-items-center gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each templates as template, index}
<BlurFade delay={0.25 + index * 0.25}>
<TemplateCard {template} />
</BlurFade>
{/each}
</div>
</div>
</section>
<!-- Templates End -->
<Separator class="my-20" />
<!-- Setup -->
<section id="setup" class="mb-20 w-full">
<BlurFade delay={0.25}>
<div class="container">
<!-- <h2 class="mb-10 text-center text-5xl font-semibold">
Up & Running <span class="font-mono italic text-blue-700">MINUTES</span>
</h2> -->
<div
class="relative mx-auto h-fit w-full max-w-3xl overflow-hidden rounded-lg border bg-neutral-800 p-20 py-40 text-center shadow-2xl"
>
<Meteors number={30} />
<h1 class="mb-5 text-5xl font-semibold text-white">
Start Customising Your University Site
</h1>
<div class="z-10 flex items-center justify-center">
<ShimmerButton
onClick={() => {
window.location.href =
'https://github.com/classroomio/classroomio/blob/a6af412b2401a8dc3f871773b96a24b2fa764f52/packages/course-app/README.md';
}}
class="shadow-2xl"
>
<span
class="whitespace-pre-wrap text-center text-sm font-medium leading-none tracking-tight text-white lg:text-lg dark:from-white dark:to-slate-900/10"
>
Go to Docs
</span>
</ShimmerButton>
</div>
</div>
</div>
</BlurFade>
</section>
<!-- Setup End -->
<Separator class="mt-20" />
<!-- Footer -->
<section class="my-10 w-full">
<div class="container">
<p class="text-center text-sm text-neutral-500">
Made with ❤️ by <a href="https://git.new/class">classroomio</a>
</p>
</div>
</section>
<!-- Footer End -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+23
View File
@@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter(),
alias: {
$lib: './src/lib',
$lib: './src/lib/*'
}
}
};
export default config;
+111
View File
@@ -0,0 +1,111 @@
import { fontFamily } from 'tailwindcss/defaultTheme';
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ['dark'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
},
secondary: {
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
},
destructive: {
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
},
muted: {
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
},
accent: {
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
},
popover: {
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
},
card: {
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: [...fontFamily.sans]
},
animation: {
meteor: 'meteor 5s linear infinite',
shimmer: 'shimmer 8s infinite',
'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
magicslide: 'magicslide var(--speed) ease-in-out infinite alternate'
},
keyframes: {
meteor: {
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: '1' },
'70%': { opacity: '1' },
'100%': {
transform: 'rotate(215deg) translateX(-500px)',
opacity: '0'
}
},
shimmer: {
'0%, 90%, 100%': {
'background-position': 'calc(-100% - var(--shimmer-width)) 0'
},
'30%, 60%': {
'background-position': 'calc(100% + var(--shimmer-width)) 0'
}
},
'spin-around': {
'0%': {
transform: 'translateZ(0) rotate(0)'
},
'15%, 35%': {
transform: 'translateZ(0) rotate(90deg)'
},
'65%, 85%': {
transform: 'translateZ(0) rotate(270deg)'
},
'100%': {
transform: 'translateZ(0) rotate(360deg)'
}
},
magicslide: {
to: {
transform: 'translate(calc(100cqw - 100%), 0)'
}
}
}
}
}
};
export default config;
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
import path from 'path';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
resolve: {
alias: {
$lib: path.resolve('./src/lib')
}
}
});
+1 -1
View File
@@ -1 +1 @@
v18.16.0
v18.17.0
+55
View File
@@ -0,0 +1,55 @@
# FROM node:18-alpine
# WORKDIR /app
# RUN npm install -g pnpm
# COPY . .
# # Install dependencies and build
# RUN pnpm install --frozen-lockfile && \
# cd apps/dashboard && \
# pnpm build || { echo "Build failed! Error output above."; exit 1; }
# EXPOSE 5004
# ENV NODE_ENV=production
# CMD [ "pnpm", "dashboard:start" ]
FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json .
COPY pnpm-lock.yaml .
RUN pnpm install --frozen-lockfile
COPY . .
# Set environment variables
ENV IS_SELFHOSTED=true
ENV DEPLOYMENT_PROVIDER=docker
ENV PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL
ENV PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY
ENV PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE
ENV PRIVATE_APP_HOST=$PRIVATE_APP_HOST
ENV PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS
ENV UNSPLASH_API_KEY=$UNSPLASH_API_KEY
ENV OPENAI_API_KEY=$OPENAI_API_KEY
ENV PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL
ENV PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
ENV NODE_OPTIONS="--max-old-space-size=10240"
WORKDIR /app/apps/dashboard
RUN pnpm install --frozen-lockfile
RUN IS_SELFHOSTED=true DEPLOYMENT_PROVIDER=docker pnpm build
RUN pnpm prune --production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/apps/dashboard/build build/
COPY --from=builder /app/apps/dashboard/node_modules node_modules/
COPY package.json .
EXPOSE 3004
ENV NODE_ENV=production
CMD [ "node", "build" ]
+1 -1
View File
@@ -109,7 +109,7 @@
},
"type": "module",
"engines": {
"node": "18.x"
"node": "^18.17.0"
},
"browser": {
"crypto": false
+3
View File
@@ -27,5 +27,8 @@
"supabase": "^1.151.1",
"tsconfig": "workspace:*",
"typescript": "^5.1.6"
},
"engines": {
"node": "^18.17.0"
}
}
+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 <help@classroomio.com>",
"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
}

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