Skip to content

Commit

Permalink
fix: Break apart image serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
kiosion committed Nov 13, 2023
1 parent 2be6e87 commit 714935c
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 197 deletions.
16 changes: 14 additions & 2 deletions svelte-app/src/components/document/content/header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import ArrowButton from '$components/controls/arrow-button.svelte';
import Divider from '$components/divider.svelte';
import Icon from '$components/icon.svelte';
import Image from '$components/images/image.svelte';
import Link from '$components/link.svelte';
import ImageCarousel from '$components/portable-text/image-carousel.svelte';
import type { PostDocument, ProjectDocument, ProjectImage } from '$types';
import type { PostDocument, ProjectDocument, ProjectImage, RouteFetch } from '$types';
export let data: PostDocument | ProjectDocument,
routeFetch: RouteFetch,
images: ProjectImage[] | undefined = undefined,
model = data._type;
</script>
Expand Down Expand Up @@ -90,7 +92,17 @@

{#if data._type === 'project' && images?.length}
<Divider />
<ImageCarousel {images} />
{#if images.length > 1}
<ImageCarousel {images} />
{:else}
<Image
image={images[0].sanityAsset}
placeholder={images[0].placeholder}
crop={images[0].crop}
srcPromise={images[0].asset}
{routeFetch}
/>
{/if}
{/if}
</div>

Expand Down
51 changes: 51 additions & 0 deletions svelte-app/src/components/images/image-modal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { BASE_ANIMATION_DURATION } from '$lib/consts';
export let dialog: HTMLDialogElement,
show = false;
</script>

<svelte:window
on:keydown={(e) => {
if (show) {
switch (e.key) {
case 'Escape':
show = false;
break;
case 'Tab':
e.preventDefault();
dialog.focus();
break;
}
}
}}
/>

{#if show}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog
bind:this={dialog}
on:click={() => (show = false)}
on:keydown={(e) => {
if (e.key === 'Escape') {
show = false;
}
}}
in:fade={{ duration: BASE_ANIMATION_DURATION }}
out:fade={{ duration: BASE_ANIMATION_DURATION }}
>
<div><slot /></div>
</dialog>
{/if}

<style lang="scss">
dialog {
@apply fixed inset-0 z-50 flex h-full w-full flex-col items-center justify-center bg-black/80;
div {
@apply relative flex h-full max-h-full w-full max-w-full flex-col items-center justify-center p-8;
}
}
</style>
173 changes: 173 additions & 0 deletions svelte-app/src/components/images/image.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<script lang="ts">
import { onMount } from 'svelte';
import { crossfade, fade } from 'svelte/transition';
import { BASE_ANIMATION_DURATION } from '$lib/consts';
import { buildImageUrl, getCrop } from '$lib/helpers/image';
import ImageModal from '$components/images/image-modal.svelte';
import Spinner from '$components/loading/spinner.svelte';
import type { RouteFetch, SanityImageObject } from '$types';
export let image: SanityImageObject,
routeFetch: RouteFetch,
placeholder: string | undefined = undefined,
crop: SanityImageObject['crop'] & { width: number; height: number } = getCrop(image),
srcPromise: Promise<string> | undefined = undefined;
const { _key } = image,
{ _ref } = image.asset,
imgDimensions = {
width: Math.min(crop.width, 1200),
height: Math.min(crop.width, 1200) * (crop.height / crop.width)
},
placeholderSrc =
placeholder || buildImageUrl({ ref: _ref, crop, width: 30, blur: 40 }),
style = `max-width: ${imgDimensions.width}px; max-height: ${imgDimensions.height}px; aspect-ratio: ${imgDimensions.width} / ${imgDimensions.height};`,
placeholderStyle = `${style} position: absolute; top: 0; left: 0; opacity: 50%;`,
[send, receive] = crossfade({
duration: (d: number) => Math.sqrt(d * 200),
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 300,
easing: params.easing,
css: (t) => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
let fullSrc: string,
dialog: HTMLDialogElement,
showImageModal = false;
onMount(() => {
srcPromise ||= routeFetch(
buildImageUrl({ ref: _ref, crop, width: imgDimensions.width })
).then(async (res) => {
const mimeType =
res.headers.get('content-type') ||
res.headers.get('Content-Type') ||
'image/jpeg',
ab = await res.arrayBuffer(),
buf = new Uint8Array(ab).reduce(
(data, byte) => data + String.fromCharCode(byte),
''
);
return `data:${mimeType};base64,${btoa(buf)}`;
});
srcPromise.then((res) => (fullSrc = res));
});
</script>

<div>
{#await srcPromise || new Promise((_res) => {})}
<div class="loading"><Spinner /></div>
<!-- svelte-ignore a11y-missing-attribute -->
<img src={placeholderSrc} draggable="false" {style} />
{:then src}
<button
{style}
on:click={() => {
showImageModal = true;
}}
on:keydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
showImageModal = true;
}
}}
>
{#if showImageModal}
<img
src={placeholderSrc}
alt={_key}
draggable="false"
style={placeholderStyle}
out:fade={{ duration: BASE_ANIMATION_DURATION }}
/>
{:else}
<img
{src}
alt={_key}
draggable="false"
in:receive={{ key: _key, duration: BASE_ANIMATION_DURATION }}
out:send={{ key: _key, duration: BASE_ANIMATION_DURATION }}
/>
{/if}
</button>
{:catch e}
<p class="error">Error: {e?.message || e}</p>
<img src={placeholderSrc} alt={_key} draggable="false" {style} />
{/await}
<img
class="backdrop"
src={placeholderSrc}
alt={_key}
draggable="false"
aria-hidden="true"
style={placeholderStyle}
/>
</div>

<ImageModal bind:dialog bind:show={showImageModal}>
<img
src={fullSrc}
draggable="false"
alt={_key}
{style}
in:receive={{ key: _key, duration: BASE_ANIMATION_DURATION }}
out:send={{ key: _key, duration: BASE_ANIMATION_DURATION }}
/>
</ImageModal>

<style lang="scss">
@import '@styles/mixins';
div:not(.loading) {
@apply relative w-full;
.backdrop {
@apply transition-opacity;
z-index: -1;
filter: blur(20px);
opacity: 0.2 !important;
}
&:hover,
&:focus-visible {
.backdrop {
opacity: 0.3 !important;
}
}
}
button {
@apply relative block max-h-fit w-full rounded-sm;
@include focus-state(sm);
}
img {
@apply mx-auto w-full select-none rounded-sm;
}
.error,
.loading {
@apply absolute left-1/2 top-1/2 h-fit w-fit max-w-full -translate-x-1/2 -translate-y-1/2 transform text-center font-code text-base;
}
:global(.dark) {
button {
@include focus-state(sm, dark);
}
}
</style>
Loading

0 comments on commit 714935c

Please sign in to comment.