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:
parent
6c7dfd8b40
commit
c977828350
11
package-lock.json
generated
11
package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "blog-backup",
|
"name": "blog-backup",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
@ -2706,6 +2707,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|||||||
141
src/app/page.tsx
141
src/app/page.tsx
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
@ -9,6 +10,50 @@ interface Message {
|
|||||||
timestamp: number;
|
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() {
|
export default function Home() {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [newMessage, setNewMessage] = useState("");
|
const [newMessage, setNewMessage] = useState("");
|
||||||
@ -17,6 +62,7 @@ export default function Home() {
|
|||||||
return today.toISOString().split("T")[0];
|
return today.toISOString().split("T")[0];
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMessages();
|
fetchMessages();
|
||||||
@ -84,12 +130,15 @@ export default function Home() {
|
|||||||
new Date(b).getTime() - new Date(a).getTime()
|
new Date(b).getTime() - new Date(a).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if message is a digest
|
||||||
|
const isDigest = (content: string) => content.includes("## Daily Digest");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-950 text-zinc-100">
|
<div className="min-h-screen bg-zinc-950 text-zinc-100">
|
||||||
<div className="max-w-3xl mx-auto px-4 py-8">
|
<div className="max-w-3xl mx-auto px-4 py-8">
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<h1 className="text-3xl font-bold mb-2">📓 Daily Blog Backup</h1>
|
<h1 className="text-3xl font-bold mb-2">📓 Daily Blog & Digest</h1>
|
||||||
<p className="text-zinc-400">Backup of your daily messages and thoughts</p>
|
<p className="text-zinc-400">Daily digests and notes backup</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Add new message */}
|
{/* 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 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">
|
<div className="bg-zinc-800/50 px-4 py-2 border-b border-zinc-800">
|
||||||
<h2 className="font-semibold text-zinc-300">
|
<h2 className="font-semibold text-zinc-300">
|
||||||
{new Date(date).toLocaleDateString("en-US", {
|
{format(new Date(date), "EEEE, MMMM d, yyyy")}
|
||||||
weekday: "long",
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-zinc-800">
|
<div className="divide-y divide-zinc-800">
|
||||||
{groupedMessages[date].map((msg) => (
|
{groupedMessages[date].map((msg) => {
|
||||||
<div key={msg.id} className="p-4 group">
|
const isDailyDigest = isDigest(msg.content);
|
||||||
<div className="flex justify-between items-start gap-4">
|
const parsed = isDailyDigest ? parseDigest(msg.content) : null;
|
||||||
<p className="text-zinc-300 whitespace-pre-wrap">{msg.content}</p>
|
|
||||||
<button
|
return (
|
||||||
onClick={() => deleteMessage(msg.id)}
|
<div key={msg.id} className={`p-4 group ${isDailyDigest ? 'bg-blue-900/10' : ''}`}>
|
||||||
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm"
|
{isDailyDigest && (
|
||||||
title="Delete"
|
<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>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<p className="text-xs text-zinc-600 mt-2">
|
);
|
||||||
{new Date(msg.timestamp).toLocaleTimeString()}
|
})}
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user