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]>
@@ -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}}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -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-*
|
||||
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
||||
@@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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 – 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 – 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 – 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>
|
||||
@@ -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> </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}
|
||||
/>
|
||||
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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()}
|
||||
@@ -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 -->
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 891 B |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 230 KiB |
@@ -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"}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 @@
|
||||
v18.16.0
|
||||
v18.17.0
|
||||
|
||||
@@ -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" ]
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "18.x"
|
||||
"node": "^18.17.0"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
|
||||
@@ -27,5 +27,8 @@
|
||||
"supabase": "^1.151.1",
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-*
|
||||
@@ -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'
|
||||
}`
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_
|
||||
|
||||
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
|
||||
}
|
||||