finished navbar and blog. todo: finishing touches

This commit is contained in:
2025-08-21 02:03:21 +02:00
parent 519eb1ff2a
commit da67406b5f
49 changed files with 3260 additions and 626 deletions

View File

@@ -0,0 +1,9 @@
<script lang="ts">
let { children } = $props();
</script>
<blockquote
class="my-6 rounded-r-lg border-l-4 border-primary bg-muted/30 py-4 pl-6 text-lg text-muted-foreground italic"
>
{@render children()}
</blockquote>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import { cn } from '$lib/utils';
let { portableText, children } = $props();
const { value } = portableText;
const getHeadingClass = (style: string) => {
const baseClasses = 'font-bold tracking-tight';
switch (style) {
case 'h1':
return cn(baseClasses, 'text-4xl md:text-5xl lg:text-6xl mb-6 mt-8');
case 'h2':
return cn(baseClasses, 'text-3xl md:text-4xl mb-4 mt-8');
case 'h3':
return cn(baseClasses, 'text-2xl md:text-3xl mb-3 mt-6');
case 'h4':
return cn(baseClasses, 'text-xl md:text-2xl mb-3 mt-6');
case 'h5':
return cn(baseClasses, 'text-lg md:text-xl mb-2 mt-4');
case 'h6':
return cn(baseClasses, 'text-base md:text-lg mb-2 mt-4');
default:
return cn(baseClasses, 'text-xl mb-3 mt-6');
}
};
</script>
{#if value.style === 'h1'}
<h1 class={getHeadingClass(value.style)}>
{@render children()}
</h1>
{:else if value.style === 'h2'}
<h2 class={getHeadingClass(value.style)}>
{@render children()}
</h2>
{:else if value.style === 'h3'}
<h3 class={getHeadingClass(value.style)}>
{@render children()}
</h3>
{:else if value.style === 'h4'}
<h4 class={getHeadingClass(value.style)}>
{@render children()}
</h4>
{:else if value.style === 'h5'}
<h5 class={getHeadingClass(value.style)}>
{@render children()}
</h5>
{:else if value.style === 'h6'}
<h6 class={getHeadingClass(value.style)}>
{@render children()}
</h6>
{/if}

View File

@@ -0,0 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<p class="mb-4 leading-relaxed text-foreground">
{@render children()}
</p>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { getImageDimensions } from '@sanity/asset-utils';
import { client } from '$lib/sanity';
import imageUrlBuilder from '@sanity/image-url';
let { image, alt = '', width = 1200, height = 600, class: className = '' } = $props();
const { projectId, dataset } = client.config();
const builder = imageUrlBuilder({ projectId: projectId ?? '', dataset: dataset ?? '' });
function generateCoverImageUrl(
imageAsset: any,
targetWidth: number,
targetHeight: number
): string {
if (!imageAsset || !projectId || !dataset) {
return '';
}
const imageRef = imageAsset.asset?._ref || imageAsset.asset?._id || imageAsset.asset;
if (!imageRef) {
return '';
}
try {
const imageBuilder = builder
.image(imageRef)
.fit('crop')
.crop('center')
.width(targetWidth)
.height(targetHeight)
.format('webp')
.auto('format');
return imageBuilder.url();
} catch (error) {
console.error('Error generating cover image URL:', error);
return '';
}
}
const imageUrl = $derived(generateCoverImageUrl(image, width, height));
const dimensions = $derived(
image?.asset ? getImageDimensions(image) : { width: width, height: height }
);
</script>
{#if imageUrl}
<img
src={imageUrl}
{alt}
{width}
{height}
loading="lazy"
class="h-full w-full object-cover object-center {className}"
/>
{/if}

View File

@@ -45,17 +45,7 @@
items?: NavigationItem[];
}
let {
settings,
items = [
{ name: 'Home', url: '/' },
{ name: 'Test', url: '/test' },
{
name: 'Test Subitems',
subitems: [{ name: 'Test Page', url: '#' }]
}
]
}: Props = $props();
let { settings, items = [] }: Props = $props();
let activeIndex = $state(0);
let navigationList: HTMLElement;
@@ -74,7 +64,7 @@
const currentPath = $page.url.pathname;
let newActiveIndex = -1;
items.forEach((item, index) => {
items?.forEach((item, index) => {
if (item.url === currentPath) {
newActiveIndex = index;
} else if (item.subitems) {
@@ -125,15 +115,17 @@
function toggleSubmenu(itemName: string) {
expandedSubmenu = expandedSubmenu === itemName ? null : itemName;
}
const shouldReload = $derived($page.url.pathname.startsWith('/blog/'));
</script>
<nav class="bg-primary py-3">
<nav class="relative z-[50] bg-primary py-3">
<div class="container px-4 md:mx-auto md:px-0">
<div class="relative flex items-center">
<div class="flex items-center">
{#if settings}
<a href="/">
<Button variant="ghost" class="px-1">
<Button variant="ghost">
<Logo {settings} class="text-white" />
</Button>
</a>
@@ -143,35 +135,56 @@
</div>
<div class="absolute left-1/2 hidden -translate-x-1/2 transform md:flex">
<NavigationMenuRoot class="" viewport={false}>
<NavigationMenuRoot class="relative z-[50]" viewport={false}>
<div bind:this={navigationList}>
<NavigationMenuList class="relative">
<div
class="pointer-events-none absolute top-0 z-10 h-full rounded-md bg-white/10"
class="pointer-events-none absolute top-1/2 z-10 h-9 -translate-y-1/2 rounded-md bg-white/10"
style="left: {$indicatorPosition.left}px; width: {$indicatorPosition.width}px;"
></div>
{#each items as item}
{#each items || [] as item}
<NavigationMenuItem data-navigation-menu-item>
{#if item.url && !item.subitems}
<NavigationMenuLink
class={cn('relative z-20 text-white transition-colors hover:text-white/80')}
href={item.url}
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{item.name}
</NavigationMenuLink>
{:else if item.subitems}
<NavigationMenuTrigger
class="relative z-20 text-white transition-colors hover:text-white/80"
>
{item.name}
</NavigationMenuTrigger>
{#if item.url}
<NavigationMenuTrigger
class="relative z-20 cursor-pointer px-2 text-white transition-colors hover:text-white/80"
>
<NavigationMenuLink
href={item.url}
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{item.name}
</NavigationMenuLink>
</NavigationMenuTrigger>
{:else}
<NavigationMenuTrigger
class="relative z-20 cursor-pointer text-white transition-colors hover:text-white/80"
>
{item.name}
</NavigationMenuTrigger>
{/if}
<NavigationMenuContent
class="bg-primary absolute left-1/2 z-50 mt-2 min-w-full -translate-x-1/2 rounded-md border border-white/20 shadow-lg"
class="absolute left-1/2 z-[50] mt-2 min-w-max -translate-x-1/2 rounded-md border border-white/20 bg-primary shadow-lg"
>
{#each item.subitems as subitem}
<NavigationMenuLink
href={subitem.url}
class="block rounded-md px-4 py-2 text-white transition-colors hover:text-white/80"
data-sveltekit-reload={subitem.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{subitem.name}
</NavigationMenuLink>
@@ -191,7 +204,7 @@
<MoreHorizontal size={24} />
</Button>
</SheetTrigger>
<SheetContent side="top" class="bg-primary border-primary h-full w-full text-white">
<SheetContent side="top" class="h-full w-full border-primary bg-primary text-white">
<SheetHeader class="border-b border-white/20 pb-4">
<SheetTitle class="flex items-center justify-start">
{#if settings}
@@ -206,7 +219,7 @@
</SheetTitle>
</SheetHeader>
<div class="relative flex flex-col space-y-2 pt-6">
{#each items as item, index}
{#each items || [] as item, index}
{#if item.url && !item.subitems}
<div class="relative">
<a
@@ -216,6 +229,9 @@
index === activeIndex && 'bg-white/20'
)}
onclick={closeMobileMenu}
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{item.name}
</a>
@@ -224,7 +240,7 @@
<div class="relative space-y-1">
{#if index === activeIndex}
<div
class="absolute bottom-0 left-0 top-0 w-1 rounded-r-md bg-white/20"
class="absolute top-0 bottom-0 left-0 w-1 rounded-r-md bg-white/20"
></div>
{/if}
<Button
@@ -246,11 +262,26 @@
class="ml-4 space-y-1"
transition:slide={{ duration: 300, easing: quintOut }}
>
{#if item.url}
<a
href={item.url}
class="block rounded-md px-6 py-2 text-sm text-white/80 transition-colors hover:text-white"
onclick={closeMobileMenu}
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{item.name}
</a>
{/if}
{#each item.subitems as subitem}
<a
href={subitem.url}
class="block rounded-md px-6 py-2 text-sm text-white/80 transition-colors hover:text-white"
onclick={closeMobileMenu}
data-sveltekit-reload={subitem.url?.startsWith('/blog/') && shouldReload
? 'true'
: undefined}
>
{subitem.name}
</a>

View File

@@ -6,13 +6,24 @@
import SanityButton from './blocks/sanity-button.svelte';
import Callout from './blocks/callout.svelte';
import LinkMark from './blocks/link-mark.svelte';
import Heading from './blocks/heading.svelte';
import Blockquote from './blocks/blockquote.svelte';
import Paragraph from './blocks/paragraph.svelte';
let { body }: { body: BlockContent } = $props();
</script>
<div class="prose prose-lg max-w-none">
<PortableText
value={body}
<div
class="prose prose-lg prose-strong:font-bold
prose-em:italic prose-code:bg-muted
prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono prose-a:text-primary
prose-a:hover:underline prose-ul:list-disc
prose-ul:ml-6 prose-ul:space-y-2 prose-ol:list-decimal
prose-ol:ml-6 prose-ol:space-y-2 prose-li:leading-relaxed
max-w-none"
>
<PortableText
value={body}
components={{
types: {
callout: Callout,
@@ -21,9 +32,19 @@
file: SanityFile,
button: SanityButton
},
block: {
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
blockquote: Blockquote,
normal: Paragraph
},
marks: {
link: LinkMark
}
}}
}}
/>
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View File

@@ -0,0 +1,25 @@
import Root from "./pagination.svelte";
import Content from "./pagination-content.svelte";
import Item from "./pagination-item.svelte";
import Link from "./pagination-link.svelte";
import PrevButton from "./pagination-prev-button.svelte";
import NextButton from "./pagination-next-button.svelte";
import Ellipsis from "./pagination-ellipsis.svelte";
export {
Root,
Content,
Item,
Link,
PrevButton,
NextButton,
Ellipsis,
//
Root as Pagination,
Content as PaginationContent,
Item as PaginationItem,
Link as PaginationLink,
PrevButton as PaginationPrevButton,
NextButton as PaginationNextButton,
Ellipsis as PaginationEllipsis,
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
</script>
<ul
bind:this={ref}
data-slot="pagination-content"
class={cn("flex flex-row items-center gap-1", className)}
{...restProps}
>
{@render children?.()}
</ul>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
</script>
<span
bind:this={ref}
aria-hidden="true"
data-slot="pagination-ellipsis"
class={cn("flex size-9 items-center justify-center", className)}
{...restProps}
>
<EllipsisIcon class="size-4" />
<span class="sr-only">More pages</span>
</span>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import type { WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLLiAttributes> = $props();
</script>
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
{@render children?.()}
</li>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type Props, buttonVariants } from "$lib/components/ui/button/index.js";
let {
ref = $bindable(null),
class: className,
size = "icon",
isActive,
page,
children,
...restProps
}: PaginationPrimitive.PageProps &
Props & {
isActive: boolean;
} = $props();
</script>
{#snippet Fallback()}
{page.value}
{/snippet}
<PaginationPrimitive.Page
bind:ref
{page}
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
class={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.NextButtonProps = $props();
</script>
{#snippet Fallback()}
<span>Next</span>
<ChevronRightIcon class="size-4" />
{/snippet}
<PaginationPrimitive.NextButton
bind:ref
aria-label="Go to next page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pr-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: PaginationPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronLeftIcon class="size-4" />
<span>Previous</span>
{/snippet}
<PaginationPrimitive.PrevButton
bind:ref
aria-label="Go to previous page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pl-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { Pagination as PaginationPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
count = 0,
perPage = 10,
page = $bindable(1),
siblingCount = 1,
...restProps
}: PaginationPrimitive.RootProps = $props();
</script>
<PaginationPrimitive.Root
bind:ref
bind:page
role="navigation"
aria-label="pagination"
data-slot="pagination"
class={cn("mx-auto flex w-full justify-center", className)}
{count}
{perPage}
{siblingCount}
{...restProps}
/>