Cleanup AI slop and simplify db interface
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { minifyCSS, minifyJS } from './utils';
|
||||
|
||||
// @ts-expect-error - Importing as text, not a module
|
||||
import styles from './styles.css' with { type: "text" };
|
||||
// @ts-expect-error - Importing as text, not a module
|
||||
import headScript from './onLoad' with { type: "text" };
|
||||
|
||||
import { ThemePicker } from './components/theme-picker';
|
||||
@@ -9,7 +11,7 @@ import { ProfileBadge } from './components/profile-badge';
|
||||
import { TagPicker } from './components/tag-picker';
|
||||
import { PostArchive } from './components/post-archive';
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
export function AppShell({ children, searchParams }: { children: ReactNode, searchParams?: URLSearchParams }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -23,7 +25,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<aside>
|
||||
<ThemePicker />
|
||||
<ProfileBadge />
|
||||
<TagPicker />
|
||||
<TagPicker searchParams={searchParams} />
|
||||
<PostArchive />
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
// Client-side tag picker toggle functionality
|
||||
class TagPickerManager {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
// // Client-side tag picker toggle functionality
|
||||
// class TagPickerManager {
|
||||
// constructor() {
|
||||
// this.init();
|
||||
// }
|
||||
|
||||
init() {
|
||||
this.attachTagClickListeners();
|
||||
}
|
||||
// init() {
|
||||
// this.attachTagClickListeners();
|
||||
// }
|
||||
|
||||
// Parse current URL to get active tags
|
||||
getActiveTags(): string[] {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.getAll('tag').map(tag => decodeURIComponent(tag).replace(/-/g, ' '));
|
||||
}
|
||||
// // Parse current URL to get active tags
|
||||
// getActiveTags(): string[] {
|
||||
// const urlParams = new URLSearchParams(window.location.search);
|
||||
// return urlParams.getAll('tag').map(tag => decodeURIComponent(tag).replace(/-/g, ' '));
|
||||
// }
|
||||
|
||||
// Generate query string from tags array
|
||||
generateTagQueryString(tags: string[]): string {
|
||||
if (tags.length === 0) return '';
|
||||
const params = new URLSearchParams();
|
||||
tags.forEach(tag => {
|
||||
params.append('tag', tag.toLowerCase().replace(/\s+/g, '-'));
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
// // Generate query string from tags array
|
||||
// generateTagQueryString(tags: string[]): string {
|
||||
// if (tags.length === 0) return '';
|
||||
// const params = new URLSearchParams();
|
||||
// tags.forEach(tag => {
|
||||
// params.append('tag', tag.toLowerCase().replace(/\s+/g, '-'));
|
||||
// });
|
||||
// return params.toString();
|
||||
// }
|
||||
|
||||
// Toggle a tag and update the page
|
||||
toggleTag(tagName: string) {
|
||||
const currentTags = this.getActiveTags();
|
||||
// // Toggle a tag and update the page
|
||||
// toggleTag(tagName: string) {
|
||||
// const currentTags = this.getActiveTags();
|
||||
|
||||
// Toggle logic: if tag is active, remove it; otherwise add it
|
||||
const newTags = currentTags.includes(tagName)
|
||||
? currentTags.filter(t => t !== tagName)
|
||||
: [...currentTags, tagName];
|
||||
// // Toggle logic: if tag is active, remove it; otherwise add it
|
||||
// const newTags = currentTags.includes(tagName)
|
||||
// ? currentTags.filter(t => t !== tagName)
|
||||
// : [...currentTags, tagName];
|
||||
|
||||
// Navigate to new URL
|
||||
const queryString = this.generateTagQueryString(newTags);
|
||||
const newUrl = queryString ? `/?${queryString}` : '/';
|
||||
window.location.href = newUrl;
|
||||
}
|
||||
// // Navigate to new URL
|
||||
// const queryString = this.generateTagQueryString(newTags);
|
||||
// const newUrl = queryString ? `/?${queryString}` : '/';
|
||||
// window.location.href = newUrl;
|
||||
// }
|
||||
|
||||
// Attach click listeners to tag links
|
||||
attachTagClickListeners() {
|
||||
const tagLinks = document.querySelectorAll('.tag-pill, .post-tag');
|
||||
// // Attach click listeners to tag links
|
||||
// attachTagClickListeners() {
|
||||
// const tagLinks = document.querySelectorAll('[data-taglink], .post-tag');
|
||||
|
||||
tagLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const tagName = link.textContent?.trim();
|
||||
if (tagName) {
|
||||
this.toggleTag(tagName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// tagLinks.forEach(link => {
|
||||
// link.addEventListener('click', (e) => {
|
||||
// e.preventDefault();
|
||||
// const tagName = link.textContent?.trim();
|
||||
// if (tagName) {
|
||||
// this.toggleTag(tagName);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// Update visual state of tags based on current URL
|
||||
updateTagVisualState() {
|
||||
const activeTags = this.getActiveTags();
|
||||
// // Update visual state of tags based on current URL
|
||||
// updateTagVisualState() {
|
||||
// const activeTags = this.getActiveTags();
|
||||
|
||||
// Update tag pills in sidebar
|
||||
document.querySelectorAll('.tag-pill').forEach(link => {
|
||||
const tagName = link.textContent?.trim();
|
||||
if (tagName && activeTags.includes(tagName)) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// // Update tag pills in sidebar
|
||||
// document.querySelectorAll('.tag-pill').forEach(link => {
|
||||
// const tagName = link.textContent?.trim();
|
||||
// if (tagName && activeTags.includes(tagName)) {
|
||||
// link.classList.add('active');
|
||||
// } else {
|
||||
// link.classList.remove('active');
|
||||
// }
|
||||
// });
|
||||
|
||||
// Update post tags
|
||||
document.querySelectorAll('.post-tag').forEach(link => {
|
||||
const tagName = link.textContent?.trim();
|
||||
if (tagName && activeTags.includes(tagName)) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// // Update post tags
|
||||
// document.querySelectorAll('.post-tag').forEach(link => {
|
||||
// const tagName = link.textContent?.trim();
|
||||
// if (tagName && activeTags.includes(tagName)) {
|
||||
// link.classList.add('active');
|
||||
// } else {
|
||||
// link.classList.remove('active');
|
||||
// }
|
||||
// });
|
||||
|
||||
// Update clear filters button visibility
|
||||
const tagActions = document.querySelector('.tag-actions');
|
||||
if (tagActions) {
|
||||
tagActions.style.display = activeTags.length > 0 ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Update clear filters button visibility
|
||||
// const tagActions = document.querySelector('.tag-actions');
|
||||
// if (tagActions) {
|
||||
// tagActions.style.display = activeTags.length > 0 ? 'block' : 'none';
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Initialize on page load
|
||||
const tagPickerManager = new TagPickerManager();
|
||||
// // Initialize on page load
|
||||
// const tagPickerManager = new TagPickerManager();
|
||||
|
||||
// Update visual state after page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
tagPickerManager.updateTagVisualState();
|
||||
});
|
||||
// // Update visual state after page loads
|
||||
// document.addEventListener('DOMContentLoaded', () => {
|
||||
// tagPickerManager.updateTagVisualState();
|
||||
// });
|
||||
|
||||
// Also call immediately if DOM is already loaded
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
tagPickerManager.updateTagVisualState();
|
||||
}
|
||||
// // Also call immediately if DOM is already loaded
|
||||
// if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
// tagPickerManager.updateTagVisualState();
|
||||
// }
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
import React from 'react';
|
||||
import postArchiveScript from '../clientJS/post-archive' with { type: "text" };
|
||||
import { getPostsByYearAndMonth } from '../../db';
|
||||
import { minifyJS } from '../utils';
|
||||
import { db } from '../../db';
|
||||
|
||||
// @ts-expect-error - Importing as text, not a module
|
||||
import postArchiveScript from '../clientJS/post-archive' with { type: "text" };
|
||||
|
||||
// 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[];
|
||||
}
|
||||
|
||||
// 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;
|
||||
return cachedData;
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
export function PostArchive() {
|
||||
const archiveData = getArchiveData();
|
||||
|
||||
|
||||
return (
|
||||
<div className="postList sheet-background">
|
||||
<h3>Posts</h3>
|
||||
@@ -59,3 +76,69 @@ export function PostArchive() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
import React from 'react';
|
||||
import tagPickerScript from '../clientJS/tag-picker.js' with { type: "text" };
|
||||
import { minifyJS } from '../utils';
|
||||
import { dbConnection } from '../../db/index.js';
|
||||
|
||||
import { getAllTags } from '../../db/tags';
|
||||
// @ts-expect-error - Importing as text, not a module
|
||||
import tagPickerScript from '../clientJS/tag-picker.js' with { type: "text" };
|
||||
|
||||
const tags = getAllTags();
|
||||
const tags = dbConnection.getAllTags().map(tag => ({
|
||||
name: tag.name,
|
||||
post_count: tag.post_count,
|
||||
urlNormalized: tag.urlNormalized
|
||||
}));
|
||||
|
||||
export function TagPicker({ searchParams }: { searchParams?: URLSearchParams }) {
|
||||
const selectedTags = typeof searchParams === 'object' ? searchParams.getAll('tag') : [];
|
||||
|
||||
export function TagPicker() {
|
||||
return (
|
||||
<div className="tags sheet-background">
|
||||
<h3>Tags</h3>
|
||||
{tags.length > 0 ? (
|
||||
<ul className="tag-pills">
|
||||
{tags.map(tag => (
|
||||
<li key={tag.name}>
|
||||
<a
|
||||
data-tag
|
||||
href={`?tag=${tag.name.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
className="tag-pill"
|
||||
title={`${tag.post_count} post${tag.post_count !== 1 ? 's' : ''} (click to view)`}
|
||||
>
|
||||
{tag.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="no-tags-available">No tags available</p>
|
||||
<ul className="tag-pills">
|
||||
{tags.map(tag => {
|
||||
const active = selectedTags.includes(tag.urlNormalized)
|
||||
|
||||
return (
|
||||
< li key={tag.name} >
|
||||
<a
|
||||
title={`${tag.post_count} post${tag.post_count !== 1 ? 's' : ''} (click to view)`}
|
||||
data-tag
|
||||
className={`tag-pill ${active ? 'active' : ''}`}
|
||||
href={`?tag=${tag.urlNormalized}`}
|
||||
>
|
||||
{tag.name}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* Show clear tags button if there are selected tags */}
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="tag-actions">
|
||||
<a href="/" className="clear-tags-btn">
|
||||
Clear all filters
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="tag-actions" style={{ display: 'none' }}>
|
||||
<a href="/" className="clear-tags-btn">
|
||||
Clear all filters
|
||||
</a>
|
||||
</div>
|
||||
<script dangerouslySetInnerHTML={{ __html: minifyJS(tagPickerScript) }} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import themePickerScript from '../clientJS/theme-picker' with { type: "text" };
|
||||
import { minifyJS } from '../utils';
|
||||
|
||||
// @ts-expect-error - Importing as text, not a module
|
||||
import themePickerScript from '../clientJS/theme-picker' with { type: "text" };
|
||||
|
||||
const LIGHT_THEMES = ['latte', 'solarized-light', 'gruvbox-light'];
|
||||
const DARK_THEMES = ['frappe', 'macchiato', 'mocha', 'solarized-dark', 'gruvbox-dark', 'nord', 'dracula', 'one-dark', 'tokyo-night'];
|
||||
|
||||
@@ -26,15 +28,15 @@ export function ThemePicker() {
|
||||
<label htmlFor="theme" className="hidden">Theme</label>
|
||||
<div className="theme-controls">
|
||||
<div className="theme-mode-toggle">
|
||||
<button
|
||||
className="mode-btn active"
|
||||
<button
|
||||
className="mode-btn active"
|
||||
data-mode="light"
|
||||
id="lightModeBtn"
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
<button
|
||||
className="mode-btn"
|
||||
<button
|
||||
className="mode-btn"
|
||||
data-mode="dark"
|
||||
id="darkModeBtn"
|
||||
>
|
||||
@@ -42,8 +44,8 @@ export function ThemePicker() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="theme-dropdown-wrapper">
|
||||
<button
|
||||
className="theme-dropdown-trigger"
|
||||
<button
|
||||
className="theme-dropdown-trigger"
|
||||
id="themeDropdownTrigger"
|
||||
>
|
||||
<span id="currentThemeDisplay">Catppuccin Latte</span>
|
||||
@@ -62,4 +64,4 @@ export function ThemePicker() {
|
||||
<script dangerouslySetInnerHTML={{ __html: minifyJS(themePickerScript) }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import React from 'react';
|
||||
import { getRecentPosts, formatDate, calculateReadTime, getNumOfPosts } from '../../db/posts';
|
||||
import { parseTags } from '../../db/tags';
|
||||
import { type BlogPost } from '../../db/queries';
|
||||
|
||||
import { dbConnection } from '../../db';
|
||||
import { formatDate } from '../utils';
|
||||
|
||||
// Extract the post type from the database return type
|
||||
type Post = {
|
||||
id: number;
|
||||
path: string;
|
||||
title: string;
|
||||
date: string;
|
||||
readingTime: string;
|
||||
summary: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export function Home({ searchParams }: { searchParams: URLSearchParams }) {
|
||||
const postsPerPage = 10;
|
||||
const tags = searchParams.getAll('tag');
|
||||
|
||||
const currentPage = parseInt(searchParams.get('page') || "1", 10);
|
||||
const totalPages = Math.ceil(getNumOfPosts(tags) / postsPerPage);
|
||||
const totalPages = Math.ceil(dbConnection.getNumOfPosts(tags) / postsPerPage);
|
||||
const offset = (currentPage - 1) * postsPerPage;
|
||||
|
||||
|
||||
const posts = getRecentPosts(postsPerPage, offset, tags); // Get posts for the current page
|
||||
const posts = dbConnection.getPosts(postsPerPage, offset, tags); // Get posts for the current page
|
||||
|
||||
return (
|
||||
<main>
|
||||
@@ -33,12 +44,8 @@ export function Home({ searchParams }: { searchParams: URLSearchParams }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface PostCardProps {
|
||||
post: BlogPost;
|
||||
}
|
||||
|
||||
function PostCard({ post }: PostCardProps) {
|
||||
const tags = parseTags(post.tags);
|
||||
function PostCard({ post }: { post: Post }) {
|
||||
const tags = post.tags;
|
||||
const formattedDate = formatDate(post.date);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,4 +15,23 @@ export function minifyJS(code: string): string {
|
||||
});
|
||||
|
||||
return transpiler.transformSync(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate read time
|
||||
export function calculateReadTime(content: string): number {
|
||||
const wordsPerMinute = 200;
|
||||
const words = content.split(/\s+/).length;
|
||||
return Math.max(1, Math.ceil(words / wordsPerMinute));
|
||||
}
|
||||
|
||||
// Helper function to format date for display
|
||||
export function formatDate(dateString: string): string {
|
||||
// Parse ISO date string (YYYY-MM-DD)
|
||||
const date = new Date(dateString + 'T00:00:00');
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user