Compare commits

...

34 Commits

Author SHA1 Message Date
a25500bb91 Removed swiper from album section 2025-11-22 17:19:22 +13:00
c3e05b44ea removed photos page... scary 2025-11-22 16:54:17 +13:00
b4f8a42e05 Disabled about page, enabled photos nav buttons, other fluff 2025-11-22 16:17:04 +13:00
ff486c01e3 Added album remarks, lazy loading images 2025-11-22 16:03:50 +13:00
33ead9d968 refactor 2025-11-22 15:28:17 +13:00
20f8b38c8a Add detail page for album 2025-11-22 15:18:57 +13:00
7f0b65fa20 makin an albums page :3 2025-11-22 15:03:49 +13:00
2fdaf64b47 removed paragraph from index 2025-11-20 11:18:20 +13:00
4bad9e095e Add field for film used in photos 2025-11-19 23:02:40 +13:00
facb2b0f99 update post info to use border separators 2025-11-19 12:14:31 +13:00
ef0c8dd981 Add spacing between Now entries 2025-11-19 11:55:58 +13:00
32b9a29028 Add header/metadata fields 2025-11-19 11:48:29 +13:00
1da77d6074 Add last updated section to now page 2025-11-18 19:21:48 +13:00
64ecc338be Add now page! 2025-11-18 18:39:10 +13:00
66623a64b4 increase thumbnail size 2025-11-18 17:11:48 +13:00
6997e313e6 redesigned H1, i should try get away from tailwind... 2025-11-18 15:44:43 +13:00
435ca0f2d7 h1-3 font sizes 2025-11-18 15:31:47 +13:00
3e742ba952 Improve TOC handling 2025-11-18 15:28:51 +13:00
caad696191 add post link 2025-11-18 15:18:10 +13:00
51419e4c7c yeah :) 2025-11-18 15:16:20 +13:00
4310c90f48 reaallly not sure on some of this stuff now huh 2025-11-18 15:13:46 +13:00
3378844823 Add recent posts section to index 2025-11-18 14:53:10 +13:00
02c7e2f45a increased size of photos on mobile 2025-11-18 14:23:32 +13:00
f0d58a1a33 even more improve 2025-11-18 14:19:17 +13:00
a37955cbec Greatly improve postCard design 2025-11-18 14:15:05 +13:00
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
22 changed files with 470 additions and 126 deletions

20
web/package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@tailwindcss/vite": "^4.1.17",
"astro": "^5.15.4",
"pocketbase": "^0.26.3",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17"
}
},
@@ -4966,6 +4967,25 @@
"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": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",

View File

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

View File

@@ -0,0 +1,18 @@
---
import { authPB } from "@utils/pocketbase"
const { a } = Astro.props
const pb = await authPB()
---
<div class="swiper-slide flex flex-col md:flex-row items-center justify-center">
<a class="b1-no-underline" href={`/albums/${encodeURI(a.title)}`}>
<h1 class="text-center pb-0 md:flex-1">{a.title}</h1>
<p class="text-center">{a.date}</p>
<img
src={pb.files.getURL(a, a.images[0])}
class="w-full h-full object-contain"
loading="lazy"
alt={a.title}
/>
</a>
</div>

View File

@@ -12,10 +12,14 @@ const links = [
txt: "email",
lnk: "mailto:contact@breadone.net"
},
{
txt: "copyright",
lnk: "/copyright"
}
// {
// txt: "about",
// lnk: "/about"
// },
// {
// txt: "copyright",
// lnk: "/copyright"
// }
]
---

View File

@@ -4,9 +4,13 @@ const links = [
txt: 'now',
lnk: '/now'
},
// {
// txt: 'photos',
// lnk: '/photos'
// },
{
txt: 'photos',
lnk: '/photos'
txt: 'albums',
lnk: '/albums'
},
{
txt: 'posts',

View File

@@ -0,0 +1,39 @@
---
import SummaryCard from "./summaryCard.astro";
import { authPB } from "@utils/pocketbase";
import { getFormattedDate } from "@utils/date";
const pb = await authPB()
const posts = await pb.collection('posts').getList(1, 3, {
sort: '-publishDate',
filter: 'published=true',
})
const postImages = await Promise.all( posts.items.map(p => pb.files.getURL(p, p.headerImage)) )
---
<SummaryCard
title="Recent Posts",
titleLink="/posts"
>
<div class="flex flex-col md:flex-row gap-4 md:gap-4">
{
posts.items.map((p, i) => (
<div class="flex md:flex-col flex-row gap-3 md:gap-2 flex-1">
<time class="hidden md:block text-sm text-gray-500" datetime={p.publishDate}>{getFormattedDate(p.publishDate)}</time>
<a href={`/posts/${p.slug}`} class="shrink-0">
<img class="w-24 h-24 md:w-full md:h-48 object-cover rounded-sm" src={postImages[i]} alt={p.title}/>
</a>
<div class="flex flex-col justify-start md:justify-start">
<a href={`/posts/${p.slug}`} class="b1-no-underline line-clamp-2 md:line-clamp-none">
{p.title}
</a>
<time class="md:hidden text-sm text-gray-500" datetime={p.publishDate}>{getFormattedDate(p.publishDate)}</time>
</div>
</div>
))
}
</div>
</SummaryCard>

View File

@@ -0,0 +1,14 @@
---
const { title, titleLink } = Astro.props
---
<div>
<a href={titleLink} rel="prefetch" class="b1-no-underline">
<h2 class="text-xl">{title}</h2>
</a>
<div class="flex flex-row gap-2">
<slot/>
</div>
</div>

View File

@@ -1,73 +1,102 @@
---
import { authPB } from "src/utils/pocketbase";
import type { RecordModel } from 'pocketbase'
// export const prerender = false
const pb = await authPB()
const photos = await pb.collection('photos').getFullList({
sort: '-created'
sort: '-created',
filter: 'published=true'
})
const getImageLink = async (record: any) => {
return pb.files.getURL(record, record.image)
}
const photoLinks = await Promise.all(
photos.map(p => pb.files.getURL(p, p.image, { thumb: '0x1800' }))
)
---
<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 photos = dataElement ? JSON.parse(dataElement.textContent || '[]') : [];
const cap = photos.length - 1;
const img = document.getElementById('carousel-img') as HTMLImageElement;
const photoLinksElement = document.getElementById('photo-links-data');
const photoLinks = photoLinksElement ? JSON.parse(photoLinksElement.textContent || '[]') : [];
const titleEl = document.getElementById('photo-title');
const cameraEl = document.getElementById('photo-camera');
const filmEl = document.getElementById('photo-film');
const locationEl = document.getElementById('photo-location');
const currentPosIndicator = document.getElementById('current-pos');
const maxPosIndicator = document.getElementById('max-pos');
maxPosIndicator!.innerText = (cap + 1) as string
currentPosIndicator!.innerText = (pos + 1) as string
function updatePhoto() {
const currentPhoto = photos[pos];
currentPosIndicator!.innerText = (pos + 1) as string
// Update image src
if (img && currentPhoto) {
const imageUrl = `/api/files/photos/${currentPhoto.id}/${currentPhoto.image}`;
img.src = imageUrl;
if (maxPosIndicator) {
maxPosIndicator.innerText = String(photos.length);
}
if (currentPosIndicator) {
currentPosIndicator.innerText = '1';
}
// Update metadata
if (titleEl) {
const swiper = new Swiper('.swiper', {
modules: [Navigation, Keyboard, EffectFade],
spaceBetween: 20,
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";
}
if (cameraEl) {
cameraEl.textContent = `📸 ${currentPhoto.camera}`
if (cameraEl && currentPhoto) {
cameraEl.textContent = `📸 ${currentPhoto.camera}`;
}
if (locationEl) {
locationEl.textContent = `📌 ${currentPhoto.location}`
if (filmEl && currentPhoto) {
filmEl.textContent = currentPhoto.film !== "" ? `🎞️ ${currentPhoto.film}` : "";
}
if (locationEl && currentPhoto) {
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>
<!-- Hidden element to pass server data to client -->
<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-[70vh] 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"
loading="lazy"
alt={photo.title || 'Photo'}
/>
</div>
))}
</div>
</div>

View File

@@ -3,8 +3,15 @@ import { getFormattedDate } from '@utils/date'
const { publishDate, wordCount, readTime, tags } = Astro.props
---
<time datetime={publishDate}>{getFormattedDate(publishDate)}</time> |
<span title={`${wordCount} words`}>{readTime} min. read</span> |
<div class="flex flex-row gap-1">
<time datetime={publishDate}>{getFormattedDate(publishDate)}</time>
<span class="tag-separator"/>
<span title={`${wordCount} words`}>{readTime} min. read</span>
<span class="tag-separator"/>
{
tags.map((tag, i) => (
<>
@@ -15,3 +22,4 @@ const { publishDate, wordCount, readTime, tags } = Astro.props
</>
))
}
</div>

View File

@@ -16,6 +16,20 @@
lnk.className = 'b1-no-underline'
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')
li.className = 'line-clamp-1'
li.appendChild(lnk)
@@ -24,14 +38,11 @@
}
// get all titles in the page
const titles = document.querySelectorAll('h2, h3')
const titles = document.querySelectorAll('h1, h2, h3')
const toc = document.getElementById('toc') as Element
const toc_cnt = document.getElementById('toc-container') as Element
console.log(titles)
// if there's more than two titles (ie, there's more than just the article title and TOC title, then appropriate to show)
if (titles.length > 2) {
if (titles.length > 0) {
toc_cnt.classList.add('lg:block')
}
@@ -40,19 +51,7 @@
// Assign IDs in case they haven't been (by me)
t.id = t.innerHTML
let depth = 1
// determine what depth to use
switch (t.tagName) {
case 'H2':
depth = 1
break
case 'H3':
depth = 2
break
}
let depth = t.tagName.substring(1) as number
addLink(t, depth)
}
</script>

View File

@@ -1,18 +1,42 @@
---
import { getFormattedDate } from "@utils/date"
import { calcReadTime } from "@utils/post"
import { authPB } from "@utils/pocketbase"
import PostInfo from "@components/post/postInfo"
const pb = await authPB()
const { p } = Astro.props
const wordCount = p.content.split(' ').length;
const readTime = calcReadTime(wordCount);
---
<div class="mb-5 w-2/3">
<time datetime={p['publishDate']} class=" text-gray-500">{getFormattedDate(p['publishDate'])}</time>
<div>
<a href={`/posts/${p['slug']}/`} rel="prefetch">
<img src={ pb.files.getURL(p, p.headerImage) } class="rounded-xl border-12 border-white"/>
{p['title']}
<div class="mb-8 pb-7 md:pb-8 border-b border-gray-200 last:border-b-0 flex flex-col md:flex-row gap-4 md:gap-3">
<!-- Text content on left -->
<div class="flex-1 flex flex-col justify-start order-2 md:order-1">
<a href={`/posts/${p.slug}/`} rel="prefetch" class="no-underline hover:underline">
<h2 class="text-xl ">{p.title}</h2>
</a>
{
p.description !== "" &&
<p class="line-clamp-3 block italic text-gray-600">{p.description}</p>
}
<PostInfo
publishDate={p.publishDate},
wordCount={wordCount},
readTime={readTime},
tags={p.tags}
/>
</div>
<!-- Image on right -->
<div class="md:max-w-1/2 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>
</div>
<q class="line-clamp-3 block italic">{p['description']}</q>
</div>

View File

@@ -3,11 +3,27 @@ import BaseHead from "./BaseHead.astro"
import Sidebar from "@components/sidebar"
import Navbar from "@components/index/navbar"
import Footer from "@components/footer"
const {
meta = {}
} = Astro.props;
const {
title = "",
description = "",
ogImage = "",
articleDate = ""
} = meta;
---
<html>
<head>
<BaseHead/>
<BaseHead
title={title}
description={description}
ogImage={ogImage}
articleDate={articleDate}
/>
</head>

View File

@@ -0,0 +1,27 @@
---
import Base from "@layout/Base";
import { authPB } from "@utils/pocketbase";
export const prerender = false
const { title } = Astro.params
const pb = await authPB()
const album = await pb.collection('albums').getFirstListItem(`title ~ "${decodeURI(title!)}"`)
const images = await Promise.all(
album.images.map(i => pb.files.getURL(album, i))
)
---
<Base meta={{title: "album", description: album.title, ogImage: images[0]}}>
<div slot="sidebar">
<h1>{album.title}</h1>
<p>{album.date}</p>
<p>{album.location}</p><br>
<Fragment set:html={album.remarks}/>
</div>
<div slot="content">
{ images.map(img => <img class="pb-2" src={img}/> ) }
</div>
</Base>

View File

@@ -0,0 +1,49 @@
---
import Base from "@layout/Base";
import { authPB } from "@utils/pocketbase";
import AlbumCard from "@components/albums/albumCard";
export const prerender = false
const pb = await authPB()
const albums = await pb.collection('albums').getFullList({
sort: '-created'
})
---
<script>
import Swiper from "swiper";
import { Keyboard } from 'swiper/modules';
import 'swiper/css';
const s = new Swiper('.swiper-album', {
modules: [Keyboard],
slidesPerView: 1,
spaceBetween: 0,
speed: 400,
loop: true,
keyboard: {
enabled: true,
onlyInViewport: false,
}
}
)
</script>
<Base meta={{title: "albums"}}>
<div slot="sidebar">
<p class="text-left"> More focused, themed collections of photos compared to the more miscellaneous images of the (now defunct) <a href="/photos">photos page.</a> Best viewed on a large screen!</p>
<!-- <br>
← / → -->
</div>
<div slot="content">
<div class=" w-full">
<div class="">
{ albums.map(a => <AlbumCard a={a}/> ) }
</div>
</div>
</div>
</Base>

View File

@@ -3,7 +3,7 @@ import Base from "src/layout/Base.astro"
import Sidebar from "src/components/sidebar.astro"
---
<Base>
<Base meta={{title: "copyright"}}>
<div slot="content">
<p class="text-xl">Copyright & License</p>

View File

@@ -1,13 +1,26 @@
---
import Base from "src/layout/Base.astro"
import RecentPosts from "@components/index/recentPosts"
export const prerender = false
---
<Base>
<div slot="sidebar">
sidebar stuff
<div slot="sidebar" class="text-left">
<p>New website new me!</p> <br>
<p>This is the third major iteration of the website, and by far the most flexible and best designed (imo!)</p><br>
<!-- todo: write this post lmfao -->
<!-- <p>If you're interested, I actually wrote <a href="/posts/design-for-purpose">a post</a> about the new design and all that went into it.</p><br> -->
<p>I'm... really not sure what to put on this landing page, make sure to check out some of the photos and posts I've added recently, I've been totally revamping those too and I'm really proud of this batch.</p><br>
<p>Hope you enjoy your time here :)</p>
</div>
<div slot="content">
ya
</div>
<!-- <div slot="content" class="md:py-10">
<RecentPosts/>
</div> -->
</Base>

37
web/src/pages/now.astro Normal file
View File

@@ -0,0 +1,37 @@
---
import Base from "@layout/Base";
import { authPB } from "@utils/pocketbase";
import { getFormattedDate } from '@utils/date'
export const prerender = false
const pb = await authPB()
const records = await pb.collection('now').getFullList({
sort: '-updated'
})
const lastUpdated = getFormattedDate(records[0].updated)
---
<Base meta={{title: "now"}}>
<div slot="sidebar" class="text-left">
<h2>The Now!</h2>
<p>Trying out a <a href="https://nownownow.com/about">Now Page</a>, inspired by some other personal sites such as <a href="https://lai.nz">this great one.</a></p><br>
<p>I'll update this every now and then; the gist is "this is what you would tell someone that you haven't seem in a year, what you've been up to". Love that concept!</p>
</div>
<div slot="content" class="">
<p class="mb-1 md:mt-5 italic ">Last Updated: {lastUpdated}</p>
{
records.map(r => (
<div class="mb-3">
<h3>{r.heading}</h3>
<Fragment set:html={r.content}/>
</div>
))
}
</div>
</Base>

View File

@@ -11,19 +11,16 @@ const photos = await pb.collection('photos').getFullList({
})
---
<Base>
<Base meta={{title: "photos"}}>
<div slot="sidebar">
<p id="photo-title" class="bold text-2xl">{photos[0].title === "" ? "Untitled" : photos[0].title}</p>
<p id="photo-camera" class="text-sm">📸 {photos[0].camera}</p>
<p id="photo-film" class="text-sm">{photos[0].film !== "" ? `🎞️ ${photos[0].film}` : ""}</p>
<p id="photo-location" class="text-sm">📌 {photos[0].location}</p>
<button id="dec-button" onclick="dec()">&lt;</button>
<span id="current-pos" /> of <span id="max-pos" />
<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 slot="content" class="">

View File

@@ -20,12 +20,11 @@ const readTime = calcReadTime(wordCount);
---
<Base>
<Base meta={{title: "post", description: title, ogImage: headerImage, articleDate: publishDate}}>
<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>
<PostInfo
@@ -39,11 +38,15 @@ const readTime = calcReadTime(wordCount);
</div>
<div id="post-content" slot="content" class="py-12">
<div id="post-content" slot="content" class="md:py-12">
<Fragment set:html={content} />
</div>
<style is:global>
html {
scroll-behavior: smooth;
}
#post-content img {
width: 90%;
margin-inline: auto;

View File

@@ -17,14 +17,14 @@ const tags = posts.flatMap(p => p.tags)
const uniqueTags = tags.filter((v, i, a) => a.indexOf(v) === i)
---
<Base>
<Base meta={{title: "posts"}}>
<div slot="sidebar" class="text-left">
A few thoughts I've had, whenever I feel like it as you can see!
<TagList tags={uniqueTags}/>
</div>
<div slot="content" class="py-12">
<div slot="content" class="md:py-12">
{ posts.map(p => <PostCard p={p}/>) }
</div>
</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>

View File

@@ -42,9 +42,22 @@
@apply hover:decoration-(--accent);
}
h2 {
@apply text-xl;
h1 {
font-size: 28px;
@apply py-4;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 20px;
}
}
.tag-separator {
@apply border-l border-gray-400;
}
.content-panel {