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

* feat: v1 templates

* update nvm

* working on the displaying the course content

* working on the displaying the course content

* displayed the content

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

* post hog template UI is good to go

* classic and minimalistic template UI is good to go

* some refactoring

* some refactoring

* layout adjustments

* update tsconfig

* trying to get the filter to work well

* this is working now

* cleanup

* cleanup

* UI updates

* course page UI for posthog

* navbar and sidebar for course page in all templates complete

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

* wokring on sections

* posthog lesson content done

* all templates fetches the lesson content from the section

* all templates properly typed

* scaffolding the new template

* finished the webflow-saas

* done with the webflow saas UI

* new calcom template

* new calcom template

* masonry layout for testitomonail section layout done

* calcom UI done

* UI all done

* cleanup

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

* done with this

* done with this

* working on tests

* migrated minimalistic template to svelte 5

* updated the syntax to svelte 5 for webflow

* migrated classic to svelte 5

* migrated posthog to svelte 5

* used svelte 5 syntax in calcom

* cli tool woks locally

* rename to course-sites

* use pnpm in root

* calcom refactoring

* remove unused files

* chore: biggest refactoring in my life

* add basic readme

* refactoring on course page

* restructure how courses are ordered

* proper state propagation

* add cf adapter

* fix pnpm i issue

* fix folder icon size

* use 18.17 version

* update npmrc

* add engine to packagejson

* pnpm install

* add engine to cio-com

* make it 18.x

* remove sequence

* move mdsvex to dependencies

* update lock

* mdsvex please work

* replace fs with import from vite

* add lock

* styling improvements

* fix styling

* sidebar style improvements

* fix: mobile concerns

* fix spacing of course header on desktop

* update header

* top nav fix

* feat: make cal template

* setup another template

* classic basic UI

* fixed the /courses fetching wrong page content

* fix breaking template cause of logo

* adding some updates

* improve styles theming

* finished updates from reviews

* updated the custom styling css

* clean up posthog template

* cli tool done, needs some cleaning though

* moved components to home

* improved the UI of the minimal template

* Add CLI tool and course app site

* prettier lint

* improve CLI

* ignore sveltekit folder

* publish course app

* fix tailwing theme + improve README

* finish up course-app site

* update README.md

---------

Co-authored-by: emmanuel <[email protected]>
This commit is contained in:
Rotimi Best
2024-12-14 03:40:47 +01:00
committed by GitHub
co-authored by emmanuel
parent 7e239da7d2
commit 26700dccd0
343 changed files with 17993 additions and 350 deletions
+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')
}
}
});