svelte and nx rewrite
This commit is contained in:
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
11
template/apps/client/src/lib/components/footer.svelte
Normal file
11
template/apps/client/src/lib/components/footer.svelte
Normal 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>
|
||||
106
template/apps/client/src/lib/components/link-button.svelte
Normal file
106
template/apps/client/src/lib/components/link-button.svelte
Normal 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}
|
||||
29
template/apps/client/src/lib/components/sanity-block.svelte
Normal file
29
template/apps/client/src/lib/components/sanity-block.svelte
Normal 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>
|
||||
84
template/apps/client/src/lib/components/section/cta.svelte
Normal file
84
template/apps/client/src/lib/components/section/cta.svelte
Normal 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>
|
||||
@@ -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}
|
||||
17
template/apps/client/src/lib/components/ui/button/index.ts
Normal file
17
template/apps/client/src/lib/components/ui/button/index.ts
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user