update the layout

This commit is contained in:
Jesse Winton
2025-02-26 15:03:16 -05:00
parent 3b5d3992d9
commit 79677476c7
8 changed files with 244 additions and 260 deletions

View File

@@ -0,0 +1,9 @@
<script lang="ts">
export let title: string;
</script>
<div class="text-caption text-secondary flex gap-3 pt-8 pb-16">
<span>Blog</span>
<span>/</span>
<span class="text-primary line-clamp-1">{title}</span>
</div>

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import { page } from '$app/state';
import { socialSharingOptions, type SocialShareOption } from '$lib/constants';
import { classNames } from '$lib/utils/classnames';
import { handleCopy } from '$lib/utils/copy';
import { formatDate } from '$lib/utils/date';
import type { AuthorData } from '$routes/blog/content';
export let date: string = new Date().toISOString();
export let timeToRead: string = '0';
export let title: string = '';
export let description: string = '';
export let authorData: Partial<AuthorData> = {};
export let currentURL: string = '';
const getShareLink = (shareOption: SocialShareOption) => {
const blogPostUrl = encodeURI(currentURL);
return shareOption.link.replace('{TITLE}', title + '.').replace('{URL}', blogPostUrl);
};
</script>
<header>
<div class="flex flex-col gap-4">
<div class="text-caption flex gap-2">
<time datetime={date}>{formatDate(date)}</time>
<span></span>
{#if timeToRead}
<span>{timeToRead} min</span>
{/if}
</div>
<h1 class="text-title font-aeonik-pro text-primary">
{title}
</h1>
{#if description}
<p class="text-description text-secondary mt-2">
{description}
</p>
{/if}
</div>
<div class="border-smooth mb-8 flex justify-between border-b py-8">
{#if authorData}
<a href={authorData.href} class="flex items-center gap-2">
{#if authorData.avatar}
<img
class="size-11 rounded-full"
src={authorData.avatar}
alt={authorData.name}
loading="lazy"
width="44"
height="44"
/>
{/if}
<div class="flex flex-col">
<h4 class="text-sub-body text-primary">
{authorData.name}
</h4>
<p class="text-caption">{authorData.role}</p>
</div>
</a>
{/if}
<div class="mt-4 flex items-center gap-4">
<span class="text-micro text-secondary uppercase">SHARE</span>
<ul class="flex gap-2">
{#each socialSharingOptions as sharingOption}
<li
class="bg-smooth flex size-7 items-center justify-center rounded-lg text-white"
>
{#if sharingOption.type === 'link'}
<a
aria-label={sharingOption.label}
href={getShareLink(sharingOption)}
target="_blank"
rel="noopener, noreferrer"
>
<span class={sharingOption.icon} aria-hidden="true" />
</a>
{:else}
<button
aria-label={sharingOption.label}
on:click={() => handleCopy(currentURL)}
>
<span class={sharingOption.icon} aria-hidden="true" />
</button>
{/if}
</li>
{/each}
</ul>
</div>
</div>
</header>

View File

@@ -0,0 +1,49 @@
<script lang="ts" context="module">
export const extractHeadings = () => {
const headings: Array<string> = [];
return headings;
};
</script>
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
export let headings: Array<string> = [
'Accessibility in design systems',
'Use high color contrast',
'Not relying on color'
];
const backToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
let activeIndex: number = 0;
</script>
<nav class="border-smooth col-span-3 ml-4 hidden border-l lg:block">
<span class="text-micro tracking-loose text-primary pl-8 uppercase">Table of Contents</span>
<div class="relative">
<ul class="border-smooth mt-11 ml-7 flex flex-col gap-7 border-b pb-11">
{#each headings as heading, i}
{@const isActive = i === 0}
<li class={classNames(isActive ? 'text-primary' : 'text-secondary', 'relative')}>
{heading}
</li>
{/each}
</ul>
<div
class="bg-primary absolute top-0 -left-px h-6 w-px rounded-full transition duration-500 ease-in-out"
style:transform={`translateY(${activeIndex * 52}px)`}
/>
</div>
<button
class="text-primary group mt-8 flex cursor-pointer items-center gap-2 pl-7 transition-all active:scale-95"
on:click={backToTop}
>
<span class="web-icon-arrow-up transition group-hover:-translate-y-0.5" />
Back to Top
</button>
</nav>

View File

@@ -1,47 +1,11 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { tryCatch } from './try-catch';
async function securedCopy(value: string) { export const copyToClipboard = async (value: string) => {
try { const { data } = await tryCatch(navigator.clipboard.writeText(value));
await navigator.clipboard.writeText(value);
} catch {
return false;
}
return true; return data;
} };
function unsecuredCopy(value: string) {
const textArea = document.createElement('textarea');
textArea.value = value;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let success = true;
try {
document.execCommand('copy');
} catch {
success = false;
} finally {
document.body.removeChild(textArea);
}
return success;
}
export async function copy(value: string) {
// securedCopy works only in HTTPS environment.
// unsecuredCopy works in HTTP and only runs if securedCopy fails.
const success = (await securedCopy(value)) || unsecuredCopy(value);
return success;
}
export function createCopy(value: string) { export function createCopy(value: string) {
const copied = writable(false); const copied = writable(false);
@@ -50,7 +14,7 @@ export function createCopy(value: string) {
function handleCopy() { function handleCopy() {
if (timeout) clearTimeout(timeout); if (timeout) clearTimeout(timeout);
copied.set(true); copied.set(true);
copy(value); copyToClipboard(value);
timeout = setTimeout(() => copied.set(false), 1000); timeout = setTimeout(() => copied.set(false), 1000);
} }
@@ -59,3 +23,23 @@ export function createCopy(value: string) {
copy: handleCopy copy: handleCopy
}; };
} }
export const handleCopy = (value: string, duration: number = 1000) => {
const copied = writable<boolean>(false);
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;
const copy = () => {
if (timeout) clearTimeout(timeout);
copied.set(true);
copyToClipboard(value);
timeout = setTimeout(() => copied.set(false), duration);
};
return {
copied,
copy
};
};
// backward compatibility
export { copyToClipboard as copy };

View File

@@ -1,9 +1,9 @@
import { format } from 'date-fns';
export const formatDate = (date: string | Date | number): string => { export const formatDate = (date: string | Date | number): string => {
const dt = new Date(date); const dt = new Date(date);
const month = dt.toLocaleString('en-US', { month: 'short' });
const day = dt.getDate(); return format(dt, 'MMMM d, yyyy');
const year = dt.getFullYear();
return `${month} ${day}, ${year}`;
}; };
export const addDays = (date: Date, days: number) => { export const addDays = (date: Date, days: number) => {

View File

@@ -0,0 +1,20 @@
type Success<T> = {
data: T;
error: null;
};
type Failure<E> = {
data: null;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
export const tryCatch = async <T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> => {
try {
const data = await promise;
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
};

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Media } from '$lib/UI'; import { Media } from '$lib/UI';
import { scroll } from '$lib/animations';
import { Article, FooterNav, MainFooter, Newsletter, Tooltip } from '$lib/components'; import { Article, FooterNav, MainFooter, Newsletter, Tooltip } from '$lib/components';
import { Main } from '$lib/layouts'; import { Main } from '$lib/layouts';
import { formatDate } from '$lib/utils/date'; import { formatDate } from '$lib/utils/date';
@@ -13,10 +12,11 @@
import type { AuthorData, PostsData } from '$routes/blog/content'; import type { AuthorData, PostsData } from '$routes/blog/content';
import { TITLE_SUFFIX } from '$routes/titles'; import { TITLE_SUFFIX } from '$routes/titles';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { type SocialShareOption, socialSharingOptions } from '$lib/constants'; import { page } from '$app/state';
import { copy } from '$lib/utils/copy';
import { page } from '$app/stores';
import CTA from '$lib/components/BlogCta.svelte'; import CTA from '$lib/components/BlogCta.svelte';
import PostMeta from '$lib/components/blog/post-meta.svelte';
import Breadcrumbs from '$lib/components/blog/breadcrumbs.svelte';
import TableOfContents from '$lib/components/blog/table-of-contents.svelte';
export let title: string; export let title: string;
export let description: string; export let description: string;
@@ -40,31 +40,7 @@
callToAction ??= true; callToAction ??= true;
let readPercentage = 0; const currentURL = `https://appwrite.io${page.url.pathname}`;
const currentURL = `https://appwrite.io${$page.url.pathname}`;
enum CopyStatus {
Copy = 'Copy URL',
Copied = 'Copied'
}
let copyText = CopyStatus.Copy;
async function handleCopy() {
const blogPostUrl = encodeURI(currentURL);
await copy(blogPostUrl);
copyText = CopyStatus.Copied;
setTimeout(() => {
copyText = CopyStatus.Copy;
}, 1000);
}
function getShareLink(shareOption: SocialShareOption): string {
const blogPostUrl = encodeURI(currentURL);
return shareOption.link.replace('{TITLE}', title + '.').replace('{URL}', blogPostUrl);
}
</script> </script>
<svelte:head> <svelte:head>
@@ -109,150 +85,40 @@
</svelte:head> </svelte:head>
<Main> <Main>
<div <div class="py-10">
class="web-big-padding-section" <div class="container">
use:scroll <Breadcrumbs {title} />
on:web-scroll={(e) => { <article class="grid grid-cols-1 gap-4 lg:grid-cols-12">
readPercentage = e.detail.percentage; <div class="lg:col-span-9">
}} <PostMeta {authorData} {title} {timeToRead} {currentURL} {date} {description} />
> {#if cover}
<div class="web-big-padding-section"> <div>
<div class="py-10"> <Media class="block aspect-video" src={cover} />
<div class="web-big-padding-section-level-2"> </div>
<div class="container max-w-[42.5rem]">
<article class="web-main-article">
<header class="web-main-article-header">
<a
class="web-link is-secondary web-u-color-text-secondary items-baseline"
href="/blog"
>
<span class="web-icon-chevron-left" aria-hidden="true" />
<span>Back to blog</span>
</a>
<ul class="web-metadata text-caption">
<li>
<time datetime={date}>{formatDate(date)}</time>
</li>
{#if timeToRead}
<li>{timeToRead} min</li>
{/if}
</ul>
<h1 class="text-title font-aeonik-pro text-primary">
{title}
</h1>
{#if description}
<p class="text-description mt-2">
{description}
</p>
{/if}
{#if authorData}
<div class="web-author mt-4">
<a href={authorData.href} class="flex items-center gap-2">
{#if authorData.avatar}
<img
class="web-author-image"
src={authorData.avatar}
alt={authorData.name}
loading="lazy"
width="44"
height="44"
/>
{/if}
<div class="flex flex-col">
<h4 class="text-sub-body text-primary">
{authorData.name}
</h4>
<p class="text-caption">{authorData.role}</p>
</div>
</a>
</div>
{/if}
<div class="share-post-section mt-4 flex items-center gap-4">
<span class="text-micro pr-2 uppercase" style:color="#adadb0">
SHARE
</span>
<ul class="flex gap-2">
{#each socialSharingOptions as sharingOption}
<li class="share-list-item">
<Tooltip
placement="bottom"
disableHoverableContent={true}
>
{#if sharingOption.type === 'link'}
<a
class="web-icon-button"
aria-label={sharingOption.label}
href={getShareLink(sharingOption)}
target="_blank"
rel="noopener, noreferrer"
>
<span
class={sharingOption.icon}
aria-hidden="true"
/>
</a>
{:else}
<button
class="web-icon-button"
aria-label={sharingOption.label}
on:click={() => handleCopy()}
>
<span
class={sharingOption.icon}
aria-hidden="true"
/>
</button>
{/if}
<svelte:fragment slot="tooltip">
{sharingOption.type === 'copy'
? copyText
: `Share on ${sharingOption.label}`}
</svelte:fragment>
</Tooltip>
</li>
{/each}
</ul>
</div>
</header>
{#if cover}
<div class="web-media-container">
<Media class="web-u-media-ratio-16-9 block" src={cover} />
</div>
{/if}
<div class="web-article-content mt-8">
{#if lastUpdated}
<span class="text-body last-updated-text font-medium">
Updated:
<time dateTime={lastUpdated}>
{formatDate(lastUpdated)}
</time>
</span>
{/if}
<slot />
</div>
</article>
<!-- {#if categories?.length}
<div class="flex gap-4">
{#each categories as cat}
<a href={cat.href} class="web-tag">{cat.name}</a>
{/each}
</div>
{/if} -->
</div>
{#if typeof callToAction === 'boolean'}
<CTA />
{:else if typeof callToAction === 'object'}
<CTA {...callToAction} />
{/if} {/if}
<div class="text-secondary mt-8 flex flex-col gap-8">
{#if lastUpdated}
<span class="text-body last-updated-text font-medium">
Updated:
<time dateTime={lastUpdated}>
{formatDate(lastUpdated)}
</time>
</span>
{/if}
<slot />
</div>
</div> </div>
</div>
<TableOfContents />
</article>
</div> </div>
{#if typeof callToAction === 'boolean'}
<CTA />
{:else if typeof callToAction === 'object'}
<CTA {...callToAction} />
{/if}
</div> </div>
<div class="web-u-sep-block-start pt-10"> <div class="web-u-sep-block-start pt-10">
@@ -288,43 +154,3 @@
</div> </div>
</div> </div>
</Main> </Main>
<div class="progress-bar" style:--percentage="{readPercentage * 100}%" />
<style lang="scss">
.progress-bar {
position: fixed;
top: 0;
height: 2px;
width: var(--percentage);
background: hsl(var(--web-color-accent));
z-index: 10000;
}
@media (min-width: 1024px) {
.web-main-article-header {
padding-block-end: 0;
border-block-end: unset;
}
}
.share-post-section {
padding: 16px 0;
border-block-end: solid 0.0625rem hsl(var(--web-color-border));
border-block-start: solid 0.0625rem hsl(var(--web-color-border));
}
.web-icon-button {
.web-icon-x {
font-size: 16px;
}
.web-icon-copy {
font-size: 24px;
}
.last-updated-text {
color: var(--primary, #e4e4e7);
}
}
</style>

View File

@@ -11,8 +11,8 @@
const tag = `h${level + 1}`; const tag = `h${level + 1}`;
const ctx = hasContext('headings') ? getContext<LayoutContext>('headings') : undefined; const ctx = hasContext('headings') ? getContext<LayoutContext>('headings') : undefined;
const classList: Record<typeof level, string> = { const classList: Record<typeof level, string> = {
1: 'text-label mb-4', 1: 'text-description mb-4',
2: 'text-description mb-4', 2: 'text-description text-primary mb-4',
3: 'text-body font-medium mb-4', 3: 'text-body font-medium mb-4',
4: 'text-sub-body font-medium' 4: 'text-sub-body font-medium'
}; };
@@ -24,6 +24,8 @@
return; return;
} }
console.log(element.textContent);
$ctx = { $ctx = {
...$ctx, ...$ctx,
[id]: { [id]: {
@@ -40,6 +42,7 @@
} }
}); });
}; };
const observer = new IntersectionObserver(callback, { const observer = new IntersectionObserver(callback, {
root: null, root: null,
threshold: 1 threshold: 1
@@ -60,7 +63,7 @@
bind:this={element} bind:this={element}
class:web-snap-location={id && !inReferences} class:web-snap-location={id && !inReferences}
class:web-snap-location-references={id && inReferences} class:web-snap-location-references={id && inReferences}
class="{headingClass} text-primary" class="{headingClass} text-primary font-medium"
> >
<a href={`#${id}`} class=""><slot /></a> <a href={`#${id}`} class=""><slot /></a>
</svelte:element> </svelte:element>
@@ -68,7 +71,7 @@
<svelte:element <svelte:element
this={tag} this={tag}
bind:this={element} bind:this={element}
class="{headingClass} text-primary" class="{headingClass} text-primary font-medium"
class:in-policy={inPolicy} class:in-policy={inPolicy}
> >
<slot /> <slot />