Post Archive v1: a first pass at a functional post-archive
This commit is contained in:
parent
1b66fc8a90
commit
960f7ff4d0
@ -1,4 +1,5 @@
|
||||
// Import the database connection
|
||||
import { db } from './db';
|
||||
export { db } from './db';
|
||||
|
||||
// Export all functions related to database operations
|
||||
|
||||
@ -1,6 +1,22 @@
|
||||
import { db } from './db';
|
||||
import { parseTags } from './tags';
|
||||
|
||||
// Interface for archive data structure
|
||||
export interface ArchiveMonth {
|
||||
name: string;
|
||||
count: string;
|
||||
posts: Array<{
|
||||
title: string;
|
||||
href: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ArchiveYear {
|
||||
year: string;
|
||||
count: string;
|
||||
months: ArchiveMonth[];
|
||||
}
|
||||
|
||||
// Interface for blog post
|
||||
export interface BlogPost {
|
||||
id: number;
|
||||
@ -107,3 +123,69 @@ export function getPostsByDateRange(startDate: string, endDate: string): BlogPos
|
||||
|
||||
return dateQuery.all(startDate, endDate) as BlogPost[];
|
||||
}
|
||||
|
||||
// Function to get posts organized by year and month for the archive
|
||||
export function getPostsByYearAndMonth(): ArchiveYear[] {
|
||||
const query = db.query(`
|
||||
SELECT * FROM posts
|
||||
WHERE path NOT LIKE '%.md'
|
||||
ORDER BY date DESC
|
||||
`);
|
||||
|
||||
const posts = query.all() as any[];
|
||||
|
||||
// Group posts by year and month
|
||||
const yearMap = new Map<string, ArchiveYear>();
|
||||
|
||||
const monthNames = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
];
|
||||
|
||||
// Process each post
|
||||
posts.forEach(post => {
|
||||
const date = new Date(post.date);
|
||||
const year = String(date.getFullYear());
|
||||
const monthName = monthNames[date.getMonth()];
|
||||
|
||||
// Create clean post href from path
|
||||
const href = post.path.replace(/^.*\/content\//, '/').replace(/\.md$/, '');
|
||||
|
||||
// Initialize year if it doesn't exist
|
||||
if (!yearMap.has(year)) {
|
||||
yearMap.set(year, {
|
||||
year,
|
||||
count: "(0)",
|
||||
months: []
|
||||
});
|
||||
}
|
||||
|
||||
const yearData = yearMap.get(year)!;
|
||||
|
||||
// Find or create month data
|
||||
let monthData = yearData.months.find(m => m.name === monthName);
|
||||
if (!monthData) {
|
||||
monthData = {
|
||||
name: monthName,
|
||||
count: "(0)",
|
||||
posts: []
|
||||
};
|
||||
yearData.months.push(monthData);
|
||||
}
|
||||
|
||||
// Add post to the month
|
||||
monthData.posts.push({
|
||||
title: post.title,
|
||||
href
|
||||
});
|
||||
|
||||
// Update counts
|
||||
monthData.count = `(${monthData.posts.length})`;
|
||||
yearData.count = `(${yearData.months.reduce((total, m) => total + m.posts.length, 0)})`;
|
||||
});
|
||||
|
||||
// Convert map to array and sort
|
||||
return Array.from(yearMap.values()).sort((a, b) =>
|
||||
parseInt(b.year) - parseInt(a.year)
|
||||
);
|
||||
}
|
||||
@ -10,15 +10,238 @@ function setupArchiveToggles() {
|
||||
// Toggle expanded state
|
||||
if (toggleIcon) toggleIcon.classList.toggle('expanded');
|
||||
if (content) content.classList.toggle('expanded');
|
||||
|
||||
// Update aria-expanded
|
||||
const isExpanded = content.classList.contains('expanded');
|
||||
this.setAttribute('aria-expanded', String(isExpanded));
|
||||
|
||||
// Update tabIndex for nested elements
|
||||
updateTabIndex(content, isExpanded);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove active class from all post links
|
||||
document.querySelectorAll('.archive-post a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add click handlers for post links to toggle active class
|
||||
document.querySelectorAll('.archive-post a').forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
// Remove active class from all post links
|
||||
document.querySelectorAll('.archive-post a').forEach(postLink => {
|
||||
postLink.classList.remove('active');
|
||||
});
|
||||
// Add active class to clicked link
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize the archive state with a slight delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
initializeArchiveState();
|
||||
// Update the active post indicator after initializing the archive state
|
||||
updateActivePost();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function updateTabIndex(content: Element, isExpanded: boolean) {
|
||||
if (!content) return;
|
||||
|
||||
const tabIndex = isExpanded ? '0' : '-1';
|
||||
|
||||
// Update focusable elements based on visibility
|
||||
content.querySelectorAll('a, button, [tabindex="0"], [tabindex="-1"]').forEach(el => {
|
||||
el.setAttribute('tabindex', tabIndex);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeArchiveState() {
|
||||
// Get the current path
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Check if we're currently viewing a blog post (pattern: /YYYY/MM/slug)
|
||||
const pathParts = currentPath.split('/').filter(part => part);
|
||||
const isPostView = pathParts.length >= 3 &&
|
||||
/^\d{4}$/.test(pathParts[0]) &&
|
||||
/^\d{1,2}$/.test(pathParts[1]);
|
||||
|
||||
// Get all year and month elements
|
||||
const years = Array.from(document.querySelectorAll('.archive-year')) as HTMLElement[];
|
||||
const months = Array.from(document.querySelectorAll('.archive-month')) as HTMLElement[];
|
||||
|
||||
// Find the target year and month to keep expanded
|
||||
let targetYear: HTMLElement | null = null;
|
||||
let targetMonth: HTMLElement | null = null;
|
||||
|
||||
if (isPostView) {
|
||||
// Try to find the post link in multiple ways
|
||||
let postLink: HTMLAnchorElement | null = null;
|
||||
|
||||
// Try to find the post link with exact path or with/without trailing slash
|
||||
postLink = document.querySelector(`a[href="${currentPath}"]`) as HTMLAnchorElement;
|
||||
|
||||
if (!postLink) {
|
||||
const altPath = currentPath.endsWith('/') ?
|
||||
currentPath.slice(0, -1) : currentPath + '/';
|
||||
postLink = document.querySelector(`a[href="${altPath}"]`) as HTMLAnchorElement;
|
||||
}
|
||||
|
||||
// If still not found, try a partial match using the slug
|
||||
if (!postLink && pathParts.length >= 3) {
|
||||
const slug = pathParts[pathParts.length - 1];
|
||||
|
||||
document.querySelectorAll('.archive-post a').forEach((link) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && href.endsWith(slug)) {
|
||||
postLink = link as HTMLAnchorElement;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If we found the post link, get its parent month and year
|
||||
if (postLink) {
|
||||
const postLi = postLink.parentElement; // li.archive-post
|
||||
const postsUl = postLi?.parentElement; // ul.archive-posts
|
||||
const contentDiv = postsUl?.parentElement; // div.archive-content
|
||||
const monthLi = contentDiv?.parentElement; // li containing the month
|
||||
|
||||
// Get the month toggle element
|
||||
const monthElement = monthLi?.querySelector('.archive-month') as HTMLElement;
|
||||
|
||||
if (monthElement) {
|
||||
targetMonth = monthElement;
|
||||
|
||||
// Get the parent year
|
||||
const yearLi = monthLi?.parentElement; // ul containing the month
|
||||
const yearContentDiv = yearLi?.parentElement; // div.archive-content
|
||||
const yearMainLi = yearContentDiv?.parentElement; // li containing the year
|
||||
const yearElement = yearMainLi?.querySelector('.archive-year') as HTMLElement;
|
||||
|
||||
targetYear = yearElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find a specific target year, fall back to the most recent year
|
||||
if (!targetYear && years.length > 0) {
|
||||
targetYear = years[0];
|
||||
}
|
||||
|
||||
// If we didn't find a specific target month, fall back to the most recent month in that year
|
||||
if (!targetMonth && targetYear) {
|
||||
const yearContent = targetYear.nextElementSibling as HTMLElement;
|
||||
if (yearContent) {
|
||||
const yearMonths = yearContent.querySelectorAll('.archive-month') as NodeListOf<HTMLElement>;
|
||||
if (yearMonths.length > 0) {
|
||||
targetMonth = yearMonths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to collapse an element
|
||||
function collapse(element: HTMLElement) {
|
||||
const toggleIcon = element.querySelector('.archive-toggle');
|
||||
const content = element.nextElementSibling;
|
||||
|
||||
if (toggleIcon) toggleIcon.classList.remove('expanded');
|
||||
if (content) {
|
||||
content.classList.remove('expanded');
|
||||
updateTabIndex(content, false);
|
||||
}
|
||||
element.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// Helper function to expand an element
|
||||
function expand(element: HTMLElement) {
|
||||
const toggleIcon = element.querySelector('.archive-toggle');
|
||||
const content = element.nextElementSibling;
|
||||
|
||||
if (toggleIcon) toggleIcon.classList.add('expanded');
|
||||
if (content) {
|
||||
content.classList.add('expanded');
|
||||
updateTabIndex(content, true);
|
||||
}
|
||||
element.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
// Collapse all years except our target
|
||||
years.forEach(year => {
|
||||
if (year !== targetYear) {
|
||||
collapse(year);
|
||||
}
|
||||
});
|
||||
|
||||
// Collapse all months except our target
|
||||
months.forEach(month => {
|
||||
if (month !== targetMonth) {
|
||||
collapse(month);
|
||||
}
|
||||
});
|
||||
|
||||
// Expand the target year if we have one
|
||||
if (targetYear) {
|
||||
expand(targetYear);
|
||||
}
|
||||
|
||||
// Expand the target month if we have one
|
||||
if (targetMonth) {
|
||||
expand(targetMonth);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update the active post link based on the current URL
|
||||
function updateActivePost() {
|
||||
// Remove active class from all post links
|
||||
document.querySelectorAll('.archive-post a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
|
||||
// Get the current path
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Find the post link matching the current path
|
||||
let postLink = document.querySelector(`a[href="${currentPath}"]`) as HTMLAnchorElement;
|
||||
|
||||
// Try with/without trailing slash if not found
|
||||
if (!postLink) {
|
||||
const altPath = currentPath.endsWith('/') ?
|
||||
currentPath.slice(0, -1) : currentPath + '/';
|
||||
postLink = document.querySelector(`a[href="${altPath}"]`) as HTMLAnchorElement;
|
||||
}
|
||||
|
||||
// If found, add active class
|
||||
if (postLink) {
|
||||
postLink.classList.add('active');
|
||||
} else {
|
||||
// Try partial match using the slug
|
||||
const pathParts = currentPath.split('/').filter(part => part);
|
||||
if (pathParts.length >= 3) {
|
||||
const slug = pathParts[pathParts.length - 1];
|
||||
document.querySelectorAll('.archive-post a').forEach((link) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && href.endsWith(slug)) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupArchiveToggles();
|
||||
});
|
||||
} else {
|
||||
setupArchiveToggles();
|
||||
}
|
||||
|
||||
// Listen for browser navigation events (back/forward buttons)
|
||||
window.addEventListener('popstate', () => {
|
||||
// Re-initialize the archive state for the new URL
|
||||
initializeArchiveState();
|
||||
// Update the active post indicator after initializing the archive state
|
||||
setTimeout(updateActivePost, 10); // Small delay to ensure DOM is updated
|
||||
});
|
||||
@ -1,66 +1,23 @@
|
||||
import React from 'react';
|
||||
import postArchiveScript from '../clientJS/post-archive' with { type: "text" };
|
||||
import { getPostsByYearAndMonth } from '../../db';
|
||||
import { minifyJS } from '../utils';
|
||||
|
||||
export function PostArchive() {
|
||||
const archiveData = [
|
||||
{
|
||||
year: "2025",
|
||||
count: "(8)",
|
||||
months: [
|
||||
{
|
||||
name: "October",
|
||||
count: "(3)",
|
||||
posts: [
|
||||
{ title: "Building a Modern Blog", href: "/blog/post-1" },
|
||||
{ title: "TypeScript Tips & Tricks", href: "/blog/post-2" },
|
||||
{ title: "Designing Better UIs", href: "/blog/post-3" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "September",
|
||||
count: "(5)",
|
||||
posts: [
|
||||
{ title: "Getting Started with Bun", href: "/blog/post-4" },
|
||||
{ title: "Web Performance Optimization", href: "/blog/post-5" },
|
||||
{ title: "CSS Grid vs Flexbox", href: "/blog/post-6" },
|
||||
{ title: "JavaScript Best Practices", href: "/blog/post-7" },
|
||||
{ title: "Accessibility Matters", href: "/blog/post-8" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
year: "2024",
|
||||
count: "(12)",
|
||||
months: [
|
||||
{
|
||||
name: "December",
|
||||
count: "(4)",
|
||||
posts: [
|
||||
{ title: "Year in Review 2024", href: "/blog/post-9" },
|
||||
{ title: "Holiday Coding Projects", href: "/blog/post-10" },
|
||||
{ title: "New Year Resolutions", href: "/blog/post-11" },
|
||||
{ title: "Reflecting on Growth", href: "/blog/post-12" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "June",
|
||||
count: "(8)",
|
||||
posts: [
|
||||
{ title: "Summer Tech Trends", href: "/blog/post-13" },
|
||||
{ title: "Remote Work Tips", href: "/blog/post-14" },
|
||||
{ title: "Learning Resources", href: "/blog/post-15" },
|
||||
{ title: "Code Review Best Practices", href: "/blog/post-16" },
|
||||
{ title: "Git Workflow Tips", href: "/blog/post-17" },
|
||||
{ title: "Docker for Beginners", href: "/blog/post-18" },
|
||||
{ title: "API Design Patterns", href: "/blog/post-19" },
|
||||
{ title: "Testing Strategies", href: "/blog/post-20" }
|
||||
]
|
||||
}
|
||||
]
|
||||
// Read posts from database once and store in a closure
|
||||
const getArchiveData = (() => {
|
||||
let cachedData: ReturnType<typeof getPostsByYearAndMonth> | null = null;
|
||||
|
||||
return () => {
|
||||
if (!cachedData) {
|
||||
cachedData = getPostsByYearAndMonth();
|
||||
}
|
||||
];
|
||||
return cachedData;
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
export function PostArchive() {
|
||||
const archiveData = getArchiveData();
|
||||
|
||||
return (
|
||||
<div className="postList sheet-background">
|
||||
@ -68,25 +25,25 @@ export function PostArchive() {
|
||||
<ul className="post-archive">
|
||||
{archiveData.map((yearData) => (
|
||||
<li key={yearData.year}>
|
||||
<div className="archive-year">
|
||||
<span className="archive-toggle">▶</span>
|
||||
<div className="archive-year" tabIndex={0} role="button" aria-expanded="true">
|
||||
<span className="archive-toggle expanded">▼</span>
|
||||
<span>{yearData.year}</span>
|
||||
<span className="post-count">{yearData.count}</span>
|
||||
</div>
|
||||
<div className="archive-content">
|
||||
<div className="archive-content expanded">
|
||||
<ul className="archive-months">
|
||||
{yearData.months.map((monthData) => (
|
||||
<li key={monthData.name}>
|
||||
<div className="archive-month">
|
||||
<span className="archive-toggle">▶</span>
|
||||
<div className="archive-month" tabIndex={0} role="button" aria-expanded="true">
|
||||
<span className="archive-toggle expanded">▼</span>
|
||||
<span>{monthData.name}</span>
|
||||
<span className="post-count">{monthData.count}</span>
|
||||
</div>
|
||||
<div className="archive-content">
|
||||
<div className="archive-content expanded">
|
||||
<ul className="archive-posts">
|
||||
{monthData.posts.map((post) => (
|
||||
<li key={post.href} className="archive-post">
|
||||
<a href={post.href}>{post.title}</a>
|
||||
<a href={post.href} tabIndex={-1}>{post.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -5,8 +5,10 @@ import { type BlogPost } from '../../db/queries';
|
||||
|
||||
export function Home({ searchParams }: { searchParams: Record<string, string> }) {
|
||||
const currentPage = parseInt(searchParams.page || "1", 10);
|
||||
const totalPages = Math.ceil(getNumOfPosts() / 10);
|
||||
const posts = getRecentPosts(10, ); // Get the 10 most recent posts
|
||||
const postsPerPage = 10;
|
||||
const totalPages = Math.ceil(getNumOfPosts() / postsPerPage);
|
||||
const offset = (currentPage - 1) * postsPerPage;
|
||||
const posts = getRecentPosts(postsPerPage, offset); // Get posts for the current page
|
||||
|
||||
return (
|
||||
<main>
|
||||
|
||||
@ -628,11 +628,23 @@ h1 {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.archive-year.expanded,
|
||||
.archive-month.expanded {
|
||||
background-color: var(--bg-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.archive-year:hover,
|
||||
.archive-month:hover {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.archive-year:focus,
|
||||
.archive-month:focus {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.archive-year {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
@ -652,6 +664,7 @@ h1 {
|
||||
|
||||
.archive-toggle.expanded {
|
||||
transform: rotate(90deg);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.archive-content {
|
||||
@ -691,6 +704,11 @@ h1 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.archive-post a.active {
|
||||
color: var(--text-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-count {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user