feat: buy a courseapp template
This commit is contained in:
@@ -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';
|
||||
+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">
|
||||
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>
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Generated
+13
-1
@@ -192,6 +192,9 @@ importers:
|
||||
svelte-motion:
|
||||
specifier: ^0.12.2
|
||||
version: 0.12.2(svelte@5.10.1)
|
||||
svelte-radix:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(svelte@5.10.1)
|
||||
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/vite-plugin-svelte@3.0.0)(svelte@4.2.12)(vite@5.0.3)
|
||||
'@sveltejs/kit': 2.0.0(@sveltejs/vite-plugin-svelte@3.0.0)(svelte@4.1.2)(vite@5.0.0)
|
||||
import-meta-resolve: 4.1.0
|
||||
dev: true
|
||||
|
||||
@@ -16596,6 +16599,15 @@ packages:
|
||||
typescript: 5.1.6
|
||||
dev: true
|
||||
|
||||
/svelte-radix@2.0.1(svelte@5.10.1):
|
||||
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
|
||||
|
||||
/svelte-sequential-preprocessor@2.0.1:
|
||||
resolution: {integrity: sha512-N5JqlBni6BzElxmuFrOPxOJnjsxh1cFDACLEVKs8OHBcx8ZNRO1p5SxuQex1m3qbLzAC8G99EHeWcxGkjyKjLQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
Reference in New Issue
Block a user