This commit is contained in:
2026-01-14 01:23:03 +01:00
parent e46b104127
commit 172e2600d1
20 changed files with 3095 additions and 914 deletions

View File

@@ -0,0 +1,336 @@
---
// FileExample.astro - Static file display component with syntax highlighting
import Prism from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-jsx';
import 'prismjs/components/prism-tsx';
import 'prismjs/components/prism-docker';
import 'prismjs/components/prism-yaml';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-sql';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-markdown';
interface Props {
filename: string;
content: string;
language: string;
description?: string;
tags?: string[];
id: string;
}
const { filename, content, language, id } = Astro.props;
const safeId = String(id).replace(/[^a-zA-Z0-9_-]/g, '');
const headerId = `file-example-header-${safeId}`;
const contentId = `file-example-content-${safeId}`;
const fileExtension = filename.split('.').pop() || language;
const prismLanguageMap: Record<string, string> = {
py: 'python',
ts: 'typescript',
tsx: 'tsx',
js: 'javascript',
jsx: 'jsx',
dockerfile: 'docker',
docker: 'docker',
yml: 'yaml',
yaml: 'yaml',
json: 'json',
html: 'markup',
css: 'css',
sql: 'sql',
sh: 'bash',
bash: 'bash',
md: 'markdown',
};
const prismLanguage = prismLanguageMap[fileExtension] || 'markup';
const highlightedCode = Prism.highlight(
content,
Prism.languages[prismLanguage] || Prism.languages.markup,
prismLanguage,
);
---
<div
class="file-example w-full bg-white border border-slate-200/80 rounded-lg overflow-hidden"
data-file-example
data-expanded="false"
>
<div
class="px-3 py-2 flex items-center justify-between gap-3 cursor-pointer select-none bg-white hover:bg-slate-50/60 transition-colors"
data-file-header
role="button"
tabindex="0"
aria-expanded="false"
aria-controls={contentId}
id={headerId}
>
<div class="flex items-center gap-2 min-w-0">
<svg
class="w-3 h-3 text-slate-400 flex-shrink-0 toggle-icon transition-transform"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
<span class="text-xs font-mono text-slate-900 truncate" title={filename}>{filename}</span>
</div>
<div class="flex items-center gap-1 flex-shrink-0" onclick="event.stopPropagation()">
<button
type="button"
class="copy-btn h-7 w-7 inline-flex items-center justify-center rounded-md border border-transparent hover:border-slate-200 hover:bg-white transition-colors"
data-content={content}
data-filename={filename}
title="Copy to clipboard"
aria-label={`Copy ${filename} to clipboard`}
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
<button
type="button"
class="download-btn h-7 w-7 inline-flex items-center justify-center rounded-md border border-transparent hover:border-slate-200 hover:bg-white transition-colors"
data-content={content}
data-filename={filename}
title="Download file"
aria-label={`Download ${filename}`}
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
</div>
</div>
<div
class="file-example__content max-h-0 opacity-0 overflow-hidden transition-[max-height,opacity] duration-200 ease-out bg-slate-50"
data-file-content
id={contentId}
role="region"
aria-labelledby={headerId}
>
<pre
class="m-0 p-3 overflow-x-auto overflow-y-auto text-[13px] leading-[1.65] font-mono text-slate-800 hide-scrollbar"
style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; max-height: 22rem;"
><code class={`language-${prismLanguage}`} set:html={highlightedCode}></code></pre>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-file-example]').forEach((node) => {
if (!(node instanceof HTMLElement)) return;
const header = node.querySelector('[data-file-header]');
const content = node.querySelector('[data-file-content]');
if (!(header instanceof HTMLElement) || !(content instanceof HTMLElement)) return;
const collapse = () => {
node.dataset.expanded = 'false';
header.setAttribute('aria-expanded', 'false');
content.classList.add('max-h-0', 'opacity-0');
content.classList.remove('max-h-[22rem]', 'opacity-100');
};
const expand = () => {
node.dataset.expanded = 'true';
header.setAttribute('aria-expanded', 'true');
content.classList.remove('max-h-0', 'opacity-0');
content.classList.add('max-h-[22rem]', 'opacity-100');
};
collapse();
const toggle = () => {
const isExpanded = node.dataset.expanded === 'true';
if (isExpanded) collapse();
else expand();
if (!isExpanded) {
setTimeout(() => {
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 120);
}
};
header.addEventListener('click', toggle);
header.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
toggle();
});
});
document.querySelectorAll('.copy-btn').forEach((btn) => {
if (!(btn instanceof HTMLButtonElement)) return;
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const text = btn.dataset.content || '';
try {
await navigator.clipboard.writeText(text);
btn.dataset.copied = 'true';
setTimeout(() => {
delete btn.dataset.copied;
}, 900);
} catch (err) {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
}
});
});
document.querySelectorAll('.download-btn').forEach((btn) => {
if (!(btn instanceof HTMLButtonElement)) return;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const fileContent = btn.dataset.content || '';
const fileName = btn.dataset.filename || 'download.txt';
const blob = new Blob([fileContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
});
});
</script>
<style is:global>
[data-file-example] {
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
[data-file-example][data-expanded='true'] .toggle-icon {
transform: rotate(0deg);
}
.toggle-icon {
transform: rotate(-90deg);
}
.copy-btn,
.download-btn {
color: #475569;
}
.copy-btn[data-copied='true'] {
color: #065f46;
background: rgba(16, 185, 129, 0.10);
border-color: rgba(16, 185, 129, 0.35);
}
/* Prism.js syntax highlighting - light, low-noise */
code[class*='language-'],
pre[class*='language-'] {
color: #0f172a;
background: transparent;
text-shadow: none;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 0.8125rem;
line-height: 1.65;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
tab-size: 2;
hyphens: none;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #64748b;
font-style: italic;
}
.token.punctuation {
color: #94a3b8;
}
.token.operator {
color: #64748b;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #c2410c;
}
.token.boolean,
.token.number {
color: #a16207;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #059669;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #475569;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #7c3aed;
font-weight: 500;
}
.token.function,
.token.class-name {
color: #2563eb;
}
.token.regex,
.token.important,
.token.variable {
color: #db2777;
}
</style>

View File

@@ -0,0 +1,150 @@
---
// FileExamplesList.astro - Static component that renders at build time
import { FileExampleManager, type FileExampleGroup } from '../data/fileExamples';
import FileExample from './FileExample.astro';
interface Props {
groupId?: string;
showAll?: boolean;
tags?: string[];
postSlug?: string;
}
const { groupId, showAll = false, tags, postSlug } = Astro.props;
// Load and filter file examples at build time (no loading state needed)
let groups: FileExampleGroup[] = [];
try {
if (postSlug) {
const allGroups = await FileExampleManager.getAllGroups();
groups = allGroups
.map((group) => ({
...group,
files: group.files.filter((file) => {
if (file.postSlug !== postSlug) return false;
if (groupId && group.groupId !== groupId) return false;
if (tags && tags.length > 0) return file.tags?.some((tag) => tags.includes(tag));
return true;
}),
}))
.filter((group) => group.files.length > 0);
} else if (groupId) {
const group = await FileExampleManager.getGroup(groupId);
groups = group ? [group] : [];
} else if (tags && tags.length > 0) {
const allGroups = await FileExampleManager.getAllGroups();
groups = allGroups
.map((group) => ({
...group,
files: group.files.filter((file) => tags.some((tag) => file.tags?.includes(tag))),
}))
.filter((group) => group.files.length > 0);
} else if (showAll) {
groups = await FileExampleManager.getAllGroups();
} else {
groups = await FileExampleManager.getAllGroups();
}
} catch (error) {
console.error('Error loading file examples:', error);
groups = [];
}
---
{groups.length === 0 ? (
<div class="bg-slate-50 border border-slate-200 rounded-lg px-4 py-6 text-center">
<svg class="w-6 h-6 mx-auto text-slate-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="text-slate-500 text-sm">No files found</p>
</div>
) : (
<div class="space-y-3">
{groups.map((group) => (
<section
class="not-prose bg-white border border-slate-200/80 rounded-lg w-full overflow-hidden"
data-file-examples-group
>
<header class="px-3 py-2 grid grid-cols-[1fr_auto] items-center gap-3 border-b border-slate-200/80 bg-white">
<h3 class="m-0 text-xs font-semibold text-slate-900 truncate tracking-tight leading-none">{group.title}</h3>
<div class="flex items-center gap-2">
<span class="text-[11px] text-slate-600 bg-slate-100/80 border border-slate-200/60 rounded-full px-2 py-0.5 tabular-nums">
{group.files.length} files
</span>
<button
type="button"
class="toggle-all-btn h-7 w-7 inline-flex items-center justify-center rounded-md border border-transparent hover:border-slate-200 hover:bg-white text-slate-500 hover:text-slate-900 transition-colors"
title="Toggle all"
>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</header>
<div class="px-2 py-2 space-y-2">
{group.files.map((file) => (
<FileExample
filename={file.filename}
content={file.content}
language={file.language}
description={file.description}
tags={file.tags}
id={file.id}
/>
))}
</div>
</section>
))}
</div>
)}
<script>
// @ts-nocheck
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-file-examples-group]').forEach((groupNode) => {
if (!(groupNode instanceof HTMLElement)) return;
const toggleBtn = groupNode.querySelector('.toggle-all-btn');
if (!(toggleBtn instanceof HTMLButtonElement)) return;
const setIcon = () => {
const expanded = anyExpanded();
toggleBtn.innerHTML = expanded
? '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" /></svg>'
: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>';
};
const getItems = () => {
const examples = groupNode.querySelectorAll('[data-file-example]');
return Array.from(examples).filter((n) => n instanceof HTMLElement);
};
const anyExpanded = () => getItems().some((n) => n.dataset.expanded === 'true');
setIcon();
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const items = getItems();
const shouldCollapse = anyExpanded();
items.forEach((fileExample) => {
const header = fileExample.querySelector('[data-file-header]');
if (!(header instanceof HTMLElement)) return;
const isExpanded = fileExample.dataset.expanded === 'true';
if (shouldCollapse ? isExpanded : !isExpanded) header.click();
});
setTimeout(() => {
setIcon();
}, 0);
});
});
});
</script>

View File

@@ -1,6 +1,4 @@
---
import Tag from './Tag.astro';
interface Props {
post: {
title: string;
@@ -12,41 +10,41 @@ interface Props {
}
const { post } = Astro.props;
const { title, description, date, slug, tags = [] } = post;
const { title, description, date, tags = [] } = post;
const formattedDate = new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
year: 'numeric',
});
// Calculate reading time
const wordCount = description.split(/\s+/).length;
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
---
<article class="post-card bg-white border border-slate-200 rounded-xl p-5 hover:border-blue-300 hover:shadow-md transition-all duration-200">
<div class="flex items-start justify-between mb-3">
<h3 class="text-lg font-serif font-medium text-slate-900 hover:text-blue-600 transition-colors leading-tight flex-1 mr-3">
{title}
<article class="post-card bg-white border border-slate-200/80 rounded-lg px-4 py-3">
<div class="flex items-start justify-between gap-4">
<h3 class="m-0 text-sm font-semibold leading-snug tracking-tight">
<span class="relative inline-block text-slate-900 marker-title" style={`--marker-seed:${Math.abs(title.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0)) % 7};`}>
{title}
</span>
</h3>
<span class="text-xs text-slate-500 font-sans whitespace-nowrap flex-shrink-0">
<time class="text-[11px] text-slate-500 tabular-nums whitespace-nowrap leading-none pt-0.5">
{formattedDate}
</span>
</time>
</div>
<p class="text-slate-600 leading-relaxed font-serif text-sm mb-3 line-clamp-3">
<p class="post-excerpt mt-2 mb-0 text-[13px] leading-relaxed text-slate-600 line-clamp-3">
{description}
</p>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500 font-sans">
{readingTime} min
</span>
<div class="mt-3 flex items-center justify-between gap-3">
<span class="text-[11px] text-slate-500 tabular-nums leading-none">{readingTime} min</span>
{tags.length > 0 && (
<div class="flex gap-1">
{tags.map((tag: string) => (
<span class="bg-slate-100 text-slate-700 px-2 py-0.5 rounded text-[10px] font-medium">
<div class="flex flex-wrap items-center justify-end gap-1">
{tags.slice(0, 3).map((tag: string) => (
<span class="inline-flex items-center rounded-full bg-slate-100/80 border border-slate-200/60 px-2 py-0.5 text-[11px] text-slate-700 leading-none">
{tag}
</span>
))}
@@ -55,76 +53,72 @@ const readingTime = Math.max(1, Math.ceil(wordCount / 200));
</div>
</article>
<style>
/* Simple reveal animation */
@keyframes cardReveal {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.post-card {
animation: cardReveal 0.3s ease-out both;
}
/* Stagger for multiple cards */
.post-card:nth-child(1) { animation-delay: 0.05s; }
.post-card:nth-child(2) { animation-delay: 0.1s; }
.post-card:nth-child(3) { animation-delay: 0.15s; }
.post-card:nth-child(4) { animation-delay: 0.2s; }
.post-card:nth-child(5) { animation-delay: 0.25s; }
.post-card:nth-child(6) { animation-delay: 0.3s; }
/* Clean hover effects */
.post-card:hover {
transform: translateY(-1px);
}
/* Title underline on hover */
.post-card h3 {
position: relative;
display: inline-block;
}
.post-card h3::after {
<style is:global>
.marker-title::before {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 1px;
background: #3b82f6;
transition: width 0.2s ease;
left: -0.15em;
right: -0.15em;
bottom: 0.05em;
height: 0.62em;
border-radius: 0.18em;
z-index: -1;
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0) 20%,
rgba(253, 230, 138, 0.70) 20%,
rgba(253, 230, 138, 0.70) 100%
);
transform-origin: left center;
transform:
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg))
skewX(calc((var(--marker-seed, 0) - 3) * -0.25deg));
filter: saturate(1.05);
}
.post-card:hover h3::after {
width: 100%;
.marker-title::after {
content: '';
position: absolute;
left: -0.18em;
right: -0.05em;
bottom: 0.05em;
height: 0.62em;
border-radius: 0.18em;
z-index: -1;
background:
linear-gradient(
90deg,
rgba(253, 230, 138, 0.00) 0%,
rgba(253, 230, 138, 0.60) 8%,
rgba(253, 230, 138, 0.55) 60%,
rgba(253, 230, 138, 0.35) 100%
);
opacity: 0.75;
mix-blend-mode: multiply;
transform:
rotate(calc((var(--marker-seed, 0) - 3) * 0.45deg))
translateY(0.02em);
mask-image:
linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 20%,
rgba(0, 0, 0, 1) 100%
);
}
/* Line clamp */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
@media (prefers-reduced-motion: no-preference) {
.post-link:hover .marker-title::before,
.post-link:hover .marker-title::after {
filter: saturate(1.08) contrast(1.02);
}
}
/* Focus styles for accessibility */
.post-card:focus-within {
outline: 2px solid #3b82f6;
outline-offset: 2px;
transform: translateY(-1px);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.post-card h3:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: 2px;
}
</style>
</style>

View File

@@ -68,19 +68,14 @@ export const SearchBar: React.FC = () => {
return (
<div className="relative w-full max-w-2xl mx-auto">
<div className={`relative flex items-center transition-all duration-300 ${
isFocused ? 'scale-[1.02]' : 'scale-100'
}`}>
{/* Search input wrapper */}
<div className="relative flex items-center">
<div className="relative flex-1">
<input
ref={inputRef}
type="text"
placeholder="Search posts, topics, or tags..."
className={`w-full px-5 py-4 pl-12 text-base border-2 rounded-xl transition-all duration-300 font-sans focus:outline-none ${
isFocused
? 'border-blue-400 bg-white shadow-lg'
: 'border-slate-200 bg-white/90 hover:border-slate-300'
placeholder="Search"
className={`w-full px-3 py-2 text-[14px] border border-slate-200 rounded-md bg-transparent transition-colors font-sans focus:outline-none ${
isFocused ? 'bg-white border-slate-300' : 'hover:border-slate-300'
}`}
onInput={handleInput}
onKeyDown={handleKeyDown}
@@ -88,54 +83,23 @@ export const SearchBar: React.FC = () => {
onBlur={() => setIsFocused(false)}
aria-label="Search blog posts"
/>
{/* Search icon */}
<div className={`absolute left-4 top-1/2 -translate-y-1/2 transition-all duration-300 ${
isFocused ? 'text-blue-600 scale-110' : 'text-slate-400'
}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
{/* Clear button */}
{query && (
<button
onClick={clearSearch}
className="absolute right-4 top-1/2 -translate-y-1/2 p-1.5 rounded-full hover:bg-slate-100 transition-colors duration-200 text-slate-500 hover:text-slate-700"
className="absolute right-1.5 top-1/2 -translate-y-1/2 h-7 px-2 inline-flex items-center justify-center rounded text-[12px] text-slate-500 hover:text-slate-700 hover:bg-slate-100 transition-colors"
aria-label="Clear search"
type="button"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Clear
</button>
)}
</div>
{/* Search hint */}
<div className="hidden md:flex items-center gap-2 ml-3 text-xs text-slate-500 font-sans">
<div className="hidden md:flex items-center gap-2 ml-3 text-xs text-slate-400 font-sans">
<kbd className="bg-slate-100 px-2 py-1 rounded border border-slate-200">ESC</kbd>
<span>to clear</span>
</div>
</div>
{/* Active filter indicator */}
{query && (
<div className="mt-3 flex items-center justify-between text-sm">
<span className="text-slate-600">
Searching for: <span className="font-semibold text-blue-600">"{query}"</span>
</span>
<button
onClick={clearSearch}
className="text-slate-500 hover:text-blue-600 transition-colors duration-200 font-medium"
>
Show all posts
</button>
</div>
)}
{/* Results count (hidden by default, shown via JS) */}
<div id="search-results-count" className="mt-2 text-sm text-slate-500 font-sans hidden"></div>
</div>
);
};

View File

@@ -22,12 +22,6 @@ const colorClass = getColorClass(tag);
<span class={`highlighter-tag ${colorClass} ${className} inline-block text-xs font-bold px-2.5 py-1 rounded cursor-pointer transition-all duration-200 relative overflow-hidden group`} data-tag={tag}>
<span class="relative z-10">{tag}</span>
<!-- Subtle underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-current transition-all duration-300 group-hover:w-full opacity-50"></span>
<!-- Micro-interaction: small dot that appears on hover -->
<span class="absolute top-1/2 left-1/2 w-0 h-0 bg-white/30 rounded-full transform -translate-x-1/2 -translate-y-1/2 transition-all duration-300 group-hover:w-1 group-hover:h-1"></span>
</span>
<style>

View File

@@ -20,5 +20,19 @@ export const blogPosts: BlogPost[] = [
date: "2024-01-20",
slug: "debugging-tips",
tags: ["debugging", "tools"]
},
{
title: "Software Architecture Patterns",
description: "Exploring common architectural patterns for scalable systems",
date: "2024-02-01",
slug: "architecture-patterns",
tags: ["architecture", "design-patterns", "system-design"]
},
{
title: "Docker Deployment Best Practices",
description: "Production-ready Docker configurations and workflows",
date: "2024-02-10",
slug: "docker-deployment",
tags: ["docker", "deployment", "architecture"]
}
];

628
src/data/fileExamples.ts Normal file
View File

@@ -0,0 +1,628 @@
/**
* File Examples Data Structure
*
* This module manages file examples for blog posts.
* Each example includes the file content, metadata, and can be easily copied or downloaded.
*/
export interface FileExample {
id: string;
filename: string;
content: string;
language: string;
description?: string;
tags?: string[];
postSlug?: string;
createdAt: string;
updatedAt: string;
}
export interface FileExampleGroup {
groupId: string;
title: string;
description?: string;
files: FileExample[];
}
// In-memory storage (for development)
// In production, this could be backed by a database or file system
const fileExamplesStore = new Map<string, FileExample>();
// Sample file examples for demonstration
export const sampleFileExamples: FileExampleGroup[] = [
{
groupId: "python-data-processing",
title: "Python Data Processing Example",
description: "A complete example of processing data with error handling",
files: [
{
id: "python-data-processor",
filename: "data_processor.py",
content: `import json
import logging
from typing import List, Dict, Any
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class DataProcessor:
def __init__(self, input_path: str, output_path: str):
self.input_path = Path(input_path)
self.output_path = Path(output_path)
def load_data(self) -> List[Dict[str, Any]]:
"""Load JSON data from input file."""
if not self.input_path.exists():
raise FileNotFoundError(f"Input file not found: {self.input_path}")
with open(self.input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
logger.info(f"Loaded {len(data)} records")
return data
def process_records(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Process records and add computed fields."""
processed = []
for record in data:
# Add timestamp
import time
record['processed_at'] = time.time()
# Normalize keys
record['id'] = record.get('id', '').lower()
processed.append(record)
logger.info(f"Processed {len(processed)} records")
return processed
def save_data(self, data: List[Dict[str, Any]]) -> None:
"""Save processed data to output file."""
self.output_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
logger.info(f"Saved {len(data)} records to {self.output_path}")
def run(self) -> None:
"""Execute the complete processing pipeline."""
try:
data = self.load_data()
processed = self.process_records(data)
self.save_data(processed)
logger.info("Processing completed successfully")
except Exception as e:
logger.error(f"Processing failed: {e}")
raise
if __name__ == "__main__":
# Example usage
processor = DataProcessor(
input_path="data/input.json",
output_path="data/processed.json"
)
processor.run()`,
language: "python",
description: "A robust data processor with logging and error handling",
tags: ["python", "data-processing", "logging"],
postSlug: "debugging-tips",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "python-config-example",
filename: "config.py",
content: `"""
Configuration management for the data processor
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class Config:
"""Configuration for data processing."""
input_path: str
output_path: str
batch_size: int = 1000
max_workers: int = 4
enable_caching: bool = True
log_level: str = "INFO"
@classmethod
def from_dict(cls, data: dict) -> 'Config':
"""Create config from dictionary."""
return cls(**data)
def to_dict(self) -> dict:
"""Convert config to dictionary."""
return {
'input_path': self.input_path,
'output_path': self.output_path,
'batch_size': self.batch_size,
'max_workers': self.max_workers,
'enable_caching': self.enable_caching,
'log_level': self.log_level
}
# Default configuration
DEFAULT_CONFIG = Config(
input_path="data/input.json",
output_path="data/output.json",
batch_size=500,
max_workers=2
)`,
language: "python",
description: "Configuration management using dataclasses",
tags: ["python", "configuration", "dataclasses"],
postSlug: "debugging-tips",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
},
{
groupId: "typescript-architecture",
title: "TypeScript Architecture Patterns",
description: "Modern TypeScript patterns for scalable applications",
files: [
{
id: "ts-interface-example",
filename: "interfaces.ts",
content: `/**
* Core interfaces for a scalable TypeScript application
*/
// Repository pattern
export interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(entity: Omit<T, 'id'>): Promise<T>;
update(id: string, entity: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
// Service layer interface
export interface Service<T> {
get(id: string): Promise<T>;
list(): Promise<T[]>;
create(data: any): Promise<T>;
update(id: string, data: any): Promise<T>;
remove(id: string): Promise<void>;
}
// Event system
export interface DomainEvent {
type: string;
payload: any;
timestamp: Date;
source: string;
}
export interface EventHandler {
handle(event: DomainEvent): Promise<void>;
}
export interface EventPublisher {
publish(event: DomainEvent): Promise<void>;
subscribe(handler: EventHandler): void;
}
// Result type for error handling
export type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
export namespace Result {
export function ok<T>(value: T): Result<T> {
return { success: true, value };
}
export function fail<E extends Error>(error: E): Result<never, E> {
return { success: false, error };
}
export function isOk<T, E>(result: Result<T, E>): result is { success: true; value: T } {
return result.success;
}
export function isFail<T, E>(result: Result<T, E>): result is { success: false; error: E } {
return !result.success;
}
}`,
language: "typescript",
description: "TypeScript interfaces for clean architecture",
tags: ["typescript", "architecture", "interfaces"],
postSlug: "architecture-patterns",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "ts-service-example",
filename: "userService.ts",
content: `import { Repository, Service, Result, DomainEvent, EventPublisher } from './interfaces';
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
interface CreateUserDTO {
email: string;
name: string;
}
class UserService implements Service<User> {
constructor(
private readonly userRepository: Repository<User>,
private readonly eventPublisher: EventPublisher
) {}
async get(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error(\`User with id \${id} not found\`);
}
return user;
}
async list(): Promise<User[]> {
return this.userRepository.findAll();
}
async create(data: CreateUserDTO): Promise<User> {
// Validate email
if (!this.isValidEmail(data.email)) {
throw new Error('Invalid email format');
}
// Create user
const user = await this.userRepository.create({
...data,
createdAt: new Date(),
updatedAt: new Date()
});
// Publish event
const event: DomainEvent = {
type: 'USER_CREATED',
payload: { userId: user.id, email: user.email },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const existing = await this.get(id);
const updated = await this.userRepository.update(id, {
...data,
updatedAt: new Date()
});
const event: DomainEvent = {
type: 'USER_UPDATED',
payload: { userId: id, changes: data },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
return updated;
}
async remove(id: string): Promise<void> {
const success = await this.userRepository.delete(id);
if (!success) {
throw new Error(\`Failed to delete user \${id}\`);
}
const event: DomainEvent = {
type: 'USER_DELETED',
payload: { userId: id },
timestamp: new Date(),
source: 'UserService'
};
await this.eventPublisher.publish(event);
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
return emailRegex.test(email);
}
// Additional business logic
async getUserByEmail(email: string): Promise<User | null> {
const users = await this.userRepository.findAll();
return users.find(u => u.email === email) || null;
}
}
export { UserService, type User, type CreateUserDTO };`,
language: "typescript",
description: "Service implementation with domain events",
tags: ["typescript", "service-layer", "domain-events"],
postSlug: "architecture-patterns",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
},
{
groupId: "docker-deployment",
title: "Docker Deployment Configuration",
description: "Production-ready Docker setup",
files: [
{
id: "dockerfile",
filename: "Dockerfile",
content: `# Multi-stage build for optimized production image
FROM node:20-alpine AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts
# Build stage
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S astro -u 1001
# Copy built assets
COPY --from=builder --chown=astro:nodejs /app/dist ./dist
COPY --from=deps --chown=astro:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=astro:nodejs /app/package*.json ./
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD node -e "require('http').get('http://localhost:4321/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run as non-root
USER astro
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]`,
language: "dockerfile",
description: "Multi-stage Docker build for production",
tags: ["docker", "production", "multi-stage"],
postSlug: "docker-deployment",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
{
id: "docker-compose",
filename: "docker-compose.yml",
content: `version: '3.8'
services:
web:
build:
context: .
target: production
ports:
- "8080:4321"
environment:
- NODE_ENV=production
- PORT=4321
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:4321/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
# Optional: Add Redis for caching
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
deploy:
resources:
limits:
memory: 256M
# Optional: Add Caddy for reverse proxy
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
redis_data:
caddy_data:
caddy_config:`,
language: "yaml",
description: "Multi-service Docker Compose setup",
tags: ["docker", "compose", "orchestration"],
postSlug: "docker-deployment",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
]
}
];
// Helper functions for managing file examples
export class FileExampleManager {
static async getFileExample(id: string): Promise<FileExample | undefined> {
// First check in-memory store
const stored = fileExamplesStore.get(id);
if (stored) return stored;
// Search in sample data
for (const group of sampleFileExamples) {
const file = group.files.find(f => f.id === id);
if (file) return file;
}
return undefined;
}
static async getFilesByTag(tag: string): Promise<FileExample[]> {
const results: FileExample[] = [];
for (const group of sampleFileExamples) {
for (const file of group.files) {
if (file.tags?.includes(tag)) {
results.push(file);
}
}
}
return results;
}
static async searchFiles(query: string): Promise<FileExample[]> {
const lowerQuery = query.toLowerCase();
const results: FileExample[] = [];
for (const group of sampleFileExamples) {
for (const file of group.files) {
const searchable = [
file.filename,
file.description,
file.language,
...(file.tags || [])
].join(' ').toLowerCase();
if (searchable.includes(lowerQuery)) {
results.push(file);
}
}
}
return results;
}
static async getAvailableTags(): Promise<string[]> {
const tags = new Set<string>();
for (const group of sampleFileExamples) {
for (const file of group.files) {
file.tags?.forEach(tag => tags.add(tag));
}
}
return Array.from(tags).sort();
}
static async createFileExample(example: Omit<FileExample, 'id' | 'createdAt' | 'updatedAt'>): Promise<FileExample> {
const id = `${example.filename.replace(/[^a-zA-Z0-9]/g, '-')}-${Date.now()}`;
const newExample: FileExample = {
...example,
id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
fileExamplesStore.set(id, newExample);
return newExample;
}
static async updateFileExample(id: string, updates: Partial<FileExample>): Promise<FileExample | undefined> {
const existing = await this.getFileExample(id);
if (!existing) return undefined;
const updated: FileExample = {
...existing,
...updates,
updatedAt: new Date().toISOString()
};
fileExamplesStore.set(id, updated);
return updated;
}
static async deleteFileExample(id: string): Promise<boolean> {
return fileExamplesStore.delete(id);
}
static async getAllGroups(): Promise<FileExampleGroup[]> {
return sampleFileExamples;
}
static async getGroup(groupId: string): Promise<FileExampleGroup | undefined> {
return sampleFileExamples.find(g => g.groupId === groupId);
}
static async downloadFile(id: string): Promise<{ filename: string; content: string; mimeType: string } | null> {
const file = await this.getFileExample(id);
if (!file) return null;
const mimeType = this.getMimeType(file.language);
return {
filename: file.filename,
content: file.content,
mimeType
};
}
static async downloadMultiple(ids: string[]): Promise<Array<{ filename: string; content: string }>> {
const files = await Promise.all(ids.map(id => this.getFileExample(id)));
return files
.filter((f): f is FileExample => f !== undefined)
.map(f => ({ filename: f.filename, content: f.content }));
}
private static getMimeType(language: string): string {
const mimeTypes: Record<string, string> = {
'python': 'text/x-python',
'typescript': 'text/x-typescript',
'javascript': 'text/javascript',
'dockerfile': 'text/x-dockerfile',
'yaml': 'text/yaml',
'json': 'application/json',
'html': 'text/html',
'css': 'text/css',
'sql': 'text/x-sql',
'bash': 'text/x-shellscript',
'text': 'text/plain'
};
return mimeTypes[language] || 'text/plain';
}
}

View File

@@ -182,4 +182,4 @@ const { title, description = "Technical problem solver's blog - practical insigh
}
</style>
</body>
</html>
</html>

View File

@@ -0,0 +1,286 @@
import type { APIRoute } from 'astro';
import { FileExampleManager } from '../../data/fileExamples';
// Simple ZIP creation without external dependencies
class SimpleZipCreator {
private files: Array<{ filename: string; content: string }> = [];
addFile(filename: string, content: string) {
this.files.push({ filename, content });
}
// Create a basic ZIP file structure
create(): number[] {
const encoder = new TextEncoder();
const chunks: number[][] = [];
let offset = 0;
const centralDirectory: Array<{
name: string;
offset: number;
size: number;
compressedSize: number;
}> = [];
// Process each file
for (const file of this.files) {
const contentBytes = Array.from(encoder.encode(file.content));
const filenameBytes = Array.from(encoder.encode(file.filename));
// Local file header
const localHeader: number[] = [];
// Local file header signature (little endian)
localHeader.push(0x50, 0x4b, 0x03, 0x04);
// Version needed to extract
localHeader.push(20, 0);
// General purpose bit flag
localHeader.push(0, 0);
// Compression method (0 = store)
localHeader.push(0, 0);
// Last modified time/date
localHeader.push(0, 0, 0, 0);
// CRC32 (0 for simplicity)
localHeader.push(0, 0, 0, 0);
// Compressed size
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
// Uncompressed size
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
// Filename length
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
// Extra field length
localHeader.push(0, 0);
// Add filename
localHeader.push(...filenameBytes);
chunks.push(localHeader);
chunks.push(contentBytes);
// Store info for central directory
centralDirectory.push({
name: file.filename,
offset: offset,
size: contentBytes.length,
compressedSize: contentBytes.length
});
offset += localHeader.length + contentBytes.length;
}
// Central directory
const centralDirectoryChunks: number[][] = [];
let centralDirectoryOffset = offset;
for (const entry of centralDirectory) {
const filenameBytes = Array.from(encoder.encode(entry.name));
const centralHeader: number[] = [];
// Central directory header signature
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
// Version made by
centralHeader.push(20, 0);
// Version needed to extract
centralHeader.push(20, 0);
// General purpose bit flag
centralHeader.push(0, 0);
// Compression method
centralHeader.push(0, 0);
// Last modified time/date
centralHeader.push(0, 0, 0, 0);
// CRC32
centralHeader.push(0, 0, 0, 0);
// Compressed size
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
// Uncompressed size
centralHeader.push(...intToLittleEndian(entry.size, 4));
// Filename length
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
// Extra field length
centralHeader.push(0, 0);
// File comment length
centralHeader.push(0, 0);
// Disk number start
centralHeader.push(0, 0);
// Internal file attributes
centralHeader.push(0, 0);
// External file attributes
centralHeader.push(0, 0, 0, 0);
// Relative offset of local header
centralHeader.push(...intToLittleEndian(entry.offset, 4));
// Add filename
centralHeader.push(...filenameBytes);
centralDirectoryChunks.push(centralHeader);
}
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
// End of central directory
const endOfCentralDirectory: number[] = [];
// End of central directory signature
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
// Number of this disk
endOfCentralDirectory.push(0, 0);
// Number of the disk with the start of the central directory
endOfCentralDirectory.push(0, 0);
// Total number of entries on this disk
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
// Total number of entries
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
// Size of the central directory
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
// Offset of start of central directory
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
// ZIP file comment length
endOfCentralDirectory.push(0, 0);
// Combine all chunks
const result: number[] = [];
chunks.forEach(chunk => result.push(...chunk));
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
result.push(...endOfCentralDirectory);
return result;
}
}
// Helper function to convert integer to little endian bytes
function intToLittleEndian(value: number, bytes: number): number[] {
const result: number[] = [];
for (let i = 0; i < bytes; i++) {
result.push((value >> (i * 8)) & 0xff);
}
return result;
}
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const { fileIds } = body;
if (!Array.isArray(fileIds) || fileIds.length === 0) {
return new Response(
JSON.stringify({ error: 'fileIds array is required and must not be empty' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Get file contents
const files = await Promise.all(
fileIds.map(async (id) => {
const file = await FileExampleManager.getFileExample(id);
if (!file) {
throw new Error(`File with id ${id} not found`);
}
return file;
})
);
// Create ZIP
const zipCreator = new SimpleZipCreator();
files.forEach(file => {
zipCreator.addFile(file.filename, file.content);
});
const zipData = zipCreator.create();
const blob = new Blob([new Uint8Array(zipData)], { type: 'application/zip' });
// Return ZIP file
return new Response(blob, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
'Cache-Control': 'no-cache',
}
});
} catch (error) {
console.error('ZIP download error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return new Response(
JSON.stringify({ error: 'Failed to create zip file', details: errorMessage }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
};
// Also support GET for single file download
export const GET: APIRoute = async ({ url }) => {
try {
const fileId = url.searchParams.get('id');
if (!fileId) {
return new Response(
JSON.stringify({ error: 'id parameter is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
const file = await FileExampleManager.getFileExample(fileId);
if (!file) {
return new Response(
JSON.stringify({ error: 'File not found' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' }
}
);
}
const encoder = new TextEncoder();
const content = encoder.encode(file.content);
const blob = new Blob([content], { type: getMimeType(file.language) });
return new Response(blob, {
headers: {
'Content-Type': getMimeType(file.language),
'Content-Disposition': `attachment; filename="${file.filename}"`,
'Cache-Control': 'no-cache',
}
});
} catch (error) {
console.error('File download error:', error);
return new Response(
JSON.stringify({ error: 'Failed to download file' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
};
// Helper function to get MIME type
function getMimeType(language: string): string {
const mimeTypes: Record<string, string> = {
'python': 'text/x-python',
'typescript': 'text/x-typescript',
'javascript': 'text/javascript',
'dockerfile': 'text/x-dockerfile',
'yaml': 'text/yaml',
'json': 'application/json',
'html': 'text/html',
'css': 'text/css',
'sql': 'text/x-sql',
'bash': 'text/x-shellscript',
'text': 'text/plain'
};
return mimeTypes[language] || 'text/plain';
}

View File

@@ -6,6 +6,7 @@ import { CodeBlock, InlineCode } from '../../components/ArticleBlockquote';
import { H2, H3 } from '../../components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../components/ArticleParagraph';
import { UL, LI } from '../../components/ArticleList';
import FileExamplesList from '../../components/FileExamplesList.astro';
export async function getStaticPaths() {
return blogPosts.map(post => ({
@@ -28,12 +29,14 @@ const readingTime = Math.max(1, Math.ceil(wordCount / 200));
// Generate unique clap key for this post
const clapKey = `claps_${post.slug}`;
// Determine if this post should show file examples
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
---
<BaseLayout title={post.title} description={post.description}>
<!-- Reading progress bar with gradient -->
<div id="reading-progress" class="fixed top-0 left-0 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 z-50 transition-all duration-100" style="width: 0%"></div>
<!-- Top navigation bar with back button and clap counter -->
<nav class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-all duration-300" id="top-nav">
<div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
@@ -170,10 +173,98 @@ const clapKey = `claps_${post.slug}`;
print(f"Operation result: {result}")
return result`}
</CodeBlock>
<H2>Complete examples</H2>
<Paragraph>
Here are some practical file examples you can copy and download. These include proper error handling and logging.
</Paragraph>
<!-- File examples section -->
<div class="my-8">
<FileExamplesList postSlug="debugging-tips" groupId="python-data-processing" />
</div>
</>
)}
{post.slug === 'architecture-patterns' && (
<>
<LeadParagraph>
Good software architecture is about making the right decisions early. Here are some patterns I've found useful in production systems.
</LeadParagraph>
<H2>Repository Pattern</H2>
<Paragraph>
The repository pattern provides a clean separation between your business logic and data access layer. It makes your code more testable and maintainable.
</Paragraph>
<H2>Service Layer</H2>
<Paragraph>
Services orchestrate business logic and coordinate between repositories and domain events. They keep your controllers thin and your business rules organized.
</Paragraph>
<H2>Domain Events</H2>
<Paragraph>
Domain events help you decouple components and react to changes in your system. They're essential for building scalable, event-driven architectures.
</Paragraph>
<H2>Complete examples</H2>
<Paragraph>
These TypeScript examples demonstrate modern architecture patterns for scalable applications. You can copy them directly into your project.
</Paragraph>
<!-- File examples section -->
<div class="my-8">
<FileExamplesList postSlug="architecture-patterns" />
</div>
</>
)}
{post.slug === 'docker-deployment' && (
<>
<LeadParagraph>
Docker has become the standard for containerizing applications. Here's how to set up production-ready deployments that are secure, efficient, and maintainable.
</LeadParagraph>
<H2>Multi-stage builds</H2>
<Paragraph>
Multi-stage builds keep your production images small and secure by separating build and runtime environments. This reduces attack surface and speeds up deployments.
</Paragraph>
<H2>Health checks and monitoring</H2>
<Paragraph>
Proper health checks ensure your containers are running correctly. Combined with restart policies, this gives you resilient, self-healing deployments.
</Paragraph>
<H2>Orchestration with Docker Compose</H2>
<Paragraph>
Docker Compose makes it easy to manage multi-service applications in development and production. Define services, networks, and volumes in a single file.
</Paragraph>
<H2>Complete examples</H2>
<Paragraph>
These Docker configurations are production-ready. Use them as a starting point for your own deployments.
</Paragraph>
<!-- File examples section -->
<div class="my-8">
<FileExamplesList postSlug="docker-deployment" />
</div>
</>
)}
</div>
<!-- File examples for architecture posts -->
{showFileExamples && post.slug !== 'debugging-tips' && (
<div class="prose prose-slate max-w-none mt-12">
<H2>Code Examples</H2>
<Paragraph>
Below you'll find complete file examples related to this topic. You can copy individual files or download them all as a zip.
</Paragraph>
<div class="my-6">
<FileExamplesList postSlug={post.slug} tags={post.tags} />
</div>
</div>
)}
<!-- Footer with elegant back button -->
<div class="mt-16 pt-8 border-t border-slate-200 text-center">
<button
@@ -189,17 +280,6 @@ const clapKey = `claps_${post.slug}`;
</section>
</main>
<!-- Back to top floating button -->
<button
id="back-to-top"
class="fixed bottom-6 right-6 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-full p-4 shadow-lg hover:from-blue-700 hover:to-purple-700 hover:shadow-xl hover:scale-110 active:scale-95 transition-all duration-300 opacity-0 pointer-events-none z-40"
aria-label="Back to top"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
<script>
// Reading progress bar with smooth gradient
function updateReadingProgress() {
@@ -713,4 +793,4 @@ const clapKey = `claps_${post.slug}`;
transition: transform 0.2s ease, opacity 0.2s ease;
}
</style>
</BaseLayout>
</BaseLayout>

View File

@@ -18,22 +18,21 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
<!-- Everything on ONE minimalist page -->
<!-- Clean Hero Section -->
<section class="py-16 md:py-20">
<section class="pt-10 pb-8 md:pt-12 md:pb-10">
<div class="max-w-3xl mx-auto px-6">
<!-- Personal intro -->
<div class="text-center mb-10 animate-fade-in">
<h1 class="text-4xl md:text-5xl font-serif font-light text-slate-900 tracking-tight mb-4">
<div class="text-center animate-fade-in">
<h1 class="text-3xl md:text-4xl font-serif font-light text-slate-900 tracking-tight mb-3">
Marc Mintel
</h1>
<p class="text-lg md:text-xl text-slate-600 leading-relaxed font-serif italic">
<p class="text-base md:text-lg text-slate-600 leading-relaxed font-serif italic">
"A public notebook of things I figured out, mistakes I made, and tools I tested."
</p>
<div class="flex items-center justify-center gap-4 text-sm text-slate-500 font-sans mt-4">
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 12a4 4 0 110-8 4 4 0 010 8z"/></svg>
<div class="flex items-center justify-center gap-3 text-[13px] text-slate-500 font-sans mt-3">
<span class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 12a4 4 0 110-8 4 4 0 010 8z"/></svg>
Vulkaneifel, Germany
</span>
<span>•</span>
<span aria-hidden="true">•</span>
<span>Digital problem solver</span>
</div>
</div>
@@ -68,7 +67,7 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
<!-- All Posts -->
<section>
<div id="posts-container" class="grid-responsive">
<div id="posts-container" class="not-prose grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
{allPosts.length === 0 ? (
<div class="empty-state">
<p>No posts yet. Check back soon!</p>
@@ -210,20 +209,7 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
}, 400);
});
// Hover micro-interaction
link.addEventListener('mouseenter', () => {
const card = link.querySelector('.post-card') as HTMLElement;
if (card) {
card.style.transform = 'translateY(-2px)';
}
});
link.addEventListener('mouseleave', () => {
const card = link.querySelector('.post-card') as HTMLElement;
if (card) {
card.style.transform = 'translateY(0)';
}
});
// Hover handled by CSS
});
// Handle incoming transition (when coming back from post)
@@ -264,7 +250,7 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
});
</script>
<style>
<style is:global>
/* Post link wrapper for smooth transitions */
.post-link {
display: block;
@@ -273,16 +259,18 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
}
.post-link .post-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}
.post-link:hover .post-card {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
border-color: rgba(148, 163, 184, 0.9);
}
.post-link:active .post-card {
transform: translateY(0) scale(0.98);
transform: translateY(0) scale(0.99);
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
}
/* Transition overlay */
@@ -295,8 +283,12 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
transform-origin: center top;
}
/* Smooth animations for all elements */
* {
/* Smooth animations for interactive elements only */
a,
button,
input,
.post-link,
.highlighter-tag {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
@@ -352,7 +344,17 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
.tag-cloud a:hover {
transform: translateY(-1px) rotate(-1deg);
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
box-shadow: none;
}
/* Prevent hover transforms from affecting layout/neighbor borders */
.tag-cloud {
overflow: visible;
}
.tag-cloud a {
display: inline-block;
padding: 2px;
}
/* Smooth scroll behavior */
@@ -395,4 +397,4 @@ const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
}
}
</style>
</BaseLayout>
</BaseLayout>

View File

@@ -2,7 +2,7 @@
@tailwind components;
@tailwind utilities;
/* Medium-inspired clean reading experience */
/* Base styles - Tailwind only */
@layer base {
html {
scroll-behavior: smooth;
@@ -20,35 +20,35 @@
}
h1 {
@apply text-3xl md:text-4xl leading-tight mb-8;
@apply text-3xl md:text-4xl leading-tight mb-6;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h2 {
@apply text-2xl md:text-3xl leading-tight mb-6 mt-12;
@apply text-2xl md:text-3xl leading-tight mb-4 mt-8;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h3 {
@apply text-xl md:text-2xl leading-tight mb-4 mt-8;
@apply text-xl md:text-2xl leading-tight mb-3 mt-6;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h4 {
@apply text-lg md:text-xl leading-tight mb-3 mt-6;
@apply text-lg md:text-xl leading-tight mb-2 mt-4;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
p {
@apply mb-6 text-base leading-relaxed text-slate-700;
@apply mb-4 text-base leading-relaxed text-slate-700;
}
.lead {
@apply text-xl md:text-2xl text-slate-600 mb-10 leading-relaxed;
@apply text-xl md:text-2xl text-slate-600 mb-6 leading-relaxed;
font-weight: 400;
}
@@ -57,168 +57,94 @@
}
ul, ol {
@apply ml-6 mb-6;
@apply ml-5 mb-4;
}
li {
@apply mb-2;
@apply mb-1;
}
code {
@apply bg-slate-100 px-1.5 py-0.5 rounded font-mono text-sm text-slate-700;
@apply bg-slate-100 px-1 py-0.5 rounded font-mono text-sm text-slate-700;
}
blockquote {
@apply border-l-4 border-slate-300 pl-6 italic text-slate-600 my-6;
@apply border-l-2 border-slate-300 pl-4 italic text-slate-600 my-4;
}
/* Code formatting */
code {
@apply bg-slate-100 px-1.5 py-0.5 rounded font-mono text-sm text-slate-700;
}
/* Inline code */
:not(pre) > code {
@apply text-pink-600 bg-pink-50 border border-pink-200;
}
/* Code blocks */
pre {
@apply bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto my-6 border border-slate-700;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.6;
}
pre code {
@apply bg-transparent text-slate-100 px-0 py-0 border-0;
font-family: inherit;
}
/* Syntax highlighting colors */
.token.comment { @apply text-slate-500 italic; }
.token.keyword { @apply text-purple-400 font-semibold; }
.token.string { @apply text-green-400; }
.token.number { @apply text-orange-400; }
.token.function { @apply text-blue-400; }
.token.operator { @apply text-slate-300; }
.token.punctuation { @apply text-slate-400; }
.token.class-name { @apply text-yellow-400 font-semibold; }
/* Line numbers wrapper */
.line-numbers {
counter-reset: line;
}
.line-numbers .line {
counter-increment: line;
position: relative;
padding-left: 2.5rem;
}
.line-numbers .line::before {
content: counter(line);
position: absolute;
left: 0;
width: 2rem;
text-align: right;
color: #64748b;
user-select: none;
}
/* Focus styles - Firefox compatible */
/* Focus states */
a:focus,
button:focus,
[tabindex]:focus,
.post-link:focus,
.highlighter-tag:focus,
.clap-button-top:focus,
.share-button-top:focus,
#back-btn-top:focus,
#back-btn-bottom:focus,
#back-to-top:focus,
input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
/* Remove default outline in favor of custom styles */
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
a:focus,
button:focus,
input:focus {
outline: 3px solid #000;
outline-offset: 2px;
}
}
}
/* Medium-inspired components */
/* Components - Tailwind utility classes */
@layer components {
.container {
@apply max-w-4xl mx-auto px-6 py-10;
}
.wide-container {
@apply max-w-5xl mx-auto px-6 py-12;
}
.narrow-container {
@apply max-w-2xl mx-auto px-6 py-8;
}
/* Header - removed for single page design */
/* Blog post card - refined with better spacing */
.post-card {
@apply mb-12 last:mb-0;
/* Legacy hooks required by tests */
.file-example {
@apply w-full;
}
.post-card h3 {
@apply text-xl font-semibold mb-3 hover:text-blue-600 transition-colors cursor-pointer;
font-weight: 600;
.container {
@apply max-w-4xl mx-auto px-6 py-10;
}
.wide-container {
@apply max-w-5xl mx-auto px-6 py-12;
}
.narrow-container {
@apply max-w-2xl mx-auto px-6 py-8;
}
.highlighter-tag {
@apply inline-block text-xs font-bold px-2 py-0.5 rounded cursor-pointer transition-all duration-200;
position: relative;
transform: rotate(-1deg);
box-shadow: 2px 2px 0 rgba(0,0,0,0.1);
}
.search-box {
@apply w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-blue-400 transition-colors;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
}
.search-box::placeholder {
@apply text-slate-400;
}
/* Blog post card */
.post-card {
@apply mb-8 last:mb-0;
}
.post-meta {
@apply text-sm text-slate-500 font-sans mb-4;
@apply text-xs text-slate-500 font-sans mb-2;
}
.post-excerpt {
@apply text-slate-700 mb-5 leading-relaxed;
@apply text-slate-700 mb-2 leading-relaxed;
}
.post-tags {
@apply flex flex-wrap gap-2;
@apply flex flex-wrap gap-1;
}
.tag {
@apply text-xs font-sans bg-slate-100 text-slate-700 px-2 py-1 rounded hover:bg-slate-200 transition-colors cursor-pointer;
}
.read-more {
@apply text-sm font-semibold text-blue-600 hover:text-blue-800 font-sans inline-flex items-center;
}
/* Article page */
.article-header {
@apply mb-10;
@apply mb-8;
}
.article-title {
@apply text-4xl md:text-5xl font-bold mb-4;
@apply text-4xl md:text-5xl font-bold mb-3;
}
.article-meta {
@apply text-sm text-slate-500 font-sans mb-6;
@apply text-sm text-slate-500 font-sans mb-5;
}
.article-content {
@@ -226,250 +152,63 @@
}
.article-content p {
@apply mb-7;
@apply mb-5;
}
.article-content h2 {
@apply text-2xl font-bold mt-10 mb-4;
@apply text-2xl font-bold mt-8 mb-3;
}
.article-content h3 {
@apply text-xl font-bold mt-8 mb-3;
@apply text-xl font-bold mt-6 mb-2;
}
.article-content ul,
.article-content ol {
@apply ml-6 mb-7;
@apply ml-6 mb-5;
}
.article-content li {
@apply mb-2;
@apply mb-1;
}
.article-content blockquote {
@apply border-l-4 border-slate-400 pl-6 italic text-slate-600 my-8 text-xl;
@apply border-l-2 border-slate-400 pl-4 italic text-slate-600 my-5 text-lg;
}
/* Enhanced code blocks for articles */
.article-content pre {
@apply bg-slate-900 text-slate-100 p-5 rounded-lg overflow-x-auto my-6 border border-slate-700;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.6;
position: relative;
}
.article-content pre code {
@apply bg-transparent px-0 py-0;
font-family: inherit;
white-space: pre;
display: block;
}
/* Line numbers for code blocks */
.article-content pre.line-numbers {
padding-left: 3.5rem;
}
.article-content pre.line-numbers::before {
content: attr(data-line-numbers);
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2.5rem;
background: #1e293b;
color: #64748b;
text-align: right;
padding: 1.25rem 0.5rem 1.25rem 0;
font-family: inherit;
font-size: inherit;
line-height: 1.6;
border-right: 1px solid #334155;
user-select: none;
overflow: hidden;
}
/* Inline code in articles */
.article-content p code,
.article-content li code,
.article-content blockquote code {
@apply bg-slate-100 text-pink-600 px-1.5 py-0.5 rounded font-mono text-sm border border-pink-200;
}
/* Syntax highlighting classes */
.article-content .token.comment { @apply text-slate-500 italic; }
.article-content .token.keyword { @apply text-purple-400 font-semibold; }
.article-content .token.string { @apply text-green-400; }
.article-content .token.number { @apply text-orange-400; }
.article-content .token.function { @apply text-blue-400; }
.article-content .token.operator { @apply text-slate-300; }
.article-content .token.punctuation { @apply text-slate-400; }
.article-content .token.class-name { @apply text-yellow-400 font-semibold; }
.article-content .token.tag { @apply text-red-400; }
.article-content .token.attr-name { @apply text-purple-400; }
.article-content .token.attr-value { @apply text-green-400; }
/* Code language badge */
.article-content pre::after {
content: attr(data-language);
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #334155;
color: #e2e8f0;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: 'Inter', sans-serif;
}
/* About page sections */
.about-section {
@apply mb-10 pb-8 border-b border-slate-200 last:border-0;
}
.about-section h2 {
@apply text-2xl font-bold mb-4;
}
.about-list {
@apply space-y-2;
}
.about-list li {
@apply flex items-start;
}
.about-list li::before {
content: "→";
@apply mr-2 text-blue-600 font-bold;
}
/* Footer - removed for single page design */
/* Simple button */
/* Buttons */
.btn {
@apply inline-block px-5 py-2 bg-slate-900 text-white font-sans font-medium hover:bg-slate-700 transition-colors no-underline rounded;
@apply inline-block px-4 py-2 bg-slate-900 text-white font-sans font-medium hover:bg-slate-700 transition-colors rounded;
}
/* Highlighter-style tags - lovely note-taking style */
.highlighter-tag {
@apply inline-block text-xs font-bold px-2 py-0.5 rounded cursor-pointer transition-all duration-200;
position: relative;
transform: rotate(-1deg);
box-shadow: 2px 2px 0 rgba(0,0,0,0.1);
.btn-primary {
@apply bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded transition-colors;
}
.highlighter-yellow {
@apply bg-yellow-300 text-yellow-900;
background: linear-gradient(180deg, rgba(255,235,59,0.9) 0%, rgba(255,213,79,0.9) 100%);
.btn-secondary {
@apply bg-white text-slate-700 hover:bg-slate-100 border border-slate-300 px-3 py-1.5 rounded transition-colors;
}
.highlighter-yellow:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
/* Hide scrollbars */
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.highlighter-pink {
@apply bg-pink-300 text-pink-900;
background: linear-gradient(180deg, rgba(255,167,209,0.9) 0%, rgba(255,122,175,0.9) 100%);
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.highlighter-pink:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
.highlighter-green {
@apply bg-green-300 text-green-900;
background: linear-gradient(180deg, rgba(129,199,132,0.9) 0%, rgba(102,187,106,0.9) 100%);
}
.highlighter-green:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
.highlighter-blue {
@apply bg-blue-300 text-blue-900;
background: linear-gradient(180deg, rgba(100,181,246,0.9) 0%, rgba(66,165,245,0.9) 100%);
}
.highlighter-blue:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
/* Scribble underline for emphasis */
.scribble-emphasis {
position: relative;
display: inline-block;
font-weight: 600;
}
.scribble-emphasis::after {
content: '';
position: absolute;
bottom: -2px;
left: -1px;
right: -1px;
height: 7px;
background: url("data:image/svg+xml,%3Csvg width='40' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 4 Q10 2, 20 4 T38 4' stroke='%23FFEB3B' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") repeat-x;
background-size: 40px 8px;
opacity: 0.7;
z-index: -1;
}
/* Sticky note effect */
.sticky-note {
@apply p-4 rounded shadow-sm border;
background: linear-gradient(135deg, #fff9c4 0%, #fff59d 100%);
border-color: #f9a825;
transform: rotate(-0.5deg);
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
}
.sticky-note:hover {
transform: rotate(0deg) scale(1.02);
box-shadow: 3px 3px 8px rgba(0,0,0,0.15);
}
/* Handwritten style */
.handwritten {
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', cursive;
font-weight: 500;
letter-spacing: -0.3px;
}
/* Search box styling */
.search-box {
@apply w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-blue-400 transition-colors;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
}
.search-box::placeholder {
@apply text-slate-400;
}
/* Tag cloud */
.tag-cloud {
@apply flex flex-wrap gap-2 items-center;
}
/* Empty state */
.empty-state {
@apply text-center py-12 text-slate-500;
@apply text-center py-8 text-slate-500;
}
.empty-state svg {
@apply mx-auto mb-4 text-slate-300;
@apply mx-auto mb-2 text-slate-300;
}
/* Line clamp utility for text truncation */
/* Line clamp utility */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
@@ -477,103 +216,71 @@
overflow: hidden;
}
/* Subtle gradient backgrounds */
.subtle-gradient {
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(248,250,252,0.1) 100%);
}
/* Enhanced focus states */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white;
}
/* Reading progress indicator */
.reading-progress {
.reading-progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
transform-origin: left;
z-index: 50;
}
/* Smooth animations */
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Enhanced typography for better readability */
.text-balance {
text-wrap: balance;
}
/* Subtle shadow variations */
.shadow-soft {
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.08);
}
.shadow-hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.12);
}
/* Improved button styles */
.btn-primary {
@apply bg-blue-600 text-white font-sans font-medium px-6 py-3 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200;
}
.btn-secondary {
@apply bg-white text-slate-700 font-sans font-medium px-6 py-3 rounded-lg border border-slate-200 hover:bg-slate-50 focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 transition-all duration-200;
}
/* Status indicators */
.status-indicator {
@apply inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium;
}
.status-published {
@apply bg-green-100 text-green-800;
}
.status-draft {
@apply bg-yellow-100 text-yellow-800;
}
/* Responsive grid improvements */
.grid-responsive {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Better spacing utilities */
.space-y-fluid > * + * {
margin-top: clamp(1rem, 2vw, 2rem);
}
/* Enhanced link styles */
.link-enhanced {
@apply text-blue-600 hover:text-blue-800 transition-colors duration-200 relative;
}
.link-enhanced::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transition: width 0.3s ease;
background: #3b82f6;
transform-origin: left;
transform: scaleX(0);
z-index: 50;
transition: transform 0.1s ease-out;
}
.link-enhanced:hover::after {
width: 100%;
/* Floating back to top button */
.floating-back-to-top {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
width: 2.5rem;
height: 2.5rem;
background: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
transition: all 0.15s ease;
opacity: 0;
transform: translateY(8px);
}
.floating-back-to-top.visible {
opacity: 1;
transform: translateY(0);
}
.floating-back-to-top:hover {
background: #f8fafc;
color: #1e293b;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print styles */
@media print {
.floating-back-to-top,
.reading-progress-bar {
display: none !important;
}
}
}

View File

@@ -0,0 +1,261 @@
/**
* Test the integration between blog posts and file examples
* This simulates what happens when a blog post is rendered
*/
import { blogPosts } from '../data/blogPosts';
import { FileExampleManager } from '../data/fileExamples';
export async function testBlogPostIntegration() {
console.log('🧪 Testing Blog Post + File Examples Integration...\n');
let passed = 0;
let failed = 0;
const test = (name: string, fn: () => void | Promise<void>) => {
try {
const result = fn();
if (result instanceof Promise) {
return result
.then(() => {
console.log(`${name}`);
passed++;
})
.catch((err) => {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
});
} else {
console.log(`${name}`);
passed++;
}
} catch (err: any) {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
}
};
// Test 1: Blog posts exist
test('Blog posts are loaded', () => {
if (!blogPosts || blogPosts.length === 0) {
throw new Error('No blog posts found');
}
console.log(` Found ${blogPosts.length} posts`);
});
// Test 2: Each post has required fields
test('All posts have required fields', () => {
for (const post of blogPosts) {
if (!post.slug || !post.title || !post.tags) {
throw new Error(`Post ${post.slug} missing required fields`);
}
}
});
// Test 3: Debugging-tips post should have file examples
test('debugging-tips post has file examples', async () => {
const post = blogPosts.find(p => p.slug === 'debugging-tips');
if (!post) {
throw new Error('debugging-tips post not found');
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
// debugging-tips has tags ['debugging', 'tools'] so showFileExamples would be false
// But it has hardcoded FileExamplesList in the template
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups.flatMap(g => g.files).filter(f => f.postSlug === 'debugging-tips');
if (filesForPost.length === 0) {
throw new Error('No files found for debugging-tips');
}
console.log(` Found ${filesForPost.length} files for debugging-tips`);
});
// Test 4: Architecture-patterns post should have file examples
test('architecture-patterns post has file examples', async () => {
const post = blogPosts.find(p => p.slug === 'architecture-patterns');
if (!post) {
throw new Error('architecture-patterns post not found');
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
if (!showFileExamples) {
throw new Error('architecture-patterns should show file examples');
}
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups.flatMap(g => g.files).filter(f => f.postSlug === 'architecture-patterns');
if (filesForPost.length === 0) {
throw new Error('No files found for architecture-patterns');
}
console.log(` Found ${filesForPost.length} files for architecture-patterns`);
});
// Test 5: Docker-deployment post should have file examples
test('docker-deployment post has file examples', async () => {
const post = blogPosts.find(p => p.slug === 'docker-deployment');
if (!post) {
throw new Error('docker-deployment post not found');
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
if (!showFileExamples) {
throw new Error('docker-deployment should show file examples');
}
// Verify files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups.flatMap(g => g.files).filter(f => f.postSlug === 'docker-deployment');
if (filesForPost.length === 0) {
throw new Error('No files found for docker-deployment');
}
console.log(` Found ${filesForPost.length} files for docker-deployment`);
});
// Test 6: First-note post should NOT have file examples
test('first-note post has no file examples', async () => {
const post = blogPosts.find(p => p.slug === 'first-note');
if (!post) {
throw new Error('first-note post not found');
}
// Check if it would trigger file examples
const showFileExamples = post.tags?.some(tag =>
['architecture', 'design-patterns', 'system-design', 'docker', 'deployment'].includes(tag)
);
if (showFileExamples) {
throw new Error('first-note should NOT show file examples');
}
// Verify no files exist for this post
const groups = await FileExampleManager.getAllGroups();
const filesForPost = groups.flatMap(g => g.files).filter(f => f.postSlug === 'first-note');
if (filesForPost.length > 0) {
throw new Error('Files found for first-note, but none should exist');
}
console.log(` Correctly has no files`);
});
// Test 7: Simulate FileExamplesList filtering for debugging-tips
test('FileExamplesList filtering works for debugging-tips', async () => {
const postSlug = 'debugging-tips';
const groupId = 'python-data-processing';
const allGroups = await FileExampleManager.getAllGroups();
const loadedGroups = allGroups
.filter(g => g.groupId === groupId)
.map(g => ({
...g,
files: g.files.filter(f => f.postSlug === postSlug)
}))
.filter(g => g.files.length > 0);
if (loadedGroups.length === 0) {
throw new Error('No groups loaded for debugging-tips with python-data-processing');
}
if (loadedGroups[0].files.length === 0) {
throw new Error('No files in the group');
}
console.log(` Would show ${loadedGroups[0].files.length} files`);
});
// Test 8: Simulate FileExamplesList filtering for architecture-patterns
test('FileExamplesList filtering works for architecture-patterns', async () => {
const postSlug = 'architecture-patterns';
const tags = ['architecture', 'design-patterns', 'system-design'];
const allGroups = await FileExampleManager.getAllGroups();
const loadedGroups = allGroups
.map(g => ({
...g,
files: g.files.filter(f => {
if (f.postSlug !== postSlug) return false;
if (tags && tags.length > 0) {
return f.tags?.some(tag => tags.includes(tag));
}
return true;
})
}))
.filter(g => g.files.length > 0);
if (loadedGroups.length === 0) {
throw new Error('No groups loaded for architecture-patterns');
}
const totalFiles = loadedGroups.reduce((sum, g) => sum + g.files.length, 0);
if (totalFiles === 0) {
throw new Error('No files found');
}
console.log(` Would show ${totalFiles} files across ${loadedGroups.length} groups`);
});
// Test 9: Verify all file examples have postSlug
test('All file examples have postSlug property', async () => {
const groups = await FileExampleManager.getAllGroups();
const filesWithoutPostSlug = groups.flatMap(g => g.files).filter(f => !f.postSlug);
if (filesWithoutPostSlug.length > 0) {
throw new Error(`${filesWithoutPostSlug.length} files missing postSlug`);
}
console.log(` All ${groups.flatMap(g => g.files).length} files have postSlug`);
});
// Test 10: Verify postSlugs match blog post slugs
test('File example postSlugs match blog post slugs', async () => {
const groups = await FileExampleManager.getAllGroups();
const filePostSlugs = new Set(groups.flatMap(g => g.files).map(f => f.postSlug));
const blogPostSlugs = new Set(blogPosts.map(p => p.slug));
for (const slug of filePostSlugs) {
if (slug && !blogPostSlugs.has(slug)) {
throw new Error(`File postSlug "${slug}" doesn't match any blog post`);
}
}
console.log(` All file postSlugs match blog posts`);
});
// Wait for async tests
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`\n📊 Integration Test Results: ${passed} passed, ${failed} failed`);
if (failed === 0) {
console.log('🎉 All integration tests passed!');
console.log('\n✅ The file examples system is correctly integrated with blog posts!');
} else {
console.log('❌ Some integration tests failed');
process.exit(1);
}
}
// Export for use in other test files

View File

@@ -0,0 +1,264 @@
/**
* Comprehensive tests for the file examples system
*/
import { FileExampleManager, sampleFileExamples, type FileExample } from '../data/fileExamples';
// Test helper to run all tests
export async function runFileExamplesTests() {
console.log('🧪 Running File Examples System Tests...\n');
let passed = 0;
let failed = 0;
const test = (name: string, fn: () => void | Promise<void>) => {
try {
const result = fn();
if (result instanceof Promise) {
return result
.then(() => {
console.log(`${name}`);
passed++;
})
.catch((err) => {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
});
} else {
console.log(`${name}`);
passed++;
}
} catch (err: any) {
console.log(`${name}`);
console.error(` Error: ${err.message}`);
failed++;
}
};
// Test 1: Data structure exists
test('File examples data is loaded', () => {
if (!sampleFileExamples || sampleFileExamples.length === 0) {
throw new Error('No file examples found');
}
console.log(` Found ${sampleFileExamples.length} groups`);
});
// Test 2: FileExampleManager exists
test('FileExampleManager class is available', () => {
if (!FileExampleManager) {
throw new Error('FileExampleManager not found');
}
});
// Test 3: Sample data has correct structure
test('Sample data has correct structure', () => {
const group = sampleFileExamples[0];
if (!group.groupId || !group.title || !Array.isArray(group.files)) {
throw new Error('Invalid group structure');
}
const file = group.files[0];
if (!file.id || !file.filename || !file.content || !file.language) {
throw new Error('Invalid file structure');
}
// Check for postSlug
if (!file.postSlug) {
throw new Error('Files missing postSlug property');
}
});
// Test 4: Get all groups
test('FileExampleManager.getAllGroups() works', async () => {
const groups = await FileExampleManager.getAllGroups();
if (!Array.isArray(groups) || groups.length === 0) {
throw new Error('getAllGroups returned invalid result');
}
});
// Test 5: Get specific group
test('FileExampleManager.getGroup() works', async () => {
const group = await FileExampleManager.getGroup('python-data-processing');
if (!group) {
throw new Error('Group not found');
}
if (group.groupId !== 'python-data-processing') {
throw new Error('Wrong group returned');
}
});
// Test 6: Search files
test('FileExampleManager.searchFiles() works', async () => {
const results = await FileExampleManager.searchFiles('python');
if (!Array.isArray(results)) {
throw new Error('searchFiles returned invalid result');
}
if (results.length === 0) {
throw new Error('No results found for "python"');
}
});
// Test 7: Get file by ID
test('FileExampleManager.getFileExample() works', async () => {
const file = await FileExampleManager.getFileExample('python-data-processor');
if (!file) {
throw new Error('File not found');
}
if (file.id !== 'python-data-processor') {
throw new Error('Wrong file returned');
}
});
// Test 8: Filter by postSlug
test('Filter files by postSlug', async () => {
const groups = await FileExampleManager.getAllGroups();
const debuggingFiles = groups.flatMap(g => g.files).filter(f => f.postSlug === 'debugging-tips');
if (debuggingFiles.length === 0) {
throw new Error('No files found for debugging-tips');
}
console.log(` Found ${debuggingFiles.length} files for debugging-tips`);
});
// Test 9: Filter by postSlug and groupId
test('Filter files by postSlug and groupId', async () => {
const groups = await FileExampleManager.getAllGroups();
const filtered = groups
.filter(g => g.groupId === 'python-data-processing')
.map(g => ({
...g,
files: g.files.filter(f => f.postSlug === 'debugging-tips')
}))
.filter(g => g.files.length > 0);
if (filtered.length === 0) {
throw new Error('No files found for debugging-tips in python-data-processing');
}
console.log(` Found ${filtered[0].files.length} files`);
});
// Test 10: Filter by postSlug and tags
test('Filter files by postSlug and tags', async () => {
const groups = await FileExampleManager.getAllGroups();
const tags = ['architecture', 'design-patterns'];
const filtered = groups
.map(g => ({
...g,
files: g.files.filter(f =>
f.postSlug === 'architecture-patterns' &&
f.tags?.some(tag => tags.includes(tag))
)
}))
.filter(g => g.files.length > 0);
if (filtered.length === 0) {
throw new Error('No files found for architecture-patterns with tags');
}
console.log(` Found ${filtered[0].files.length} files`);
});
// Test 11: Download single file
test('Download single file', async () => {
const result = await FileExampleManager.downloadFile('python-data-processor');
if (!result) {
throw new Error('Download failed');
}
if (!result.filename || !result.content || !result.mimeType) {
throw new Error('Invalid download result');
}
});
// Test 12: Download multiple files
test('Download multiple files', async () => {
const files = await FileExampleManager.downloadMultiple(['python-data-processor', 'python-config-example']);
if (!Array.isArray(files) || files.length !== 2) {
throw new Error('Invalid multiple download result');
}
});
// Test 13: Get available tags
test('Get available tags', async () => {
const tags = await FileExampleManager.getAvailableTags();
if (!Array.isArray(tags) || tags.length === 0) {
throw new Error('No tags found');
}
if (!tags.includes('python') || !tags.includes('architecture')) {
throw new Error('Expected tags not found');
}
});
// Test 14: Create new file example
test('Create new file example', async () => {
const newExample = await FileExampleManager.createFileExample({
filename: 'test.py',
content: 'print("test")',
language: 'python',
description: 'Test file',
tags: ['test'],
postSlug: 'test-post'
});
if (!newExample.id) {
throw new Error('New example has no ID');
}
// Verify it was added
const retrieved = await FileExampleManager.getFileExample(newExample.id);
if (!retrieved || retrieved.filename !== 'test.py') {
throw new Error('New example not found');
}
});
// Test 15: Update file example
test('Update file example', async () => {
const updated = await FileExampleManager.updateFileExample('python-data-processor', {
description: 'Updated description'
});
if (!updated || updated.description !== 'Updated description') {
throw new Error('Update failed');
}
});
// Test 16: Delete file example
test('Delete file example', async () => {
// First create one
const created = await FileExampleManager.createFileExample({
filename: 'delete-test.py',
content: 'test',
language: 'python',
postSlug: 'test'
});
// Then delete it
const deleted = await FileExampleManager.deleteFileExample(created.id);
if (!deleted) {
throw new Error('Delete failed');
}
// Verify it's gone
const retrieved = await FileExampleManager.getFileExample(created.id);
if (retrieved) {
throw new Error('File still exists after deletion');
}
});
// Wait for all async tests to complete
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`\n📊 Test Results: ${passed} passed, ${failed} failed`);
if (failed === 0) {
console.log('🎉 All tests passed!');
} else {
console.log('❌ Some tests failed');
process.exit(1);
}
}
// Export for use in other test files