svelte and nx rewrite

This commit is contained in:
2025-08-03 18:24:31 +02:00
parent e0a743056f
commit cb47cbc620
85 changed files with 1769 additions and 5202 deletions

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { PortableText } from '@portabletext/svelte';
let { portableText } = $props();
const { value } = portableText;
</script>
{#if value?.content}
<div class="bg-secondary text-secondary-foreground shadow-lg rounded-md p-6 border my-6 border-gray-200">
<PortableText value={value.content} />
</div>
{/if}

View File

@@ -0,0 +1,13 @@
<script lang="ts">
let { portableText, children } = $props();
const { value } = portableText;
</script>
<a
href={value.href}
target={value.blank ? '_blank' : '_self'}
rel={value.blank ? 'noopener noreferrer' : undefined}
class="text-primary hover:underline"
>
{@render children()}
</a>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import { deconstructLink } from '$lib/link-helper';
import LinkButton from '../link-button.svelte';
import { onMount } from 'svelte';
let { portableText } = $props();
const { value: button } = portableText;
let linkData: { href: string; target: string } | null = $state(null);
let mounted = $state(false);
onMount(async () => {
if (button?.link) {
linkData = await deconstructLink(button.link);
}
mounted = true;
});
</script>
{#if mounted && linkData}
<LinkButton
text={button?.text || ''}
{linkData}
variant="default"
size="default"
/>
{/if}

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { getFileAsset } from '@sanity/asset-utils';
import { Download } from '@lucide/svelte';
import { client } from '$lib/sanity';
let { portableText } = $props();
const { value } = portableText;
const { projectId, dataset } = client.config();
</script>
{#if value?.asset}
{@const file = getFileAsset(value, { projectId, dataset })}
<a href={file.url} download class="inline-flex items-center gap-2 text-primary hover:underline">
<Download size={16} />
{value.title || 'Download file'}
</a>
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { getImageDimensions } from '@sanity/asset-utils';
import { generateImageUrl, dynamicHeight } from '$lib/image-url';
let { portableText } = $props();
const { value, isInline } = portableText;
</script>
{#if value?.asset}
{@const image = value}
{@const dimensions = getImageDimensions(image)}
{@const calculatedHeight = dynamicHeight(dimensions.height, dimensions.width, isInline ?? false)}
{@const imageUrl = generateImageUrl(image, dimensions.width, dimensions.height)}
<img
src={imageUrl}
alt={image.alt || ''}
width={isInline ? 100 : Math.min(dimensions.width, 1200)}
height={calculatedHeight}
loading="lazy"
class={isInline ? 'inline-block' : 'block mx-auto my-4'}
/>
{/if}

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import type { Settings } from '$lib/sanity.types';
let { settings }: { settings: Settings } = $props();
</script>
<div class="flex flex-col gap-4">
<p class="text-sm pb-4 px-8">
{settings?.footer?.replace('{YEAR}', new Date().getFullYear().toString())}
</p>
</div>

View File

@@ -0,0 +1,106 @@
<script lang="ts">
import { cn } from '$lib/utils';
import { ArrowRight, ExternalLink } from '@lucide/svelte';
let {
text,
linkData,
variant = 'default',
size = 'default',
extraIcon,
showIcon = true,
className,
onPress,
...restProps
}: {
text: string;
linkData: { href: string; target: string } | null;
variant?: 'ghost' | 'default' | 'secondary' | 'link' | 'destructive' | 'outline';
size?: 'default' | 'sm' | 'lg' | 'icon';
extraIcon?: any;
showIcon?: boolean;
className?: string;
onPress?: () => void;
[key: string]: any;
} = $props();
const isExternal = linkData?.href?.startsWith('http');
const baseClasses = cn(
'group',
'rounded-full',
'font-semibold',
'transition-all',
'duration-300',
'no-underline',
'flex',
'items-center',
'gap-2',
'inline-flex',
'justify-center',
'whitespace-nowrap',
'text-sm',
'font-medium',
'ring-offset-background',
'transition-colors',
'focus-visible:outline-none',
'focus-visible:ring-2',
'focus-visible:ring-ring',
'focus-visible:ring-offset-2',
'disabled:pointer-events-none',
'disabled:opacity-50',
showIcon ? 'pr-4' : 'px-6',
'active:transform',
'active:translate-y-[1px]',
{
// Variant styles
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
'bg-destructive text-destructive-foreground hover:bg-destructive/90':
variant === 'destructive',
'border border-input bg-background hover:bg-accent hover:text-accent-foreground':
variant === 'outline',
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
'text-primary underline-offset-4 hover:underline': variant === 'link'
},
{
// Size styles
'h-10 px-4 py-2': size === 'default',
'h-9 rounded-md px-3': size === 'sm',
'h-11 rounded-md px-8': size === 'lg',
'h-10 w-10': size === 'icon'
},
className
);
</script>
{#if linkData}
<a
href={linkData.href}
target={linkData?.target ?? '_self'}
rel={isExternal ? 'noopener noreferrer' : undefined}
onclick={onPress}
class={baseClasses}
{...restProps}
>
{#if extraIcon}
<span class="transition-transform duration-300 group-hover:scale-110">
{@render extraIcon({ size: size === 'lg' ? 24 : 20 })}
</span>
{/if}
<span>{text}</span>
{#if showIcon}
<span
class="transition-all duration-300 group-hover:transform group-hover:translate-x-1"
>
{#if isExternal}
<ExternalLink size={size === 'lg' ? 24 : 20} />
{:else}
<ArrowRight strokeWidth={3} size={size === 'lg' ? 24 : 20} />
{/if}
</span>
{/if}
</a>
{/if}

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { PortableText } from '@portabletext/svelte';
import type { BlockContent } from '$lib/sanity.types';
import SanityImage from './blocks/sanity-image.svelte';
import SanityFile from './blocks/sanity-file.svelte';
import SanityButton from './blocks/sanity-button.svelte';
import Callout from './blocks/callout.svelte';
import LinkMark from './blocks/link-mark.svelte';
let { body }: { body: BlockContent } = $props();
</script>
<div class="prose prose-lg max-w-none">
<PortableText
value={body}
components={{
types: {
callout: Callout,
image: SanityImage,
imageWithAlt: SanityImage,
file: SanityFile,
button: SanityButton
},
marks: {
link: LinkMark
}
}}
/>
</div>

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import LinkButton from '../link-button.svelte';
import type { CtaSection } from '$lib/sanity.types';
import { deconstructLink } from '$lib/link-helper';
import type { SimpleImage } from '$lib/asset-to-url';
import { cn } from '$lib/utils';
import { generateImageUrl } from '$lib/image-url';
import SanityBlock from '../sanity-block.svelte';
import { onMount } from 'svelte';
interface CTAProps {
cta?: CtaSection;
sectionTitle?: string;
background: SimpleImage | any;
backgroundColor?: string;
textColor?: string;
}
let {
cta,
background,
sectionTitle,
backgroundColor = 'bg-gray-900/80',
textColor = 'text-white'
}: CTAProps = $props();
let linkData: { href: string; target: string } | null = $state(null);
let mounted = $state(false);
onMount(async () => {
if (cta?.button?.link) {
linkData = await deconstructLink(cta.button.link);
}
mounted = true;
});
</script>
<section
class={cn(
'min-h-screen flex flex-col md:flex-row w-full overflow-hidden relative',
textColor
)}
style:background-image={background?.url ? `url(${background.url})` : undefined}
style:background-size="cover"
style:background-position="center"
style:background-repeat="no-repeat"
aria-label={background?.alt}
>
<div class={cn('absolute inset-0 z-0', backgroundColor)}></div>
<div
class="z-10 flex flex-col items-center md:items-start justify-center md:justify-start mt-20 px-6 md:px-20 md:py-32 md:flex-1 space-y-12 min-h-screen md:min-h-0"
>
<div>
{#if sectionTitle}
<p class="text-sm mb-4">
{sectionTitle}
</p>
{/if}
<h1 class="md:max-w-[60rem] text-6xl md:text-8xl font-bold leading-tight">
{cta?.title}
</h1>
</div>
<div class="flex flex-col items-start space-y-8 md:space-y-14 w-full max-w-xl">
{#if cta?.description}
<div class="text-lg">
<SanityBlock body={cta.description} />
</div>
{/if}
{#if mounted && linkData}
<div class="mb-20">
<LinkButton
text={cta?.button?.text ?? ''}
{linkData}
size="lg"
variant="default"
/>
</div>
{/if}
</div>
</div>
</section>

View File

@@ -0,0 +1,80 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
outline:
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};