feat: buy a courseapp template

This commit is contained in:
Rotimi Best
2024-12-18 12:56:10 +01:00
parent 71017133a5
commit 5d1543b1f4
30 changed files with 853 additions and 34 deletions
+2 -1
View File
@@ -41,6 +41,7 @@
"@tailwindcss/typography": "^0.5.15",
"lucide-svelte": "^0.468.0",
"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';
@@ -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">
brought to you by
<a
href="https://git.new/class"
href="https://github.com/classroomio/classroomio/tree/main/packages/course-app#readme"
target="_blank"
class="inline-flex items-center font-semibold underline-offset-2 hover:underline"
>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 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>
+44 -31
View File
@@ -1,18 +1,16 @@
<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 { TemplateCard, CustomTemplate } 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 { BorderBeam } from '$lib/components/animation/border-beam';
import { ShimmerButton } from '$lib/components/animation/shimmerbutton';
import { BuyTemplate } from '$lib/components/buy-template';
import { Sparkle } from '$lib/components/animation/sparkle';
import Copy from 'lucide-svelte/icons/copy';
import { cn } from '$lib/utils';
import { HeroVideoDialog } from '$lib/components/animation/hero-video-dialog';
const templates = [
{
@@ -92,18 +90,18 @@
<div>
<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"
className="py-10 max-w-4xl mx-auto flex flex-col items-center 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"
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>
<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">
<BlurFade delay={0.25}>
<p class="text-md text-center">
With our prebuilt templates, you can setup a fully functional customer educational
website for your product in minutes.<br />
<span>
@@ -123,25 +121,36 @@
</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"
<div class="mt-4 flex flex-col items-center justify-center gap-4 md:flex-row">
<Button
class="w-full md:w-auto"
variant="secondary"
target="_blank"
rel="noopener noreferrer"
href="/npm"
>
<span>pnpx @classroomio/course-app</span>
Install Template
</Button>
<Copy class="ml-2 size-3" />
</ShinnyText>
</button>
<BuyTemplate ctaLabel="Buy A Custom Template" ctaClass="w-full md:w-auto" />
</div>
</BlurFade>
</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>
</div>
</section>
@@ -154,13 +163,17 @@
Browse 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}>
<BlurFade delay={0.25 * 2}>
<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}
<TemplateCard {template} />
</BlurFade>
{/each}
</div>
{/each}
<CustomTemplate />
</div>
</BlurFade>
</div>
</section>
<!-- Templates End -->
+7
View File
@@ -69,6 +69,7 @@ const config: Config = {
},
animation: {
'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
meteor: 'meteor 5s linear infinite',
shimmer: 'shimmer 8s infinite',
'spin-around': 'spin-around calc(var(--speed) * 2) infinite linear',
@@ -76,6 +77,12 @@ const config: Config = {
},
keyframes: {
'border-beam': {
'100%': {
'offset-distance': '100%'
}
},
meteor: {
'0%': { transform: 'rotate(215deg) translateX(0)', opacity: '1' },
'70%': { opacity: '1' },
+13 -1
View File
@@ -192,6 +192,9 @@ importers:
svelte-motion:
specifier: ^0.12.2
version: 0.12.2([email protected])
svelte-radix:
specifier: ^2.0.1
version: 2.0.1([email protected])
devDependencies:
'@sveltejs/adapter-auto':
specifier: ^3.0.0
@@ -5979,7 +5982,7 @@ packages:
peerDependencies:
'@sveltejs/kit': ^2.0.0
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
dev: true
@@ -16596,6 +16599,15 @@ packages:
typescript: 5.1.6
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]:
resolution: {integrity: sha512-N5JqlBni6BzElxmuFrOPxOJnjsxh1cFDACLEVKs8OHBcx8ZNRO1p5SxuQex1m3qbLzAC8G99EHeWcxGkjyKjLQ==}
engines: {node: '>=16'}