feat(Header, BlogCard): enhance UX with search and lazy loading

This commit is contained in:
ZhymabekRoman 2024-10-21 00:24:19 +05:00
parent 74f12e2c1d
commit 9fb1c77586
27 changed files with 721 additions and 97 deletions

Binary file not shown.

View file

@ -17,6 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"lucide-svelte": "^0.453.0",
"mode-watcher": "^0.4.1",
"postcss": "^8.4.47",
@ -36,6 +37,10 @@
"@iconify/svelte": "^4.0.2",
"bits-ui": "^0.21.16",
"copy-to-clipboard": "^3.3.3",
"svelte-bricks": "^0.2.1"
"es-toolkit": "^1.25.2",
"medium-zoom": "^1.1.0",
"svelte-bricks": "^0.2.1",
"svelte-lazy": "^1.2.9",
"svelte-markdown": "^0.4.1"
}
}

View file

@ -4,7 +4,7 @@
@layer base {
:root {
--background: 0 0% 100%;
--background: 0, 0%, 97%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
@ -37,7 +37,7 @@
}
.dark {
--background: 240 10% 3.9%;
--background: 240 10% 12%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
@ -76,3 +76,8 @@
@apply bg-background text-foreground;
}
}
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 999;
}

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { Dialog as DialogPrimitive } from 'bits-ui';
import type { Command as CommandPrimitive } from 'cmdk-sv';
import Command from './command.svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
type $$Props = DialogPrimitive.Props & CommandPrimitive.CommandProps;
export let open: $$Props['open'] = false;
export let value: $$Props['value'] = undefined;
</script>
<Dialog.Root bind:open {...$$restProps}>
<Dialog.Content class="overflow-hidden p-0 shadow-lg">
<Command
class="[&_[data-cmdk-group-heading]]:text-muted-foreground [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:font-medium [&_[data-cmdk-group]:not([hidden])_~[data-cmdk-group]]:pt-0 [&_[data-cmdk-group]]:px-2 [&_[data-cmdk-input-wrapper]_svg]:h-5 [&_[data-cmdk-input-wrapper]_svg]:w-5 [&_[data-cmdk-input]]:h-12 [&_[data-cmdk-item]]:px-2 [&_[data-cmdk-item]]:py-3 [&_[data-cmdk-item]_svg]:h-5 [&_[data-cmdk-item]_svg]:w-5"
{...$$restProps}
bind:value
>
<slot />
</Command>
</Dialog.Content>
</Dialog.Root>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.EmptyProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Empty class={cn("py-6 text-center text-sm", className)} {...$$restProps}>
<slot />
</CommandPrimitive.Empty>

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.GroupProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Group
class={cn(
"text-foreground [&_[data-cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[data-cmdk-group-heading]]:px-2 [&_[data-cmdk-group-heading]]:py-1.5 [&_[data-cmdk-group-heading]]:text-xs [&_[data-cmdk-group-heading]]:font-medium",
className
)}
{...$$restProps}
>
<slot />
</CommandPrimitive.Group>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import Search from "lucide-svelte/icons/search";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.InputProps;
let className: string | undefined | null = undefined;
export { className as class };
export let value: string = "";
</script>
<div class="flex items-center border-b px-2" data-cmdk-input-wrapper="">
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
class={cn(
"placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
bind:value
/>
</div>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.ItemProps;
export let asChild = false;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Item
{asChild}
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...$$restProps}
let:action
let:attrs
>
<slot {action} {attrs} />
</CommandPrimitive.Item>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.ListProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.List
class={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...$$restProps}
>
<slot />
</CommandPrimitive.List>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.SeparatorProps;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Separator class={cn("bg-border -mx-1 h-px", className)} {...$$restProps} />

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<span
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...$$restProps}
>
<slot />
</span>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { Command as CommandPrimitive } from "cmdk-sv";
import { cn } from "$lib/utils.js";
type $$Props = CommandPrimitive.CommandProps;
export let value: $$Props["value"] = undefined;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<CommandPrimitive.Root
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
bind:value
{...$$restProps}
>
<slot />
</CommandPrimitive.Root>

View file

@ -0,0 +1,37 @@
import { Command as CommandPrimitive } from "cmdk-sv";
import Root from "./command.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
const Loading = CommandPrimitive.Loading;
export {
Root,
Dialog,
Empty,
Group,
Item,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};

View file

@ -26,14 +26,14 @@
)}
{...$$restProps}
>
<div class="relative">
<slot />
<slot />
<!-- <div class="relative">
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-1 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</div>
</div> -->
</DialogPrimitive.Content>
</Dialog.Portal>

View file

@ -0,0 +1,7 @@
import Root from "./skeleton.svelte";
export {
Root,
//
Root as Skeleton,
};

View file

@ -0,0 +1,11 @@
<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("bg-muted animate-pulse rounded-md", className)} {...$$restProps}></div>

View file

@ -1,5 +1,7 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import Lazy from 'svelte-lazy';
const sizes = ['small', 'medium', 'large'];
export let id: number;
@ -17,13 +19,15 @@
const sizeClasses = {
small: 'w-full',
medium: 'w-full',
large: 'w-full'
large: 'w-full',
null: 'w-full'
};
const imageClasses = {
small: 'h-32',
medium: 'h-48',
large: 'h-64'
large: 'h-64',
null: 'h-32'
};
function randomSize(): 'small' | 'medium' | 'large' {
@ -36,35 +40,44 @@
class={`border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 rounded-lg shadow-md overflow-hidden ${sizeClasses[size]} transition-transform duration-300 ease-in-out hover:scale-105 mb-4`}
>
{#if imageUrl}
<img src={imageUrl} alt={title} class={`w-full object-cover ${imageClasses[size]}`} />
<Lazy height={300}>
<img src={imageUrl} alt={title} class={`w-full object-cover ${imageClasses[size]}`} />
</Lazy>
{/if}
<div class="p-4">
<h2 class="mb-2 text-xl font-bold text-zinc-900 dark:text-zinc-100">{title}</h2>
<p class="text-gray-600 dark:text-gray-300">{excerpt}</p>
<div class="flex flex-wrap items-center mt-2 space-x-2 text-sm text-gray-500 dark:text-white">
<Icon icon="mage:medium" class="w-4 h-4 mr-1" />
{#if collection}
<div class="flex items-center">
<img
src="https://miro.medium.com/v2/resize:fill:48:48/{collection.avatarId}"
alt={collection.name.charAt(0)}
loading="eager"
class="w-4 h-4 mr-1 rounded-full no-lightense"
/>
<p>{collection.name}</p>
</div>
<span>·</span>
{/if}
<span>{creator}</span>
<span>·</span>
<span>~{readingTime} min read</span>
<span class="md:inline">·</span>
<span>{publishedAt}</span>
</div>
</div>
{#if bottomImageUrl}
<img src={bottomImageUrl} alt={title} class={`w-full object-cover ${imageClasses[size]}`} />
<Lazy height={300}>
<img
src={bottomImageUrl}
alt={title}
class={`lazy w-full object-cover ${imageClasses[size]}`}
/>
</Lazy>
{/if}
<div
class="flex flex-wrap items-center p-4 mt-2 space-x-2 text-sm text-gray-500 dark:text-white"
>
<Icon icon="mage:medium" class="w-4 h-4 mr-1" />
{#if collection}
<div class="flex items-center">
<img
src="https://miro.medium.com/v2/resize:fill:48:48/{collection.avatarId}"
alt={collection.name.charAt(0)}
loading="eager"
class="w-4 h-4 mr-1 rounded-full no-lightense"
/>
<p>{collection.name}</p>
</div>
<span>·</span>
{/if}
<span>{creator}</span>
<span>·</span>
<span>~{readingTime} min read</span>
<span class="md:inline">·</span>
<span>{publishedAt}</span>
</div>
</div>
</a>

View file

@ -0,0 +1,56 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { Button } from '$lib/components/ui/button/index.js';
</script>
<footer
class="z-20 w-full bg-white border-t shadow-sm dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800"
>
<div
class="container flex flex-col items-center justify-between px-4 py-6 mx-auto space-y-4 md:flex-row md:space-y-0"
>
<div class="text-sm text-center text-zinc-500 dark:text-zinc-400">
By Freedium and its contributors.<br />
Made with ❤️
</div>
<nav class="flex flex-wrap justify-center space-x-4">
<a
href="/about"
class="text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300"
>About</a
>
<a
href="/privacy"
class="text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300"
>Privacy</a
>
<a
href="/terms"
class="text-sm text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300"
>Terms</a
>
</nav>
<div class="flex space-x-2">
<Button
variant="ghost"
size="icon"
href="https://github.com/Freedium-cfd"
target="_blank"
rel="noopener noreferrer"
>
<Icon class="size-5" icon="simple-icons:github" />
</Button>
<Button
variant="ghost"
size="icon"
href="https://codeberg.org/Freedium-cfd"
target="_blank"
rel="noopener noreferrer"
>
<Icon class="size-5" icon="simple-icons:codeberg" />
</Button>
</div>
</div>
</footer>

View file

@ -5,14 +5,22 @@
import ReportProblem from './ReportProblem.svelte';
import PayButtons from './PayButtons.svelte';
import ExtensionsButton from './ExtensionsButton.svelte';
import Icon from '@iconify/svelte';
import SearchDialog from './SearchDialog.svelte';
import { Menu } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button/index.js';
let isNavOpen = false;
const toggleNav = () => {
isNavOpen = !isNavOpen;
};
let isSearchOpen = false;
const toggleSearch = () => {
isSearchOpen = !isSearchOpen;
};
</script>
<nav
@ -22,22 +30,25 @@
<Advertise />
<ProgressLine />
<div class="container flex items-center justify-between h-16 px-4 mx-auto">
<div class="container flex items-center justify-between h-16 px-4 mx-auto space-x-2">
<a class="text-2xl font-bold transition-opacity text-primary hover:opacity-80" href="/"
>Freedium</a
>Freedium βeta</a
>
<button
id="nav-toggle"
on:click={toggleNav}
class="p-2 transition-colors rounded-full md:hidden hover:bg-zinc-100 dark:hover:bg-zinc-800"
aria-label="Toggle navigation menu"
>
<Menu class="w-6 h-6" />
</button>
<div class="flex-grow max-w-md mx-2">
<Button
class="items-center hidden w-full h-8 gap-2 pl-2 pr-3 text-sm transition bg-white rounded-full ui-not-focus-visible:outline-none text-zinc-500 ring-1 ring-zinc-900/10 hover:ring-zinc-900/20 lg:flex dark:bg-white/5 dark:text-zinc-400 dark:ring-inset dark:ring-white/10 dark:hover:ring-white/20"
on:click={toggleSearch}
>
<Icon icon="heroicons:magnifying-glass" />
Search
</Button>
</div>
<div class="items-center hidden space-x-2 md:flex">
<ThemeToggle />
<Button class="lg:hidden" variant="ghost" size="icon" on:click={toggleSearch}>
<Icon class="size-5" icon="heroicons:magnifying-glass" />
</Button>
<ExtensionsButton />
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
<PayButtons name="Ko-fi" url="https://ko-fi.com/zhymabekroman" icon="teenyicons:cup-solid" />
@ -46,9 +57,34 @@
url="https://liberapay.com/ZhymabekRoman/"
icon="simple-icons:liberapay"
/>
<PayButtons name="PayPal" url="" icon="simple-icons:paypal" />
<!-- <PayButtons name="PayPal" url="" icon="simple-icons:paypal" /> -->
<div class="w-px h-6 bg-zinc-300 dark:bg-zinc-700"></div>
<ThemeToggle />
<ReportProblem />
</div>
<div class="flex items-center space-x-2 md:hidden">
<Button class="lg:hidden" variant="ghost" size="icon" on:click={toggleSearch}>
<Icon class="size-5" icon="heroicons:magnifying-glass" />
</Button>
<ThemeToggle />
<Button variant="ghost" size="icon" on:click={toggleNav}>
<Menu />
</Button>
</div>
</div>
{#if isNavOpen}
<div
class="!sticky w-full bg-white border-b shadow-sm md:hidden dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800"
>
<div
class="sticky z-20 flex flex-wrap items-center justify-center px-4 py-2 mx-auto space-x-2 space-y-2"
>
<ReportProblem />
</div>
</div>
{/if}
</nav>
<SearchDialog bind:open={isSearchOpen} />

View file

@ -0,0 +1,33 @@
<script>
import mediumZoom from 'medium-zoom';
export let src = undefined;
export let alt = undefined;
export let options = undefined;
let zoom = null;
function getZoom() {
if (zoom === null) {
zoom = mediumZoom(options);
}
return zoom;
}
function attachZoom(image) {
const zoom = getZoom();
zoom.attach(image);
return {
update(newOptions) {
zoom.update(newOptions);
},
destroy() {
zoom.detach();
}
};
}
</script>
<img {src} {alt} {...$$restProps} use:attachZoom={options} />

View file

@ -1,49 +0,0 @@
<script lang="ts">
import { onMount, afterUpdate } from 'svelte';
export let columnWidth = 300; // Default column width
export let gap = 16; // Gap between items
let container: HTMLElement;
let items: HTMLElement[];
let columns: number = 1;
function updateLayout() {
if (!container) return;
const containerWidth = container.offsetWidth;
columns = Math.floor(containerWidth / (columnWidth + gap));
const actualColumnWidth = (containerWidth - (columns - 1) * gap) / columns;
container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
container.style.gridGap = `${gap}px`;
items.forEach((item, i) => {
const column = i % columns;
const prevItem = items[i - columns];
const top = prevItem ? prevItem.getBoundingClientRect().bottom + gap : 0;
item.style.gridColumn = `${column + 1}`;
item.style.gridRow = `${Math.floor(top / 10)}`;
});
}
onMount(() => {
items = Array.from(container.children) as HTMLElement[];
updateLayout();
window.addEventListener('resize', updateLayout);
});
afterUpdate(updateLayout);
</script>
<div bind:this={container} class="masonry-grid">
<slot></slot>
</div>
<style>
.masonry-grid {
display: grid;
width: 100%;
}
</style>

View file

@ -53,6 +53,10 @@
<RadioGroup.Item value="suggestion" id="suggestion" />
<Label.Root for="suggestion">Suggestion</Label.Root>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="vulnerability" id="vulnerability" />
<Label.Root for="vulnerability">Vulnerability (XSS, etc.)</Label.Root>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="other" id="other" />
<Label.Root for="other">Other</Label.Root>

View file

@ -0,0 +1,126 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Command from '$lib/components/ui/command/index.js';
import { debounce } from 'es-toolkit';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
let value = '';
let searchQuery = '';
export let open = false;
let loading = false;
type Post = {
id: string;
title: string;
date: Date;
excerpt: string;
imageUrl: string;
};
let searchResults: Post[] = [];
const mockSearch = async (query: string): Promise<Post[]> => {
loading = true;
await new Promise((resolve) => setTimeout(resolve, 1500)); // Increased delay for demo
loading = false;
return [
{
id: Math.random().toString(),
title: `Post 1 about ${query}`,
date: new Date(),
excerpt: 'This is a short excerpt of the post...',
imageUrl: 'https://picsum.photos/50/50?random=1'
},
{
id: Math.random().toString(),
title: `Post 2 mentioning ${query}`,
date: new Date(Date.now() - 86400000),
excerpt: 'Another interesting excerpt here...',
imageUrl: 'https://picsum.photos/50/50?random=2'
},
{
id: Math.random().toString(),
title: `Post 3 related to ${query}`,
date: new Date(Date.now() - 172800000),
excerpt: 'Yet another captivating excerpt...',
imageUrl: 'https://picsum.photos/50/50?random=3'
}
];
};
const debouncedSearch = debounce(async (query: string) => {
const searchResultsData = await mockSearch(query);
searchResults = searchResultsData;
// if (searchQuery !== '') {
// }
}, 300);
$: {
if (searchQuery) {
console.log('searchQuery', searchQuery);
debouncedSearch(searchQuery);
} else {
searchResults = [];
loading = false;
debouncedSearch.cancel();
}
}
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
}
const defaultImageUrl = 'https://via.placeholder.com/50';
</script>
<Command.Dialog bind:open bind:value shouldFilter={false} loop>
<Command.Input placeholder="Search posts..." bind:value={searchQuery} />
<Command.List>
<!-- <Command.Empty>No posts found. Try a different search term.</Command.Empty> -->
{#if loading}
{#each Array(3) as _}
<div class="flex items-start py-2 m-2">
<Skeleton class="w-12 h-12 mr-3 rounded" />
<div class="flex flex-col flex-grow">
<Skeleton class="w-3/4 h-5 mb-1" />
<Skeleton class="w-1/4 h-4 mb-1" />
<Skeleton class="w-full h-4" />
</div>
</div>
{/each}
{:else}
{#each searchResults as post}
<Command.Item value={post.id} class="flex items-start py-2">
<img
src={post.imageUrl || defaultImageUrl}
alt={post.title}
class="object-cover w-12 h-12 mr-3 rounded"
/>
<div class="flex flex-col flex-grow">
<span class="font-medium">{post.title}</span>
<div class="mt-1 text-sm text-gray-500">{formatDate(post.date)}</div>
<div class="mt-1 text-sm">{post.excerpt}</div>
</div>
</Command.Item>
{/each}
{/if}
{#if searchResults.length === 0 && !loading}
<Command.Item>
<div class="flex flex-col gap-2">
<span class="text-sm italic text-muted-foreground"
>Start typing post name to search or enter URL.</span
>
<span class="text-sm italic text-muted-foreground">For example:</span>
<span class="text-sm italic text-muted-foreground">
- https://medium.com/post-test-1</span
>
<span class="text-sm italic text-muted-foreground"> - How to create a blog post</span>
</div>
</Command.Item>
{/if}
</Command.List>
</Command.Dialog>

View file

@ -0,0 +1,9 @@
export function formatDate(isoString) {
const date = new Date(isoString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
}

View file

@ -1,9 +1,9 @@
<script lang="ts">
import Header from '$lib/elements/Header.svelte';
import Masonry from 'svelte-bricks';
import HomeBanner from '$lib/elements/HomeBanner.svelte';
import BlogCard from '$lib/elements/BlogCard.svelte';
import Footer from '$lib/elements/Footer.svelte';
const blogPosts = [
{
@ -105,12 +105,9 @@
}
];
// Reactive statement to create items array from blogPosts
$: items = blogPosts.map((post, index) => ({ ...post, id: index }));
// Masonry configuration
let [minColWidth, maxColWidth, gap] = [300, 600, 20];
let width, height;
</script>
<head>
@ -125,3 +122,5 @@
<BlogCard {...item} />
</Masonry>
</div>
<Footer />

View file

@ -0,0 +1,5 @@
export const load = ({ params }) => {
return {
slug: params.slug
};
};

View file

@ -0,0 +1,164 @@
<script>
import Header from '$lib/elements/Header.svelte';
import SvelteMarkdown from 'svelte-markdown';
import { formatDate } from '$lib/utils/dateFormatter';
import { onMount } from 'svelte';
import Icon from '@iconify/svelte';
import ImageZoom from '$lib/elements/ImageZoom.svelte';
let data = {
title: 'UploadThing is 5x Faster',
date: '2024-09-13T12:00:00Z',
author: {
name: 'Theo Browne',
role: 'CEO @ Ping Labs',
avatar: 'https://picsum.photos/seed/post1/400/300'
},
postImage: 'https://picsum.photos/seed/postimage/1200/600', // This can now be undefined or null
content: `
## V7 Is Here!
This release has been an absurd amount of work. So proud of the team and what we've built. Huge thanks to [Julius](#) and [Mark](#) for making this happen.
It is so, so hard to not go straight into the nerdy details, but the whole point of UploadThing is that you don't need to know ANY of those details. With that in mind, here's what's relevant for most of y'all:
- UploadThing is now *way* faster
- Uploads can be paused and resumed seamlessly (huge for users on bad internet connections)
- More details...
## Revolutionary Features
We've completely overhauled our backend infrastructure to bring you unparalleled performance. Our new distributed processing system can handle millions of concurrent uploads without breaking a sweat.
### AI-Powered Optimization
UploadThing now leverages cutting-edge machine learning algorithms to optimize your uploads in real-time. Whether you're uploading images, videos, or documents, our AI will automatically adjust compression and encoding settings to give you the best possible quality at the smallest file size.
## Security Enhancements
We've implemented state-of-the-art encryption protocols to ensure your data remains safe and secure throughout the entire upload process. Our new zero-knowledge architecture means that even we can't access your files without your explicit permission.
### Compliance and Regulations
UploadThing is now fully compliant with GDPR, CCPA, and other major data protection regulations worldwide. We've also obtained ISO 27001 certification, demonstrating our commitment to information security management.
## Future Roadmap
We're not stopping here. Our team is already hard at work on the next big update. Here's a sneak peek of what's coming:
- Quantum-resistant encryption for future-proof security
- Integration with major cloud storage providers for seamless file management
- Advanced analytics dashboard for enterprise users
- Support for emerging file formats and codecs
Stay tuned for more exciting updates as we continue to revolutionize the world of file uploads!
`,
tableOfContents: [
{ id: 'v7-is-here', title: 'V7 Is Here!' },
{ id: 'benchmarks', title: 'Benchmarks' },
{ id: 'the-road-to-v7', title: 'The Road To V7' },
{ id: 'uploadthing-has-served', title: 'UploadThing Has Served...' },
{ id: 'and-were-just-getting-started', title: "...and we're just getting started" }
]
};
let contentLoaded = false;
onMount(() => {
setTimeout(() => {
contentLoaded = true;
}, 500);
});
</script>
<svelte:head>
<title>{data.title} - Freedium</title>
<meta name="description" content="Read about the latest updates to UploadThing" />
</svelte:head>
<Header />
<main class="max-w-5xl px-4 py-8 mx-auto">
<nav class="flex items-center justify-between gap-2 mb-4 text-center">
<a
href="/"
class="flex items-center justify-center transition bg-white rounded-full shadow-md text-primary hover:text-primary/90 group size-8 shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20"
>
<Icon icon="heroicons:arrow-left-20-solid" class="size-6" />
</a>
<a href="/" class="font-bold text-primary hover:text-primary/90"> Original article</a>
</nav>
<div class="lg:flex lg:space-x-8">
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg">
{#if data.postImage}
<ImageZoom
src={data.postImage}
alt="Post cover image"
class="object-cover w-full h-auto max-h-96"
/>
{/if}
<header class="p-6 bg-gray-50">
<p class="mb-2 text-gray-600">{formatDate(data.date)}</p>
<h1 class="mb-4 text-4xl font-bold text-gray-900">{data.title}</h1>
<div class="flex items-center">
<img src={data.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
<div>
<p class="font-semibold text-gray-900">{data.author.name}</p>
<p class="text-gray-600">{data.author.role}</p>
</div>
</div>
</header>
<div class="p-6 {data.postImage ? '' : 'pt-0'}">
<div class="prose max-w-none">
{#if contentLoaded}
<SvelteMarkdown source={data.content} />
{:else}
<p>Loading content...</p>
{/if}
</div>
</div>
</article>
<aside class="order-first mt-7 lg:mt-0 lg:min-w-80 lg:order-none">
<nav
aria-labelledby="toc-heading"
class="w-full p-4 bg-white rounded-lg shadow-lg lg:sticky lg:top-36"
>
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900">Contents</h2>
{#if data.tableOfContents && data.tableOfContents.length > 0}
<ul class="space-y-2">
{#each data.tableOfContents as item}
<li>
<a href={`#${item.id}`} class="transition-colors text-zinc-800 hover:text-zinc-900">
{item.title}
</a>
</li>
{/each}
</ul>
{:else}
<p>No table of contents available</p>
{/if}
</nav>
</aside>
</div>
</main>
<style lang="postcss">
:global(h2) {
@apply font-bold font-sans break-normal text-gray-900 dark:text-gray-100 md:text-2xl;
}
:global(*:not(:first-child) + h2) {
@apply pt-12;
}
:global(.prose p) {
@apply leading-8 mt-7;
}
:global(.prose h3 + p),
:global(.prose h4 + p) {
@apply mt-3;
}
</style>