Complete WIP: Architecture refactor.

Mount JSX server side templating for blog posts. Send AppShell conditionally. Maintain support for HMR via HTMLbundles using Bun's native fullstack dev server under an /hmr path. This is only mounted in development and is supported by the onImport Bun plugin. Add DB creation on startup and load pages based on those records.
This commit is contained in:
2026-01-08 05:13:48 -08:00
parent 3abd97702d
commit f46f4667a1
32 changed files with 2779 additions and 353 deletions

View File

@@ -0,0 +1,104 @@
import React from 'react';
import postArchiveScript from '../clientJS/post-archive' with { type: "text" };
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" }
]
}
]
}
];
return (
<div className="postList sheet-background">
<h3>Posts</h3>
<ul className="post-archive">
{archiveData.map((yearData) => (
<li key={yearData.year}>
<div className="archive-year">
<span className="archive-toggle"></span>
<span>{yearData.year}</span>
<span className="post-count">{yearData.count}</span>
</div>
<div className="archive-content">
<ul className="archive-months">
{yearData.months.map((monthData) => (
<li key={monthData.name}>
<div className="archive-month">
<span className="archive-toggle"></span>
<span>{monthData.name}</span>
<span className="post-count">{monthData.count}</span>
</div>
<div className="archive-content">
<ul className="archive-posts">
{monthData.posts.map((post) => (
<li key={post.href} className="archive-post">
<a href={post.href}>{post.title}</a>
</li>
))}
</ul>
</div>
</li>
))}
</ul>
</div>
</li>
))}
</ul>
<script dangerouslySetInnerHTML={{ __html: minifyJS(postArchiveScript) }} />
</div>
)
}

View File

@@ -0,0 +1,70 @@
import React from 'react';
const urlLinkedIn = 'https://linkedin.com/in/CalebBraaten';
const urlX = 'https://x.com/CalebBraaten';
const urlGit = 'https://git.cbraaten.dev/Caleb';
export function ProfileBadge() {
return (
<div className="aboutMe sheet-background">
<div className="profile-header">
<img src="http://localhost:3000/profile-picture.webp" alt="Caleb Braaten" className="profile-picture" />
<h3 className="profile-name">Caleb Braaten</h3>
</div>
<ul className="social-links">
<li>
<a href={urlLinkedIn} data-external target="_blank" rel="noopener noreferrer" aria-label="LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</li>
<li>
<a href={urlX} data-external target="_blank" rel="noopener noreferrer" aria-label="X (Twitter)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</li>
<li>
<a href={urlGit} data-external target="_blank" rel="noopener noreferrer" aria-label="GitHub / Gitea">
<div className="git-icon-wrapper">
<svg className="icon-github" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
<svg className="icon-gitea" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
<g>
<path id="teabag" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8
c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4
c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<path d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2
c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5
c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5
c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3
c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1
C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4
c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7
S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55
c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8
l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
<path d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4
c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1
c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9
c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3
c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3
c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29
c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8
C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>
</div>
</a>
</li>
</ul>
</div>
)
}

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { minifyJS } from '../utils';
export function TagPicker({ availableTags = [], activeTags = [] }: {
availableTags?: { name: string; post_count: number }[],
activeTags?: string[]
}) {
return (
<div className="tags sheet-background">
<h3>Tags</h3>
{availableTags.length > 0 ? (
<ul className="tag-pills">
{availableTags.map(tag => (
<li key={tag.name}>
<a
href={`?tag=${tag.name.toLowerCase().replace(/\s+/g, '-')}`}
className="tag-pill"
title={`${tag.post_count} post${tag.post_count !== 1 ? 's' : ''} (click to toggle)`}
>
{tag.name}
</a>
</li>
))}
</ul>
) : (
<p className="no-tags-available">No tags available</p>
)}
<div className="tag-actions" style={{ display: 'none' }}>
<a href="/" className="clear-tags-btn">
Clear all filters
</a>
</div>
<script dangerouslySetInnerHTML={{ __html: minifyJS(tagToggleScript) }} />
</div>
)
}
// Client-side script for tag toggle functionality
const tagToggleScript = `
(function() {
'use strict';
class TagPickerManager {
constructor() {
this.init();
}
init() {
this.attachTagClickListeners();
this.updateTagVisualState();
}
// Parse current URL to get active tags
getActiveTags() {
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) {
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) {
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];
// 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');
tagLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
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 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 clear filters button visibility
const tagActions = document.querySelector('.tag-actions');
if (tagActions) {
tagActions.style.display = activeTags.length > 0 ? 'block' : 'none';
}
}
}
// Initialize on DOM ready
function initializeTagPicker() {
// Remove any existing instance
if (window.tagPickerManager) {
delete window.tagPickerManager;
}
// Create new instance
window.tagPickerManager = new TagPickerManager();
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeTagPicker);
} else {
initializeTagPicker();
}
})();
`;

View File

@@ -1,13 +1,65 @@
import React from 'react';
import themePickerScript from '../clientJS/theme-picker' with { type: "text" };
import { minifyJS } from '../utils';
const LIGHT_THEMES = ['latte', 'solarized-light', 'gruvbox-light'];
const DARK_THEMES = ['frappe', 'macchiato', 'mocha', 'solarized-dark', 'gruvbox-dark', 'nord', 'dracula', 'one-dark', 'tokyo-night'];
const THEME_NAMES: Record<string, string> = {
latte: 'Catppuccin Latte',
frappe: 'Catppuccin Frappe',
macchiato: 'Catppuccin Macchiato',
mocha: 'Catppuccin Mocha',
'solarized-light': 'Solarized Light',
'solarized-dark': 'Solarized Dark',
'gruvbox-light': 'Gruvbox Light',
'gruvbox-dark': 'Gruvbox Dark',
nord: 'Nord',
dracula: 'Dracula',
'one-dark': 'One Dark',
'tokyo-night': 'Tokyo Night'
};
export function ThemePicker() {
const randomNumber = Math.random();
return (
<div>
<h1>Sub Component!</h1>
<p>Some text here... maybe? with hot reloading! {randomNumber}</p>
<div className="themePicker sheet-background">
<label htmlFor="theme" className="hidden">Theme</label>
<div className="theme-controls">
<div className="theme-mode-toggle">
<button
className="mode-btn active"
data-mode="light"
id="lightModeBtn"
>
Light
</button>
<button
className="mode-btn"
data-mode="dark"
id="darkModeBtn"
>
Dark
</button>
</div>
<div className="theme-dropdown-wrapper">
<button
className="theme-dropdown-trigger"
id="themeDropdownTrigger"
>
<span id="currentThemeDisplay">Catppuccin Latte</span>
<span className="dropdown-arrow"></span>
</button>
<div className="theme-dropdown-menu" id="themeDropdownMenu">
{LIGHT_THEMES.map(theme => (
<div key={theme} className="theme-option" data-theme={theme}>
<div className="theme-name">{THEME_NAMES[theme]}</div>
<button className="theme-selector-btn" data-theme={theme}></button>
</div>
))}
</div>
</div>
</div>
<script dangerouslySetInnerHTML={{ __html: minifyJS(themePickerScript) }} />
</div>
)
}
}