Compare commits
34 Commits
mobile-v2
...
a25500bb91
| Author | SHA1 | Date | |
|---|---|---|---|
| a25500bb91 | |||
| c3e05b44ea | |||
| b4f8a42e05 | |||
| ff486c01e3 | |||
| 33ead9d968 | |||
| 20f8b38c8a | |||
| 7f0b65fa20 | |||
| 2fdaf64b47 | |||
| 4bad9e095e | |||
| facb2b0f99 | |||
| ef0c8dd981 | |||
| 32b9a29028 | |||
| 1da77d6074 | |||
| 64ecc338be | |||
| 66623a64b4 | |||
| 6997e313e6 | |||
| 435ca0f2d7 | |||
| 3e742ba952 | |||
| caad696191 | |||
| 51419e4c7c | |||
| 4310c90f48 | |||
| 3378844823 | |||
| 02c7e2f45a | |||
| f0d58a1a33 | |||
| a37955cbec | |||
| 834e176841 | |||
| bad4457ad7 | |||
| b2657962e1 | |||
| 4992379487 | |||
| 1f499739fb | |||
| 7972acb40f | |||
| e06bb64645 | |||
| 3918171086 | |||
| 1e9489bb14 |
20
web/package-lock.json
generated
20
web/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
web/src/components/albums/albumCard.astro
Normal file
18
web/src/components/albums/albumCard.astro
Normal 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>
|
||||||
@@ -12,10 +12,14 @@ const links = [
|
|||||||
txt: "email",
|
txt: "email",
|
||||||
lnk: "mailto:contact@breadone.net"
|
lnk: "mailto:contact@breadone.net"
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
txt: "copyright",
|
// txt: "about",
|
||||||
lnk: "/copyright"
|
// lnk: "/about"
|
||||||
}
|
// },
|
||||||
|
// {
|
||||||
|
// txt: "copyright",
|
||||||
|
// lnk: "/copyright"
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ const links = [
|
|||||||
txt: 'now',
|
txt: 'now',
|
||||||
lnk: '/now'
|
lnk: '/now'
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// txt: 'photos',
|
||||||
|
// lnk: '/photos'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
txt: 'photos',
|
txt: 'albums',
|
||||||
lnk: '/photos'
|
lnk: '/albums'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
txt: 'posts',
|
txt: 'posts',
|
||||||
|
|||||||
39
web/src/components/index/recentPosts.astro
Normal file
39
web/src/components/index/recentPosts.astro
Normal 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>
|
||||||
14
web/src/components/index/summaryCard.astro
Normal file
14
web/src/components/index/summaryCard.astro
Normal 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>
|
||||||
@@ -1,73 +1,102 @@
|
|||||||
---
|
---
|
||||||
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
|
||||||
|
|
||||||
const pb = await authPB()
|
const pb = await authPB()
|
||||||
const photos = await pb.collection('photos').getFullList({
|
const photos = await pb.collection('photos').getFullList({
|
||||||
sort: '-created'
|
sort: '-created',
|
||||||
|
filter: 'published=true'
|
||||||
})
|
})
|
||||||
|
|
||||||
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, { thumb: '0x1800' }))
|
||||||
}
|
)
|
||||||
---
|
---
|
||||||
|
|
||||||
<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 filmEl = document.getElementById('photo-film');
|
||||||
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
|
|
||||||
if (titleEl) {
|
|
||||||
titleEl.textContent = currentPhoto.title || "Untitled";
|
|
||||||
}
|
|
||||||
if (cameraEl) {
|
|
||||||
cameraEl.textContent = `📸 ${currentPhoto.camera}`
|
|
||||||
}
|
|
||||||
if (locationEl) {
|
|
||||||
locationEl.textContent = `📌 ${currentPhoto.location}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function inc() {
|
|
||||||
pos = pos === cap ? 0 : pos + 1;
|
|
||||||
updatePhoto();
|
|
||||||
}
|
|
||||||
|
|
||||||
function dec() {
|
const swiper = new Swiper('.swiper', {
|
||||||
pos = pos === 0 ? cap : pos - 1;
|
modules: [Navigation, Keyboard, EffectFade],
|
||||||
updatePhoto();
|
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];
|
||||||
|
|
||||||
// make functions globally accessible
|
if (currentPosIndicator) {
|
||||||
(window as any).inc = inc;
|
currentPosIndicator.innerText = String(realIndex + 1);
|
||||||
(window as any).dec = dec;
|
}
|
||||||
|
|
||||||
|
if (titleEl && currentPhoto) {
|
||||||
|
titleEl.textContent = currentPhoto.title || "Untitled";
|
||||||
|
}
|
||||||
|
if (cameraEl && currentPhoto) {
|
||||||
|
cameraEl.textContent = `📸 ${currentPhoto.camera}`;
|
||||||
|
}
|
||||||
|
if (filmEl && currentPhoto) {
|
||||||
|
filmEl.textContent = currentPhoto.film !== "" ? `🎞️ ${currentPhoto.film}` : "";
|
||||||
|
}
|
||||||
|
if (locationEl && currentPhoto) {
|
||||||
|
locationEl.textContent = `📌 ${currentPhoto.location}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</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-[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>
|
||||||
@@ -3,15 +3,23 @@ import { getFormattedDate } from '@utils/date'
|
|||||||
const { publishDate, wordCount, readTime, tags } = Astro.props
|
const { publishDate, wordCount, readTime, tags } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<time datetime={publishDate}>{getFormattedDate(publishDate)}</time> |
|
<div class="flex flex-row gap-1">
|
||||||
<span title={`${wordCount} words`}>{readTime} min. read</span> |
|
<time datetime={publishDate}>{getFormattedDate(publishDate)}</time>
|
||||||
{
|
|
||||||
tags.map((tag, i) => (
|
<span class="tag-separator"/>
|
||||||
<>
|
|
||||||
<a class="b1-no-underline" href={`/posts/tag/${tag}/`}>
|
<span title={`${wordCount} words`}>{readTime} min. read</span>
|
||||||
#{tag}
|
|
||||||
</a>
|
<span class="tag-separator"/>
|
||||||
{i < tags.length - 1 && ", "}
|
|
||||||
</>
|
{
|
||||||
))
|
tags.map((tag, i) => (
|
||||||
}
|
<>
|
||||||
|
<a class="b1-no-underline" href={`/posts/tag/${tag}/`}>
|
||||||
|
#{tag}
|
||||||
|
</a>
|
||||||
|
{i < tags.length - 1 && ", "}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -24,14 +38,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get all titles in the page
|
// 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 = document.getElementById('toc') as Element
|
||||||
const toc_cnt = document.getElementById('toc-container') as Element
|
const toc_cnt = document.getElementById('toc-container') as Element
|
||||||
|
|
||||||
console.log(titles)
|
if (titles.length > 0) {
|
||||||
|
|
||||||
// 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) {
|
|
||||||
toc_cnt.classList.add('lg:block')
|
toc_cnt.classList.add('lg:block')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,19 +51,7 @@
|
|||||||
|
|
||||||
// Assign IDs in case they haven't been (by me)
|
// Assign IDs in case they haven't been (by me)
|
||||||
t.id = t.innerHTML
|
t.id = t.innerHTML
|
||||||
|
let depth = t.tagName.substring(1) as number
|
||||||
let depth = 1
|
|
||||||
|
|
||||||
// determine what depth to use
|
|
||||||
switch (t.tagName) {
|
|
||||||
case 'H2':
|
|
||||||
depth = 1
|
|
||||||
break
|
|
||||||
case 'H3':
|
|
||||||
depth = 2
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
addLink(t, depth)
|
addLink(t, depth)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,18 +1,42 @@
|
|||||||
---
|
---
|
||||||
import { getFormattedDate } from "@utils/date"
|
import { calcReadTime } from "@utils/post"
|
||||||
import { authPB } from "@utils/pocketbase"
|
import { authPB } from "@utils/pocketbase"
|
||||||
|
import PostInfo from "@components/post/postInfo"
|
||||||
|
|
||||||
const pb = await authPB()
|
const pb = await authPB()
|
||||||
const { p } = Astro.props
|
const { p } = Astro.props
|
||||||
|
|
||||||
|
const wordCount = p.content.split(' ').length;
|
||||||
|
const readTime = calcReadTime(wordCount);
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="mb-5 w-2/3">
|
<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">
|
||||||
<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"/>
|
<a href={`/posts/${p.slug}/`} rel="prefetch" class="no-underline hover:underline">
|
||||||
{p['title']}
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<q class="line-clamp-3 block italic">{p['description']}</q>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -3,11 +3,27 @@ import BaseHead from "./BaseHead.astro"
|
|||||||
import Sidebar from "@components/sidebar"
|
import Sidebar from "@components/sidebar"
|
||||||
import Navbar from "@components/index/navbar"
|
import Navbar from "@components/index/navbar"
|
||||||
import Footer from "@components/footer"
|
import Footer from "@components/footer"
|
||||||
|
|
||||||
|
const {
|
||||||
|
meta = {}
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = "",
|
||||||
|
description = "",
|
||||||
|
ogImage = "",
|
||||||
|
articleDate = ""
|
||||||
|
} = meta;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<BaseHead/>
|
<BaseHead
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
ogImage={ogImage}
|
||||||
|
articleDate={articleDate}
|
||||||
|
/>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
27
web/src/pages/albums/[title].astro
Normal file
27
web/src/pages/albums/[title].astro
Normal 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>
|
||||||
49
web/src/pages/albums/index.astro
Normal file
49
web/src/pages/albums/index.astro
Normal 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>
|
||||||
@@ -3,7 +3,7 @@ import Base from "src/layout/Base.astro"
|
|||||||
import Sidebar from "src/components/sidebar.astro"
|
import Sidebar from "src/components/sidebar.astro"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base meta={{title: "copyright"}}>
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<p class="text-xl">Copyright & License</p>
|
<p class="text-xl">Copyright & License</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
---
|
---
|
||||||
import Base from "src/layout/Base.astro"
|
import Base from "src/layout/Base.astro"
|
||||||
|
import RecentPosts from "@components/index/recentPosts"
|
||||||
|
|
||||||
|
export const prerender = false
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base>
|
||||||
<div slot="sidebar">
|
<div slot="sidebar" class="text-left">
|
||||||
sidebar stuff
|
<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>
|
||||||
|
|
||||||
<div slot="content">
|
<!-- <div slot="content" class="md:py-10">
|
||||||
ya
|
<RecentPosts/>
|
||||||
</div>
|
</div> -->
|
||||||
</Base>
|
</Base>
|
||||||
|
|||||||
37
web/src/pages/now.astro
Normal file
37
web/src/pages/now.astro
Normal 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>
|
||||||
@@ -11,19 +11,16 @@ const photos = await pb.collection('photos').getFullList({
|
|||||||
})
|
})
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base meta={{title: "photos"}}>
|
||||||
<div slot="sidebar">
|
<div slot="sidebar">
|
||||||
<p id="photo-title" class="bold text-2xl">{photos[0].title === "" ? "Untitled" : photos[0].title}</p>
|
<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-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>
|
<p id="photo-location" class="text-sm">📌 {photos[0].location}</p>
|
||||||
|
|
||||||
<button id="dec-button" onclick="dec()"><</button>
|
<button id="dec-button" onclick="dec()"><</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()">></button>
|
<button id="inc-button" onclick="inc()">></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="">
|
||||||
|
|||||||
@@ -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">
|
<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>
|
<style is:global>
|
||||||
#post-content img {
|
html {
|
||||||
width: 90%;
|
scroll-behavior: smooth;
|
||||||
margin-inline: auto;
|
}
|
||||||
display: block;
|
|
||||||
}
|
#post-content img {
|
||||||
</style>
|
width: 90%;
|
||||||
|
margin-inline: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
</Base>
|
</Base>
|
||||||
@@ -17,14 +17,14 @@ const tags = posts.flatMap(p => p.tags)
|
|||||||
const uniqueTags = tags.filter((v, i, a) => a.indexOf(v) === i)
|
const uniqueTags = tags.filter((v, i, a) => a.indexOf(v) === i)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base meta={{title: "posts"}}>
|
||||||
<div slot="sidebar" class="text-left">
|
<div slot="sidebar" class="text-left">
|
||||||
A few thoughts I've had, whenever I feel like it as you can see!
|
A few thoughts I've had, whenever I feel like it as you can see!
|
||||||
|
|
||||||
<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>
|
||||||
@@ -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>
|
||||||
@@ -42,9 +42,22 @@
|
|||||||
@apply hover:decoration-(--accent);
|
@apply hover:decoration-(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h1 {
|
||||||
@apply text-xl;
|
font-size: 28px;
|
||||||
|
@apply py-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-separator {
|
||||||
|
@apply border-l border-gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-panel {
|
.content-panel {
|
||||||
|
|||||||
Reference in New Issue
Block a user