Compare commits
13 Commits
mobile-v2
...
3378844823
| Author | SHA1 | Date | |
|---|---|---|---|
| 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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-lg" 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,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
|
function preloadImages() {
|
||||||
|
photoLinks.forEach((url: string) => {
|
||||||
// Update image src
|
const preloadImg = new Image();
|
||||||
if (img && currentPhoto) {
|
preloadImg.src = url;
|
||||||
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() {
|
// Call preload on page load
|
||||||
pos = pos === cap ? 0 : pos + 1;
|
preloadImages();
|
||||||
updatePhoto();
|
|
||||||
}
|
const swiper = new Swiper('.swiper', {
|
||||||
|
modules: [Navigation, Keyboard, EffectFade],
|
||||||
function dec() {
|
effect: 'cards',
|
||||||
pos = pos === 0 ? cap : pos - 1;
|
fadeEffect: {
|
||||||
updatePhoto();
|
crossFade: true
|
||||||
}
|
},
|
||||||
|
speed: 250,
|
||||||
// make functions globally accessible
|
navigation: {
|
||||||
(window as any).inc = inc;
|
nextEl: '#inc-button',
|
||||||
(window as any).dec = dec;
|
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 && currentPhoto) {
|
||||||
|
cameraEl.textContent = `📸 ${currentPhoto.camera}`;
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
alt={photo.title || 'Photo'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -15,6 +15,20 @@
|
|||||||
lnk.href=`#${tag.id}`
|
lnk.href=`#${tag.id}`
|
||||||
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'
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
---
|
---
|
||||||
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-1">
|
||||||
|
<PostInfo
|
||||||
|
publishDate={p.publishDate},
|
||||||
|
wordCount={wordCount},
|
||||||
|
readTime={readTime},
|
||||||
|
tags={p.tags}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
---
|
---
|
||||||
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>Check out some of the photos and posts I've added recently, as well as the new design.</p><br>
|
||||||
|
|
||||||
|
<!-- todo: write this post lmfao -->
|
||||||
|
<p>If you're interested, I actually wrote a post about the new design and all that went into it.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content" class="md:py-10">
|
||||||
ya
|
<RecentPosts/>
|
||||||
</div>
|
</div>
|
||||||
</Base>
|
</Base>
|
||||||
|
|||||||
@@ -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()"><</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="">
|
||||||
|
|||||||
@@ -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>
|
<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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user