Add digest viewing support and date-fns

- Enhanced UI to detect and format AI digest posts
- Added category-based display for digest entries
- Installed date-fns for better date formatting
- Added visual distinction for AI-generated digests
This commit is contained in:
Matt Bruce 2026-02-18 09:49:07 -06:00
parent 6c7dfd8b40
commit c977828350
3 changed files with 129 additions and 24 deletions

11
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "blog-backup",
"version": "0.1.0",
"dependencies": {
"date-fns": "^4.1.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
@ -2706,6 +2707,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",

View File

@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"date-fns": "^4.1.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"

View File

@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { format } from "date-fns";
interface Message {
id: string;
@ -9,6 +10,50 @@ interface Message {
timestamp: number;
}
interface DigestEntry {
category: string;
title: string;
url?: string;
summary: string;
}
interface ParsedDigest {
title: string;
entries: DigestEntry[];
raw: string;
}
function parseDigest(content: string): ParsedDigest {
const lines = content.split('\n');
const entries: DigestEntry[] = [];
let currentCategory = "";
let title = "";
for (const line of lines) {
if (line.startsWith('# ') && !title) {
title = line.replace('# ', '');
} else if (line.startsWith('## ')) {
currentCategory = line.replace('## ', '');
} else if (line.startsWith('- **')) {
const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/);
if (match) {
entries.push({
category: currentCategory,
title: match[1],
summary: match[2],
});
}
} else if (line.startsWith(' - ')) {
const urlMatch = line.match(/\[.+?\]\((.+?)\)/);
if (urlMatch && entries.length > 0) {
entries[entries.length - 1].url = urlMatch[1];
}
}
}
return { title, entries, raw: content };
}
export default function Home() {
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
@ -17,6 +62,7 @@ export default function Home() {
return today.toISOString().split("T")[0];
});
const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<string | null>(null);
useEffect(() => {
fetchMessages();
@ -84,12 +130,15 @@ export default function Home() {
new Date(b).getTime() - new Date(a).getTime()
);
// Check if message is a digest
const isDigest = (content: string) => content.includes("## Daily Digest");
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100">
<div className="max-w-3xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-3xl font-bold mb-2">📓 Daily Blog Backup</h1>
<p className="text-zinc-400">Backup of your daily messages and thoughts</p>
<h1 className="text-3xl font-bold mb-2">📓 Daily Blog & Digest</h1>
<p className="text-zinc-400">Daily digests and notes backup</p>
</header>
{/* Add new message */}
@ -133,32 +182,76 @@ export default function Home() {
<div key={date} className="bg-zinc-900 rounded-lg border border-zinc-800 overflow-hidden">
<div className="bg-zinc-800/50 px-4 py-2 border-b border-zinc-800">
<h2 className="font-semibold text-zinc-300">
{new Date(date).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
{format(new Date(date), "EEEE, MMMM d, yyyy")}
</h2>
</div>
<div className="divide-y divide-zinc-800">
{groupedMessages[date].map((msg) => (
<div key={msg.id} className="p-4 group">
<div className="flex justify-between items-start gap-4">
<p className="text-zinc-300 whitespace-pre-wrap">{msg.content}</p>
<button
onClick={() => deleteMessage(msg.id)}
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm"
title="Delete"
>
</button>
{groupedMessages[date].map((msg) => {
const isDailyDigest = isDigest(msg.content);
const parsed = isDailyDigest ? parseDigest(msg.content) : null;
return (
<div key={msg.id} className={`p-4 group ${isDailyDigest ? 'bg-blue-900/10' : ''}`}>
{isDailyDigest && (
<div className="flex items-center gap-2 mb-3">
<span className="bg-blue-600 text-white text-xs px-2 py-1 rounded">🤖 AI Digest</span>
<span className="text-xs text-blue-400">Daily Research</span>
</div>
)}
<div className="flex justify-between items-start gap-4">
{isDailyDigest && parsed ? (
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-400 mb-2">
{parsed.title}
</h3>
{parsed.entries.length > 0 ? (
<div className="space-y-4">
{Array.from(new Set(parsed.entries.map(e => e.category))).map(cat => (
<div key={cat}>
<h4 className="text-sm font-medium text-zinc-400 mb-2">{cat}</h4>
<ul className="space-y-2">
{parsed.entries.filter(e => e.category === cat).map((entry, i) => (
<li key={i} className="text-sm text-zinc-300">
<span className="font-medium">{entry.title}:</span> {entry.summary}
{entry.url && (
<a href={entry.url} target="_blank" rel="noopener noreferrer"
className="text-blue-400 hover:underline ml-1">
[link]
</a>
)}
</li>
))}
</ul>
</div>
))}
</div>
) : (
<pre className="text-sm text-zinc-300 whitespace-pre-wrap font-sans">
{msg.content}
</pre>
)}
</div>
) : (
<p className="text-zinc-300 whitespace-pre-wrap flex-1">{msg.content}</p>
)}
<button
onClick={() => deleteMessage(msg.id)}
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm"
title="Delete"
>
</button>
</div>
<p className="text-xs text-zinc-600 mt-2">
{format(new Date(msg.timestamp), "h:mm a")}
</p>
</div>
<p className="text-xs text-zinc-600 mt-2">
{new Date(msg.timestamp).toLocaleTimeString()}
</p>
</div>
))}
);
})}
</div>
</div>
))