Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@
"test:unit": "vitest run"
},
"devDependencies": {
"@playwright/test": "^1.58.1",
"@playwright/test": "^1.58.2",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/enhanced-img": "^0.9.3",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/enhanced-img": "^0.10.2",
"@sveltejs/kit": "^2.52.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"eslint": "^9.39.2",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0",
"jsdom": "^28.0.0",
"eslint-plugin-svelte": "^3.15.0",
"jsdom": "^28.1.0",
"mdsvex": "^0.12.6",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"svelte": "^5.49.1",
"svelte": "^5.51.2",
"unplugin-icons": "^23.0.1",
"vite": "^7.3.1",
"vitest": "^4.0.18"
Expand Down
20 changes: 20 additions & 0 deletions src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -287,19 +287,26 @@ div.description {
// Internal navigation
a.nav {
position: relative;
color: var(--txt);
text-decoration: none;
.arrow {
position: absolute;
top: -0.02em;
transform: rotate(-72deg) scale(1, 0);
transition: $transition-base;
transform-origin: 50% 53%;
left: -0.2em;
display: inline-block;
width: auto;
text-align: left;
}
.slash {
display: inline-block;
transition: $transition-base;
transform: scale(1, 1) rotate(0deg);
line-height: 0;
width: auto;
text-align: left;
}
&:hover {
.arrow {
Expand All @@ -314,13 +321,18 @@ a.nav {
// External links
a.external {
position: relative;
color: var(--txt-2);
text-decoration: none;
.arrow {
display: inline-block;
margin-left: 0.4ch;
transition: $transition-fast;
color: var(--txt-2);
width: auto;
text-align: left;
}
&:hover {
color: var(--txt);
text-decoration: underline;
.arrow {
transform: translateY(-0.4ch) translateX(-0.2ch) scale(1, 1) rotate(-30deg);
Expand All @@ -330,19 +342,25 @@ a.external {

// Internal links
a.link {
color: var(--txt);
text-decoration: none;
.arrow {
display: inline-block;
margin-left: 0.5ch;
opacity: 0;
transform: translateX(-0.6em);
transition: $transition-fast;
width: auto;
text-align: left;
}
.slash {
display: inline-block;
transition: $transition-base;
transform: scale(1, 1) rotate(0deg);
line-height: 0;
opacity: 0;
width: 1ch;
text-align: center;
}
&:hover {
.arrow {
Expand Down Expand Up @@ -413,6 +431,8 @@ figure {
&:hover {
&::after {
content: '#';
margin-left: 0.3ch;
color: var(--txt-3);
}
}
}
Expand Down
114 changes: 85 additions & 29 deletions src/lib/components/Image.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,118 @@

let loaded = false;

async function importImage(image) {
if (!image || typeof image !== 'string') {
return null;
const lazyPictures = import.meta.glob(
`/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`,
{
import: 'default',
query: {
enhanced: true,
w: '2400;2000;1600;1200;800;400'
}
}
);

const pictures = import.meta.glob(`/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`, {
const eagerPictures = import.meta.glob(
`/src/content/**/*.{avif,gif,heif,jpeg,jpg,png,tiff,webp}`,
{
eager: true,
import: 'default',
query: {
enhanced: true,
w: '2400;2000;1600;1200;800;400'
}
});
}
);

const isSSR = import.meta.env.SSR;

// Simple exact filename match
for (const [path, src] of Object.entries(pictures)) {
function findModule(modules, image) {
if (!image || typeof image !== 'string') {
return null;
}

// Normalize path for relative references
const normalizedImage = image.replace(/^\.\//, '').toLowerCase();

for (const [path, src] of Object.entries(modules)) {
const normalizedPath = path.toLowerCase();
const fileName = path.split('/').pop();
if (fileName === image) {
try {
return await src();
} catch (error) {
console.error('Error loading image:', error);
return null;
}
const normalizedFileName = fileName?.toLowerCase();

if (
normalizedFileName === normalizedImage ||
normalizedPath.endsWith(`/${normalizedImage}`)
) {
return src;
}
}

return null;
}

async function importImage(image) {
const module = findModule(lazyPictures, image);
if (!module) return null;

try {
return typeof module === 'function' ? await module() : module;
} catch (error) {
console.error('Error loading image:', error);
return null;
}
}

function resolveImageSync(image) {
const module = findModule(eagerPictures, image);
if (!module) return null;
return typeof module === 'function' ? null : module;
}

$: resolvedImage = isSSR ? resolveImageSync(image) : null;

function handleLoad() {
loaded = true;
}
</script>

<picture>
{#await importImage(image)}
<div class="loading">Loading...</div>
{:then src}
{#if src}
<source srcset={src.sources.avif} type="image/avif" {sizes} />
<source srcset={src.sources.webp} type="image/webp" {sizes} />
{#if isSSR}
{#if resolvedImage}
<source srcset={resolvedImage.sources.avif} type="image/avif" {sizes} />
<source srcset={resolvedImage.sources.webp} type="image/webp" {sizes} />
<img
src={src.img.src}
src={resolvedImage.img.src}
{alt}
{loading}
on:load={handleLoad}
class:loaded
width={src.img.w}
height={src.img.h}
width={resolvedImage.img.w}
height={resolvedImage.img.h}
/>
{:else}
<div class="error">Image not found: {image}</div>
<div class="error">{alt || 'Image unavailable'}</div>
{/if}
{:catch error}
<div class="error">Error loading image: {error.message}</div>
{/await}
{:else}
{#await importImage(image)}
<div class="loading">Loading...</div>
{:then src}
{#if src}
<source srcset={src.sources.avif} type="image/avif" {sizes} />
<source srcset={src.sources.webp} type="image/webp" {sizes} />
<img
src={src.img.src}
{alt}
{loading}
on:load={handleLoad}
class:loaded
width={src.img.w}
height={src.img.h}
/>
{:else}
<div class="error">{alt || 'Image unavailable'}</div>
{/if}
{:catch error}
<div class="error">{alt || 'Image unavailable'}</div>
{/await}
{/if}
</picture>

<style lang="scss">
Expand Down
48 changes: 48 additions & 0 deletions src/lib/components/NavLogo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script>
import { onMount } from 'svelte';
import { theme } from '$lib/js/theme';
import pfpinDark from '$lib/assets/pfpin-dark.json?raw';
import pfpinLight from '$lib/assets/pfpin-light.json?raw';

export let size = '2rem';

let container;
let lottie;
let animation;

$: currentTheme = $theme;

function loadAnimation(theme) {
if (!lottie || !container) return;

if (animation) {
animation.destroy();
}

animation = lottie.loadAnimation({
name: 'nav-logo',
container,
renderer: 'svg',
loop: true,
autoplay: true,
animationData: theme === 'dark' ? JSON.parse(pfpinDark) : JSON.parse(pfpinLight)
});
}

onMount(async () => {
lottie = await import('lottie-web/build/player/lottie_light.min.js');
loadAnimation(currentTheme);
});

$: if (lottie) {
loadAnimation(currentTheme);
}
</script>

<div class="logo" style={`width: ${size}; height: ${size};`} bind:this={container}></div>

<style lang="scss">
.logo {
display: inline-flex;
}
</style>
43 changes: 36 additions & 7 deletions src/lib/components/PageHead.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,61 @@
export let title;
export let description;
export let type = 'website';
export let image = {};
export let image = null;
export let jsonLd = null;

const hostname = 'https://techquests.dev';
const siteName = 'Tech Quests';
const authorName = 'Andre Nogueira';

$: canonicalUrl = $page.url.href.replace(/^http:\/\/[^/]+/, hostname);
$: canonicalUrl = `${hostname}${$page.url.pathname}`;
$: hasImage = Boolean(image?.img?.src);
$: twitterCard = hasImage ? 'summary_large_image' : 'summary';
</script>

<svelte:head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:site_name" content="Tech Quests" />
<meta name="author" content={authorName} />
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="canonical" href={canonicalUrl} />
<link
rel="alternate"
type="application/rss+xml"
title="Tech Quests RSS"
href={`${hostname}/rss.xml`}
/>
<meta property="og:site_name" content={siteName} />
<meta property="og:locale" content="en_US" />
<meta property="og:title" content={title} />
<meta property="og:type" content={type} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta name="twitter:card" content="summary" />
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:site" content="@0xaanogueira" />
<meta name="twitter:creator" content="@0xaanogueira" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{#if image}
{#if hasImage}
<meta property="og:image" content={hostname + image.img.src} />
<meta property="og:image:width" content={image.img.w} />
<meta property="og:image:height" content={image.img.h} />
{#if image.img.w && image.img.h}
<meta property="og:image:width" content={image.img.w} />
<meta property="og:image:height" content={image.img.h} />
{/if}
<meta property="og:image:alt" content={title} />
<meta name="twitter:image" content={hostname + image.img.src} />
<meta name="twitter:image:alt" content={title} />
{:else}
<meta property="og:image" content={`${hostname}/blog.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={siteName} />
<meta name="twitter:image" content={`${hostname}/blog.png`} />
<meta name="twitter:image:alt" content={siteName} />
{/if}
{#if jsonLd}
<script type="application/ld+json">
{@html JSON.stringify(jsonLd)}
</script>
{/if}
</svelte:head>
Loading
Loading