feat: buy a courseapp template
This commit is contained in:
@@ -41,6 +41,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"lucide-svelte": "^0.468.0",
|
"lucide-svelte": "^0.468.0",
|
||||||
"svelte-inview": "^4.0.4",
|
"svelte-inview": "^4.0.4",
|
||||||
"svelte-motion": "^0.12.2"
|
"svelte-motion": "^0.12.2",
|
||||||
|
"svelte-radix": "^2.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
|
export let size = 200;
|
||||||
|
export let duration = 15;
|
||||||
|
export let anchor = 90;
|
||||||
|
export let borderWidth = 1.5;
|
||||||
|
export let colorFrom = '#ffaa40';
|
||||||
|
export let colorTo = '#9c40ff';
|
||||||
|
export let delay = 0;
|
||||||
|
let delaySec = delay + 's';
|
||||||
|
|
||||||
|
let className: any = '';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style:--border-width={borderWidth}
|
||||||
|
style:--size={size}
|
||||||
|
style:--color-from={colorFrom}
|
||||||
|
style:--color-to={colorTo}
|
||||||
|
style:--delay={delaySec}
|
||||||
|
style:--anchor={anchor}
|
||||||
|
style:--duration={duration}
|
||||||
|
class={cn(
|
||||||
|
'pointer-events-none absolute inset-[0] rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]',
|
||||||
|
|
||||||
|
// mask styles
|
||||||
|
'![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]',
|
||||||
|
|
||||||
|
// pseudo styles
|
||||||
|
'after:animate-border-beam after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
></div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as BorderBeam } from './border-beam.svelte';
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
export let width: string | number = 16;
|
||||||
|
export let height: string | number = 16;
|
||||||
|
export let x = 0;
|
||||||
|
export let y = 0;
|
||||||
|
export let cx = 1;
|
||||||
|
export let cy = 1;
|
||||||
|
export let cr = 1;
|
||||||
|
export let fillColor = 'rgb(163 163 163 / 0.8)';
|
||||||
|
|
||||||
|
let className: any = '';
|
||||||
|
export { className as class };
|
||||||
|
let id = crypto.randomUUID().toString().slice(0, 10); // generating a unique ID for Components
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class={cn('pointer-events-none absolute inset-0 h-full w-full ', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
fill={fillColor}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
{id}
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
patternContentUnits="userSpaceOnUse"
|
||||||
|
{x}
|
||||||
|
{y}
|
||||||
|
>
|
||||||
|
<circle id="pattern-circle" {cx} {cy} r={cr} />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" stroke-width={0} fill={`url(#${id})`} />
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as DotPattern } from './dot-pattern.svelte';
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { AnimatePresence, Motion } from 'svelte-motion';
|
||||||
|
import { Play, X } from 'lucide-svelte';
|
||||||
|
|
||||||
|
type AnimationStyle =
|
||||||
|
| 'from-bottom'
|
||||||
|
| 'from-center'
|
||||||
|
| 'from-top'
|
||||||
|
| 'from-left'
|
||||||
|
| 'from-right'
|
||||||
|
| 'fade'
|
||||||
|
| 'top-in-bottom-out'
|
||||||
|
| 'left-in-right-out';
|
||||||
|
|
||||||
|
// interface HeroVideoProps {
|
||||||
|
// animationStyle?: AnimationStyle;
|
||||||
|
// videoSrc: string;
|
||||||
|
// thumbnailSrc: string;
|
||||||
|
// thumbnailAlt?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export let animationStyle: AnimationStyle = 'from-center';
|
||||||
|
export let videoSrc: string;
|
||||||
|
export let thumbnailSrc: string;
|
||||||
|
export let thumbnailAlt: string = 'Video thumbnail';
|
||||||
|
export let iconColor: string = 'white';
|
||||||
|
|
||||||
|
const isVideoOpen = writable(false);
|
||||||
|
const isCloseHovered = writable(false);
|
||||||
|
const isPlayHovered = writable(false);
|
||||||
|
|
||||||
|
function openVideo() {
|
||||||
|
isVideoOpen.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeVideo() {
|
||||||
|
isVideoOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationVariants = {
|
||||||
|
'from-bottom': {
|
||||||
|
initial: { y: 100, opacity: 0 },
|
||||||
|
animate: { y: 0, opacity: 1 },
|
||||||
|
exit: { y: 100, opacity: 0 }
|
||||||
|
},
|
||||||
|
'from-center': {
|
||||||
|
initial: { scale: 0.5, opacity: 0 },
|
||||||
|
animate: { scale: 1, opacity: 1 },
|
||||||
|
exit: { scale: 0.5, opacity: 0 }
|
||||||
|
},
|
||||||
|
'from-top': {
|
||||||
|
initial: { y: -100, opacity: 0 },
|
||||||
|
animate: { y: 0, opacity: 1 },
|
||||||
|
exit: { y: -100, opacity: 0 }
|
||||||
|
},
|
||||||
|
'from-left': {
|
||||||
|
initial: { x: -100, opacity: 0 },
|
||||||
|
animate: { x: 0, opacity: 1 },
|
||||||
|
exit: { x: -100, opacity: 0 }
|
||||||
|
},
|
||||||
|
'from-right': {
|
||||||
|
initial: { x: 100, opacity: 0 },
|
||||||
|
animate: { x: 0, opacity: 1 },
|
||||||
|
exit: { x: 100, opacity: 0 }
|
||||||
|
},
|
||||||
|
fade: {
|
||||||
|
initial: { opacity: 0 },
|
||||||
|
animate: { opacity: 1 },
|
||||||
|
exit: { opacity: 0 }
|
||||||
|
},
|
||||||
|
'top-in-bottom-out': {
|
||||||
|
initial: { y: -100, opacity: 0 },
|
||||||
|
animate: { y: 0, opacity: 1 },
|
||||||
|
exit: { y: 200, opacity: 0 }
|
||||||
|
},
|
||||||
|
'left-in-right-out': {
|
||||||
|
initial: { x: -100, opacity: 0 },
|
||||||
|
animate: { x: 0, opacity: 1 },
|
||||||
|
exit: { x: 200, opacity: 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: selectedAnimation = animationVariants[animationStyle];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Thumbnail -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="relative cursor-pointer" on:click={openVideo}>
|
||||||
|
<img
|
||||||
|
src={thumbnailSrc}
|
||||||
|
alt={thumbnailAlt}
|
||||||
|
width={1920}
|
||||||
|
height={1080}
|
||||||
|
class="w-full rounded-2xl transition-all duration-200"
|
||||||
|
/>
|
||||||
|
<!-- {$isPlayHovered===true ? 'blur-[2px]' :'' } to make background blur put this in above img class -->
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class="flex size-24 items-center justify-center rounded-full border border-neutral-200 backdrop-blur-md transition-transform duration-300 ease-out"
|
||||||
|
on:mouseenter={() => isPlayHovered.set(true)}
|
||||||
|
on:mouseleave={() => isPlayHovered.set(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative flex size-20 items-center justify-center rounded-full border border-neutral-200 backdrop-blur-2xl transition-all duration-300 ease-out"
|
||||||
|
class:isPlayHovered={$isPlayHovered}
|
||||||
|
>
|
||||||
|
<Play
|
||||||
|
strokeWidth={1.7}
|
||||||
|
class="size-8"
|
||||||
|
style="transform: scale({$isPlayHovered ? 1.1 : 1}); transition: transform 0.3s ease;
|
||||||
|
color: {iconColor};
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence let:item list={[{ key: $isVideoOpen }]}>
|
||||||
|
{#if item.key}
|
||||||
|
<Motion initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} let:motion>
|
||||||
|
<div
|
||||||
|
use:motion
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
|
||||||
|
>
|
||||||
|
<!-- Modal Content -->
|
||||||
|
<Motion
|
||||||
|
initial={selectedAnimation.initial}
|
||||||
|
animate={selectedAnimation.animate}
|
||||||
|
exit={selectedAnimation.exit}
|
||||||
|
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
|
||||||
|
let:motion
|
||||||
|
>
|
||||||
|
<div use:motion class="relative mx-4 aspect-video w-full max-w-4xl md:mx-0">
|
||||||
|
<Motion let:motion whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<button
|
||||||
|
use:motion
|
||||||
|
class="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md"
|
||||||
|
on:click={closeVideo}
|
||||||
|
on:mouseenter={() => isCloseHovered.set(true)}
|
||||||
|
on:mouseleave={() => isCloseHovered.set(false)}
|
||||||
|
>
|
||||||
|
<X class="size-5" />
|
||||||
|
</button>
|
||||||
|
</Motion>
|
||||||
|
<Motion
|
||||||
|
animate={{ scale: $isCloseHovered ? 0.98 : 1 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
let:motion
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
use:motion
|
||||||
|
class="relative isolate z-[1] size-full overflow-hidden rounded-2xl border-2 border-white"
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
<iframe
|
||||||
|
src={videoSrc}
|
||||||
|
class="size-full rounded-2xl"
|
||||||
|
allowfullscreen
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
</Motion>
|
||||||
|
{/if}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.size-24 {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
|
.size-20 {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
.size-8 {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
.size-full {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as HeroVideoDialog } from './hero-video-dialog.svelte';
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button';
|
||||||
|
import { ShimmerButton } from '$lib/components/animation/shimmerbutton';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import BadgeCheck from 'lucide-svelte/icons/badge-check';
|
||||||
|
|
||||||
|
const PAYMENT_LINK = 'https://buy.stripe.com/8wM6pF5Sdabhbny28c';
|
||||||
|
|
||||||
|
const whatYouGet = [
|
||||||
|
'A custom template for your SAAS',
|
||||||
|
'A free 101 course on how to use your SAAS',
|
||||||
|
'Deploy to Vercel or Selfhost',
|
||||||
|
'Request 1 free revision',
|
||||||
|
'Get first draft in 48 hours'
|
||||||
|
];
|
||||||
|
|
||||||
|
let { ctaLabel = 'Learn More', ctaClass = '' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root>
|
||||||
|
<!-- <span
|
||||||
|
class="absolute inset-0 z-30 flex items-center justify-center bg-black/50 opacity-0 transition-all duration-500 hover:opacity-80"
|
||||||
|
>
|
||||||
|
</span> -->
|
||||||
|
<Dialog.Trigger class={cn(buttonVariants({ variant: 'default' }), `z-20 ${ctaClass}`)}>
|
||||||
|
{ctaLabel}
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Buy A Custom Template</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Place an order for a custom template and we will ship it in 48 hours.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="grid gap-4 py-4">
|
||||||
|
<p>What you get:</p>
|
||||||
|
<ul>
|
||||||
|
{#each whatYouGet as item}
|
||||||
|
<li class="mb-2 flex items-center gap-2">
|
||||||
|
<BadgeCheck class="text-green-700" />
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<ShimmerButton
|
||||||
|
href={PAYMENT_LINK}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full px-6 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"
|
||||||
|
>
|
||||||
|
Buy
|
||||||
|
</span>
|
||||||
|
</ShimmerButton>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as BuyTemplate } from './buy-template.svelte';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Install } from './install.svelte';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { ShinnyText } from '$lib/components/animation/shinny-text';
|
||||||
|
import Copy from 'lucide-svelte/icons/copy';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
|
let { command = 'pnpx @classroomio/course-app' } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
navigator.clipboard.writeText(command);
|
||||||
|
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>{command}</span>
|
||||||
|
|
||||||
|
<Copy class="ml-2 size-3" />
|
||||||
|
</ShinnyText>
|
||||||
|
</button>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<p class="bg-blue-700 py-1.5 text-center text-sm font-medium text-white">
|
<p class="bg-blue-700 py-1.5 text-center text-sm font-medium text-white">
|
||||||
brought to you by
|
brought to you by
|
||||||
<a
|
<a
|
||||||
href="https://git.new/class"
|
href="https://github.com/classroomio/classroomio/tree/main/packages/course-app#readme"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="inline-flex items-center font-semibold underline-offset-2 hover:underline"
|
class="inline-flex items-center font-semibold underline-offset-2 hover:underline"
|
||||||
>classroomio.com
|
>classroomio.com
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Santa from './santa.svg';
|
||||||
|
import { BorderBeam } from '$lib/components/animation/border-beam';
|
||||||
|
import { DotPattern } from '$lib/components/animation/dot-pattern';
|
||||||
|
import { BuyTemplate } from '$lib/components/buy-template';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="custom-template-card"
|
||||||
|
class="group relative mb-5 flex h-full w-full max-w-[340px] flex-col items-center justify-center rounded-md border px-4 py-2 shadow-2xl transition-all duration-500"
|
||||||
|
>
|
||||||
|
<BorderBeam />
|
||||||
|
<DotPattern />
|
||||||
|
<img src={Santa} alt="Santa" class="z-20 h-28 w-28" />
|
||||||
|
<h3 class="z-20 mb-6 text-center font-sans text-5xl font-bold">Buy A Custom Template</h3>
|
||||||
|
|
||||||
|
<BuyTemplate ctaLabel="Learn More" />
|
||||||
|
</div>
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { default as TemplateCard } from './template-card.svelte';
|
export { default as TemplateCard } from './template-card.svelte';
|
||||||
|
export { default as CustomTemplate } from './custom-template.svelte';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import Cross2 from "svelte-radix/Cross2.svelte";
|
||||||
|
import * as Dialog from "./index.js";
|
||||||
|
import { cn, flyAndScale } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DialogPrimitive.ContentProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let transition: $$Props["transition"] = flyAndScale;
|
||||||
|
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||||
|
duration: 200,
|
||||||
|
};
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn(
|
||||||
|
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<Cross2 class="h-4 w-4" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DialogPrimitive.DescriptionProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogPrimitive.Description>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DialogPrimitive.OverlayProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let transition: $$Props["transition"] = fade;
|
||||||
|
export let transitionConfig: $$Props["transitionConfig"] = {
|
||||||
|
duration: 150,
|
||||||
|
};
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
type $$Props = DialogPrimitive.PortalProps;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Portal {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = DialogPrimitive.TitleProps;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogPrimitive.Title>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
import Title from "./dialog-title.svelte";
|
||||||
|
import Portal from "./dialog-portal.svelte";
|
||||||
|
import Footer from "./dialog-footer.svelte";
|
||||||
|
import Header from "./dialog-header.svelte";
|
||||||
|
import Overlay from "./dialog-overlay.svelte";
|
||||||
|
import Content from "./dialog-content.svelte";
|
||||||
|
import Description from "./dialog-description.svelte";
|
||||||
|
|
||||||
|
const Root = DialogPrimitive.Root;
|
||||||
|
const Trigger = DialogPrimitive.Trigger;
|
||||||
|
const Close = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Dialog,
|
||||||
|
Title as DialogTitle,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Footer as DialogFooter,
|
||||||
|
Header as DialogHeader,
|
||||||
|
Trigger as DialogTrigger,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Description as DialogDescription,
|
||||||
|
Close as DialogClose,
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export type FormInputEvent<T extends Event = Event> = T & {
|
||||||
|
currentTarget: EventTarget & HTMLInputElement;
|
||||||
|
};
|
||||||
|
export type InputEvents = {
|
||||||
|
blur: FormInputEvent<FocusEvent>;
|
||||||
|
change: FormInputEvent<Event>;
|
||||||
|
click: FormInputEvent<MouseEvent>;
|
||||||
|
focus: FormInputEvent<FocusEvent>;
|
||||||
|
focusin: FormInputEvent<FocusEvent>;
|
||||||
|
focusout: FormInputEvent<FocusEvent>;
|
||||||
|
keydown: FormInputEvent<KeyboardEvent>;
|
||||||
|
keypress: FormInputEvent<KeyboardEvent>;
|
||||||
|
keyup: FormInputEvent<KeyboardEvent>;
|
||||||
|
mouseover: FormInputEvent<MouseEvent>;
|
||||||
|
mouseenter: FormInputEvent<MouseEvent>;
|
||||||
|
mouseleave: FormInputEvent<MouseEvent>;
|
||||||
|
mousemove: FormInputEvent<MouseEvent>;
|
||||||
|
paste: FormInputEvent<ClipboardEvent>;
|
||||||
|
input: FormInputEvent<InputEvent>;
|
||||||
|
wheel: FormInputEvent<WheelEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes } from "svelte/elements";
|
||||||
|
import type { InputEvents } from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = HTMLInputAttributes;
|
||||||
|
type $$Events = InputEvents;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export let value: $$Props["value"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||||
|
// Fixed in Svelte 5, but not backported to 4.x.
|
||||||
|
export let readonly: $$Props["readonly"] = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{readonly}
|
||||||
|
on:blur
|
||||||
|
on:change
|
||||||
|
on:click
|
||||||
|
on:focus
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:keydown
|
||||||
|
on:keypress
|
||||||
|
on:keyup
|
||||||
|
on:mouseover
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
|
on:mousemove
|
||||||
|
on:paste
|
||||||
|
on:input
|
||||||
|
on:wheel|passive
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./label.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Label,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Label as LabelPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type $$Props = LabelPrimitive.Props;
|
||||||
|
|
||||||
|
let className: $$Props["class"] = undefined;
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
class={cn(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</LabelPrimitive.Root>
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
|
|
||||||
import { CardBody, CardContainer } from '$lib/components/animation/card';
|
import { CardBody, CardContainer } from '$lib/components/animation/card';
|
||||||
import { BlurFade } from '$lib/components/animation/blurfade';
|
import { BlurFade } from '$lib/components/animation/blurfade';
|
||||||
import { BlurIn } from '$lib/components/animation/blurin';
|
import { BlurIn } from '$lib/components/animation/blurin';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { TemplateCard } from '$lib/components/template-card';
|
import { TemplateCard, CustomTemplate } from '$lib/components/template-card';
|
||||||
import { Meteors } from '$lib/components/animation/meteors';
|
import { Meteors } from '$lib/components/animation/meteors';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ShinnyText } from '$lib/components/animation/shinny-text';
|
import { BorderBeam } from '$lib/components/animation/border-beam';
|
||||||
import { ShimmerButton } from '$lib/components/animation/shimmerbutton';
|
import { ShimmerButton } from '$lib/components/animation/shimmerbutton';
|
||||||
|
import { BuyTemplate } from '$lib/components/buy-template';
|
||||||
import { Sparkle } from '$lib/components/animation/sparkle';
|
import { Sparkle } from '$lib/components/animation/sparkle';
|
||||||
import Copy from 'lucide-svelte/icons/copy';
|
import { HeroVideoDialog } from '$lib/components/animation/hero-video-dialog';
|
||||||
import { cn } from '$lib/utils';
|
|
||||||
|
|
||||||
const templates = [
|
const templates = [
|
||||||
{
|
{
|
||||||
@@ -92,18 +90,18 @@
|
|||||||
<div>
|
<div>
|
||||||
<CardContainer className="md:px-20">
|
<CardContainer className="md:px-20">
|
||||||
<CardBody
|
<CardBody
|
||||||
className="max-w-4xl min-h-[50vh] flex flex-col justify-center px-30 gap-2 md:gap-5"
|
className="py-10 max-w-4xl mx-auto flex flex-col items-center justify-center px-30 gap-2 md:gap-5"
|
||||||
>
|
>
|
||||||
<BlurIn
|
<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"
|
class="pointer-events-none flex max-w-2xl flex-col items-center 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>
|
Create a <span class="font-mono italic text-blue-700">UNIVERSITY</span>
|
||||||
<span>Site for your SAAS in</span>
|
<span>Site for your SAAS in</span>
|
||||||
<span class="ml-1 font-mono italic text-blue-700">MINUTES</span>
|
<span class="ml-1 font-mono italic text-blue-700">MINUTES</span>
|
||||||
</BlurIn>
|
</BlurIn>
|
||||||
|
|
||||||
<BlurFade delay={0.25 * 2}>
|
<BlurFade delay={0.25}>
|
||||||
<p class="text-md">
|
<p class="text-md text-center">
|
||||||
With our prebuilt templates, you can setup a fully functional customer educational
|
With our prebuilt templates, you can setup a fully functional customer educational
|
||||||
website for your product in minutes.<br />
|
website for your product in minutes.<br />
|
||||||
<span>
|
<span>
|
||||||
@@ -123,25 +121,36 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<div class="mt-4 flex flex-col items-center justify-center gap-4 md:flex-row">
|
||||||
onclick={() => {
|
<Button
|
||||||
navigator.clipboard.writeText('pnpx @classroomio/course-app');
|
class="w-full md:w-auto"
|
||||||
toast('Copied to clipboard 😉');
|
variant="secondary"
|
||||||
}}
|
target="_blank"
|
||||||
class={cn(
|
rel="noopener noreferrer"
|
||||||
'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'
|
href="/npm"
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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>
|
Install Template
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Copy class="ml-2 size-3" />
|
<BuyTemplate ctaLabel="Buy A Custom Template" ctaClass="w-full md:w-auto" />
|
||||||
</ShinnyText>
|
</div>
|
||||||
</button>
|
|
||||||
</BlurFade>
|
</BlurFade>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="relative mx-auto my-4 flex max-w-5xl flex-col items-center justify-center px-7"
|
||||||
|
>
|
||||||
|
<div class="relative overflow-hidden rounded-2xl p-1">
|
||||||
|
<BorderBeam />
|
||||||
|
<HeroVideoDialog
|
||||||
|
animationStyle="from-right"
|
||||||
|
videoSrc="https://www.youtube.com/embed/P2I2HoyrjVo"
|
||||||
|
thumbnailSrc="https://cdn.courseapp.oncws.com/courseapp-og.png"
|
||||||
|
thumbnailAlt="Courseapp by ClassroomIO – Create your SAAS Academy in Minutes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- </BlurFade> -->
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -154,13 +163,17 @@
|
|||||||
Browse Templates
|
Browse Templates
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid max-w-6xl grid-cols-1 place-items-center gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<BlurFade delay={0.25 * 2}>
|
||||||
{#each templates as template, index}
|
<div
|
||||||
<BlurFade delay={0.25 + index * 0.25}>
|
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}
|
||||||
<TemplateCard {template} />
|
<TemplateCard {template} />
|
||||||
</BlurFade>
|
{/each}
|
||||||
{/each}
|
|
||||||
</div>
|
<CustomTemplate />
|
||||||
|
</div>
|
||||||
|
</BlurFade>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- Templates End -->
|
<!-- Templates End -->
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
animation: {
|
animation: {
|
||||||
|
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
|
||||||
meteor: 'meteor 5s linear infinite',
|
meteor: 'meteor 5s linear infinite',
|
||||||
shimmer: 'shimmer 8s infinite',
|
shimmer: 'shimmer 8s infinite',
|
||||||
'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
|
'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
|
||||||
@@ -76,6 +77,12 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
'border-beam': {
|
||||||
|
'100%': {
|
||||||
|
'offset-distance': '100%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
meteor: {
|
meteor: {
|
||||||
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: '1' },
|
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: '1' },
|
||||||
'70%': { opacity: '1' },
|
'70%': { opacity: '1' },
|
||||||
|
|||||||
Generated
+13
-1
@@ -192,6 +192,9 @@ importers:
|
|||||||
svelte-motion:
|
svelte-motion:
|
||||||
specifier: ^0.12.2
|
specifier: ^0.12.2
|
||||||
version: 0.12.2([email protected])
|
version: 0.12.2([email protected])
|
||||||
|
svelte-radix:
|
||||||
|
specifier: ^2.0.1
|
||||||
|
version: 2.0.1([email protected])
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
@@ -5979,7 +5982,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sveltejs/kit': ^2.0.0
|
'@sveltejs/kit': ^2.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 2.0.0(@sveltejs/[email protected])(svelte@4.2.12)([email protected].3)
|
'@sveltejs/kit': 2.0.0(@sveltejs/[email protected])([email protected].2)([email protected].0)
|
||||||
import-meta-resolve: 4.1.0
|
import-meta-resolve: 4.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -16596,6 +16599,15 @@ packages:
|
|||||||
typescript: 5.1.6
|
typescript: 5.1.6
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/[email protected]([email protected]):
|
||||||
|
resolution: {integrity: sha512-YrX44Dj+Rp6YZuPSjdmyd6P8QTkb2NXwySUCZYzjwkP6Cl3dZaTBPPeaSOutP3v3ycQ2XwyNOpyn4p0QcN+uYQ==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=7.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.10.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/[email protected]:
|
/[email protected]:
|
||||||
resolution: {integrity: sha512-N5JqlBni6BzElxmuFrOPxOJnjsxh1cFDACLEVKs8OHBcx8ZNRO1p5SxuQex1m3qbLzAC8G99EHeWcxGkjyKjLQ==}
|
resolution: {integrity: sha512-N5JqlBni6BzElxmuFrOPxOJnjsxh1cFDACLEVKs8OHBcx8ZNRO1p5SxuQex1m3qbLzAC8G99EHeWcxGkjyKjLQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|||||||
Reference in New Issue
Block a user