Compare commits

...

9 Commits

Author SHA1 Message Date
834e176841 trying and half-failing to get image preloading to work lol 2025-11-18 13:21:41 +13:00
bad4457ad7 Fix images looking weird on mobile 2025-11-18 12:54:27 +13:00
b2657962e1 Add much more responsible Swiper.JS image carousel over self-made one 2025-11-18 12:51:59 +13:00
4992379487 Remove border... whoops 2025-11-18 12:34:11 +13:00
1f499739fb Add smooth scrolling to TOC 2025-11-18 12:33:23 +13:00
7972acb40f Add tag page 2025-11-18 12:27:01 +13:00
e06bb64645 Add header images to post content 2025-11-18 12:04:38 +13:00
3918171086 slight tweaks 2025-11-18 11:40:48 +13:00
1e9489bb14 Start making new post card 2025-11-18 11:37:49 +13:00
9 changed files with 182 additions and 77 deletions

20
web/package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"astro": "^5.15.4", "astro": "^5.15.4",
"pocketbase": "^0.26.3", "pocketbase": "^0.26.3",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
} }
}, },
@@ -4966,6 +4967,25 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/swiper": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz",
"integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.17", "version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",

View File

@@ -13,6 +13,7 @@
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"astro": "^5.15.4", "astro": "^5.15.4",
"pocketbase": "^0.26.3", "pocketbase": "^0.26.3",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17"
} }
} }

View File

@@ -1,6 +1,5 @@
--- ---
import { authPB } from "src/utils/pocketbase"; import { authPB } from "src/utils/pocketbase";
import type { RecordModel } from 'pocketbase'
// export const prerender = false // export const prerender = false
@@ -9,65 +8,97 @@ const photos = await pb.collection('photos').getFullList({
sort: '-created' sort: '-created'
}) })
const getImageLink = async (record: any) => { const photoLinks = await Promise.all(
return pb.files.getURL(record, record.image) photos.map(p => pb.files.getURL(p, p.image))
} )
--- ---
<script> <script>
let pos = 0; import Swiper from 'swiper';
import { Navigation, Keyboard, EffectFade } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/effect-fade';
const dataElement = document.getElementById('carousel-data'); const dataElement = document.getElementById('carousel-data');
const photos = dataElement ? JSON.parse(dataElement.textContent || '[]') : []; const photos = dataElement ? JSON.parse(dataElement.textContent || '[]') : [];
const cap = photos.length - 1; const photoLinksElement = document.getElementById('photo-links-data');
const img = document.getElementById('carousel-img') as HTMLImageElement; const photoLinks = photoLinksElement ? JSON.parse(photoLinksElement.textContent || '[]') : [];
const titleEl = document.getElementById('photo-title'); const titleEl = document.getElementById('photo-title');
const cameraEl = document.getElementById('photo-camera'); const cameraEl = document.getElementById('photo-camera');
const locationEl = document.getElementById('photo-location'); const locationEl = document.getElementById('photo-location');
const currentPosIndicator = document.getElementById('current-pos'); const currentPosIndicator = document.getElementById('current-pos');
const maxPosIndicator = document.getElementById('max-pos'); const maxPosIndicator = document.getElementById('max-pos');
maxPosIndicator!.innerText = (cap + 1) as string if (maxPosIndicator) {
currentPosIndicator!.innerText = (pos + 1) as string maxPosIndicator.innerText = String(photos.length);
}
function updatePhoto() { if (currentPosIndicator) {
const currentPhoto = photos[pos]; currentPosIndicator.innerText = '1';
currentPosIndicator!.innerText = (pos + 1) as string
// Update image src
if (img && currentPhoto) {
const imageUrl = `/api/files/photos/${currentPhoto.id}/${currentPhoto.image}`;
img.src = imageUrl;
} }
// Update metadata function preloadImages() {
if (titleEl) { photoLinks.forEach((url: string) => {
const preloadImg = new Image();
preloadImg.src = url;
});
}
// Call preload on page load
preloadImages();
const swiper = new Swiper('.swiper', {
modules: [Navigation, Keyboard, EffectFade],
effect: 'cards',
fadeEffect: {
crossFade: true
},
speed: 250,
navigation: {
nextEl: '#inc-button',
prevEl: '#dec-button',
},
keyboard: {
enabled: true,
onlyInViewport: false,
},
loop: true,
on: {
slideChange: function() {
const realIndex = this.realIndex;
const currentPhoto = photos[realIndex];
if (currentPosIndicator) {
currentPosIndicator.innerText = String(realIndex + 1);
}
if (titleEl && currentPhoto) {
titleEl.textContent = currentPhoto.title || "Untitled"; titleEl.textContent = currentPhoto.title || "Untitled";
} }
if (cameraEl) { if (cameraEl && currentPhoto) {
cameraEl.textContent = `📸 ${currentPhoto.camera}` cameraEl.textContent = `📸 ${currentPhoto.camera}`;
} }
if (locationEl) { if (locationEl && currentPhoto) {
locationEl.textContent = `📌 ${currentPhoto.location}` locationEl.textContent = `📌 ${currentPhoto.location}`;
} }
} }
function inc() {
pos = pos === cap ? 0 : pos + 1;
updatePhoto();
} }
});
function dec() {
pos = pos === 0 ? cap : pos - 1;
updatePhoto();
}
// make functions globally accessible
(window as any).inc = inc;
(window as any).dec = dec;
</script> </script>
<!-- Hidden element to pass server data to client --> <!-- Hidden element to pass server data to client -->
<div class="hidden" id="carousel-data">{JSON.stringify(photos)}</div> <div class="hidden" id="carousel-data">{JSON.stringify(photos)}</div>
<div class="hidden" id="photo-links-data">{JSON.stringify(photoLinks)}</div>
<img id="carousel-img" class="w-full md:h-[calc(100vh-7rem)] object-contain" src={ pb.files.getURL(photos[0], photos[0].image) } /> <div class="swiper w-full h-[60vh] md:h-[calc(100vh-7rem)]">
<div class="swiper-wrapper">
{photos.map((photo, i) => (
<div class="swiper-slide flex items-center justify-center">
<img
src={photoLinks[i]}
class="w-full h-full object-contain"
alt={photo.title || 'Photo'}
/>
</div>
))}
</div>
</div>

View File

@@ -16,6 +16,20 @@
lnk.className = 'b1-no-underline' lnk.className = 'b1-no-underline'
lnk.innerHTML = `<span class="mr-1">${"#".repeat(depth)}</span>${tag.innerHTML}` lnk.innerHTML = `<span class="mr-1">${"#".repeat(depth)}</span>${tag.innerHTML}`
// Add smooth scroll with offset
lnk.addEventListener('click', (e) => {
e.preventDefault()
const target = document.getElementById(tag.id)
if (target) {
const offset = 20 // pixels from top
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
})
}
})
let li = document.createElement('li') let li = document.createElement('li')
li.className = 'line-clamp-1' li.className = 'line-clamp-1'
li.appendChild(lnk) li.appendChild(lnk)

View File

@@ -6,13 +6,24 @@ const pb = await authPB()
const { p } = Astro.props const { p } = Astro.props
--- ---
<div class="mb-5 w-2/3"> <div class="mb-8 pb-4 md:pb-8 border-b border-gray-200 last:border-b-0 flex flex-col md:flex-row gap-4 md:gap-3">
<time datetime={p['publishDate']} class=" text-gray-500">{getFormattedDate(p['publishDate'])}</time> <!-- Text content on left -->
<div> <div class="flex-1 flex flex-col justify-start order-2 md:order-1">
<a href={`/posts/${p['slug']}/`} rel="prefetch">
<img src={ pb.files.getURL(p, p.headerImage) } class="rounded-xl border-12 border-white"/> <time datetime={p.publishDate} class="text-sm text-gray-500">{getFormattedDate(p.publishDate)}</time>
{p['title']} <a href={`/posts/${p.slug}/`} rel="prefetch" class="no-underline hover:underline">
<h2 class="text-xl font-bold ">{p.title}</h2>
</a>
{
p.description !== "" &&
<q class="line-clamp-3 block italic text-gray-700">{p['description']}</q>
}
</div>
<!-- Image on right -->
<div class="md:w-80 order-1 md:order-2">
<a href={`/posts/${p.slug}/`} rel="prefetch">
<img src={ pb.files.getURL(p, p.headerImage) } class="rounded-sm w-full h-48 md:h-full object-cover" alt={p['title']}/>
</a> </a>
</div> </div>
<q class="line-clamp-3 block italic">{p['description']}</q>
</div> </div>

View File

@@ -17,13 +17,9 @@ const photos = await pb.collection('photos').getFullList({
<p id="photo-camera" class="text-sm">📸 {photos[0].camera}</p> <p id="photo-camera" class="text-sm">📸 {photos[0].camera}</p>
<p id="photo-location" class="text-sm">📌 {photos[0].location}</p> <p id="photo-location" class="text-sm">📌 {photos[0].location}</p>
<button id="dec-button" onclick="dec()">&lt;</button> <!-- <button id="dec-button" onclick="dec()">&lt;</button> -->
<span id="current-pos" /> of <span id="max-pos" /> <span id="current-pos" /> of <span id="max-pos" />
<button id="inc-button" onclick="inc()">&gt;</button> <!-- <button id="inc-button" onclick="inc()">&gt;</button> -->
<!-- <p class="text-2xl">Photography :)</p>
<p class="text-left">Chuck me an email if you'd like a print of these, I'll see what I can do</p> -->
</div> </div>
<div slot="content" class=""> <div slot="content" class="">

View File

@@ -22,10 +22,9 @@ const readTime = calcReadTime(wordCount);
<Base> <Base>
<div slot="sidebar" class="text-left"> <div slot="sidebar" class="text-left">
<p class="text-2xl">{title}</p> <img src={headerImage}/>
<!-- <img src={headerImage}/> -->
<p class="text-2xl pt-2">{title}</p>
<p class="italic">{description}</p> <p class="italic">{description}</p>
<PostInfo <PostInfo
@@ -37,18 +36,22 @@ const readTime = calcReadTime(wordCount);
<TableOfContents/> <TableOfContents/>
</div> </div>
<div id="post-content" slot="content" class="py-12"> <div id="post-content" slot="content" class="md:py-12">
<Fragment set:html={content} /> <Fragment set:html={content} />
</div> </div>
<style is:global>
html {
scroll-behavior: smooth;
}
<style is:global>
#post-content img { #post-content img {
width: 90%; width: 90%;
margin-inline: auto; margin-inline: auto;
display: block; display: block;
} }
</style> </style>
</Base> </Base>

View File

@@ -24,7 +24,7 @@ const uniqueTags = tags.filter((v, i, a) => a.indexOf(v) === i)
<TagList tags={uniqueTags}/> <TagList tags={uniqueTags}/>
</div> </div>
<div slot="content" class="py-12"> <div slot="content" class="md:py-12">
{ posts.map(p => <PostCard p={p}/>) } { posts.map(p => <PostCard p={p}/>) }
</div> </div>
</Base> </Base>

View File

@@ -1,4 +1,33 @@
--- ---
import Base from "@layout/Base"
import PostCard from '@components/postList/postCard'
import { authPB } from "@utils/pocketbase"
export const prerender = false
const { tag } = Astro.params
const pb = await authPB()
const posts = await pb.collection('posts').getFullList({
sort: '-publishDate',
filter: `tags ~ "${tag}" && published=true`
})
const postCount = posts.length
const plural = postCount === 1 ? "post" : "posts"
---
export const prerender = false <Base>
---
<div slot="sidebar">
<a href="javascript:history.back()">Back</a>
<p class="text-xl">{postCount} {plural} with tag <code class="bg-gray-300 py-1 px-2 text-sm rounded-md">{tag}</code></p>
</div>
<div slot="content" class="md:py-12">
{
posts.map(p => <PostCard p={p}/>)
}
</div>
</Base>