Add markdown rendering with react-markdown
- Installed react-markdown and remark-gfm for GitHub-flavored markdown - Installed @tailwindcss/typography for proper prose styling - Rewrote page.tsx to render markdown content as HTML - Links are now clickable with proper hover states - Headers, lists, and other markdown elements properly styled - Fixes Task #12: Blog now renders markdown correctly
This commit is contained in:
parent
664d48abb5
commit
b618772883
@ -1,4 +1,16 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"id": "1771511887581",
|
||||||
|
"date": "2025-02-19",
|
||||||
|
"content": "# Daily Digest - February 19, 2025 (Retro Edition)\n\n## 🤖 iOS + AI Development\n- **[Apple Releases iOS 18.3 with Enhanced AI Features](https://developer.apple.com/news)** — On-device Siri improvements and CoreML optimizations\n- **[Swift 6 Enters Beta](https://swift.org/blog)** — Major concurrency improvements for iOS developers\n- **[Vision Pro Launches with 600+ Apps](https://apple.com/vision-pro)** — Spatial computing officially arrives\n\n## 🧑💻 AI Coding Assistants\n- **[Claude 3 Announced by Anthropic](https://anthropic.com/news)** — Claude 3 Opus, Sonnet, and Haiku models launched\n- **[GitHub Copilot Gets GPT-4 Turbo](https://github.blog)** — Faster, more accurate code suggestions\n- **[Cursor IDE Gains Traction](https://cursor.com)** — AI-native editor starting to challenge VS Code\n\n## 🏆 Latest Coding Models\n- **[GPT-4 Turbo with Vision Released](https://openai.com/blog)** — Multimodal capabilities for developers\n- **[Gemini 1.5 Pro Debuts](https://deepmind.google)** — 1 million token context window\n- **[Mistral Large Announced](https://mistral.ai)** — European LLM challenging GPT-4\n\n## 🦾 OpenClaw Updates\n- **[OpenClaw Beta Launched](https://github.com/openclaw/openclaw)** — Early access for AI agent framework\n- **[Terminal UI Tools Added](https://docs.openclaw.ai)** — Command-line interface improvements\n\n## 💰 Digital Entrepreneurship\n- **[Indie Hackers $100K Club Growing](https://indiehackers.com)** — More solo founders hitting six figures\n- **[AI App Revenue Surges](https://sensor-tower.com)** — AI-powered apps dominating App Store charts\n- **[No-Code Tools Evolution](https://webflow.com/blog)** — Webflow, Bubble adding AI features\n\n---\n*Retro Digest: Looking back at February 2025 | Generated by Max*",
|
||||||
|
"timestamp": 1771511887581
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1771506266870",
|
||||||
|
"date": "2026-02-19",
|
||||||
|
"content": "# Daily Digest - February 19, 2026\n\n## iOS AI Development\n\n- [iOS 26.4 Beta Released - Get Ready with Latest SDKs](https://developer.apple.com/news/?id=xgkk9w83)\n- [Swift Student Challenge 2026 Submissions Now Open](https://developer.apple.com/swift-student-challenge/)\n- [Exploring LLMs with MLX on Apple Silicon Macs](https://machinelearning.apple.com/research/exploring-llms-mlx-m5)\n- [Updated App Review Guidelines - Anonymous Chat Apps](https://developer.apple.com/news/?id=d75yllv4)\n\n## AI Coding Assistants\n\n- [Cursor Composer 1.5 - Improved Reasoning with 20x RL Scaling](https://cursor.com/blog/composer-1-5)\n- [Stripe Rolls Out Cursor to 3,000 Engineers](https://cursor.com/blog/stripe)\n- [Cursor Launches Plugin Marketplace](https://cursor.com/blog/marketplace)\n- [Cursor Long-Running Agents Now in Web App](https://cursor.com/blog/long-running-agents)\n- [Box Chooses Cursor - 85% of Engineers Use Daily](https://cursor.com/blog/box)\n- [NVIDIA Commits 3x More Code with Cursor Across 30,000 Developers](https://cursor.com/blog/nvidia)\n- [Dropbox Uses Cursor to Index 550,000+ Files](https://cursor.com/blog/dropbox)\n- [Clankers with Claws - DHH on OpenClaw and Terminal UIs](https://world.hey.com/dhh/clankers-with-claws-9f86fa71)\n\n## Latest Coding Models\n\n- [Anthropic Claude Opus 4.6 Released - Industry-Leading Agentic Coding](https://www.anthropic.com/news)\n- [Anthropic Raises $30B Series G at $380B Valuation](https://www.anthropic.com/news)\n- [SWE-bench February 2026 Leaderboard Update](https://www.swebench.com/)\n- [Don't Trust the Salt: AI Summarization and LLM Guardrails](https://royapakzad.substack.com/p/multilingual-llm-evaluation-to-guardrails)\n\n## OpenClaw Updates\n\n- [OpenClaw Documentation - Full Tool Reference](https://docs.openclaw.ai/)\n- [Clankers with Claws - DHH on OpenClaw AI Agents](https://world.hey.com/dhh/clankers-with-claws-9f86fa71)\n- [Omarchy and OpenCode Coming to New York - Omacon April 10](https://world.hey.com/dhh/omacon-comes-to-new-york-e6ee93cb)\n\n## Digital Entrepreneurship / Indie Hacking\n\n- [Bootstrapping a $20k/mo AI Portfolio After VC-Backed Company Failed](https://www.indiehackers.com/post/tech/bootstrapping-a-20k-mo-ai-portfolio-after-his-vc-backed-company-failed-rQxwZBD9xWVgfHhIxvbJ)\n- [Vibe is Product Logic - Injecting Branding into Your AI](https://www.indiehackers.com/post/vibe-is-product-logic-how-to-inject-branding-into-your-ai-e9c6766a2d)\n- [Indie Hackers Truth: Distribution is the Bottleneck](https://www.indiehackers.com/product/leadsynthai)\n- [Copylio - AI Tool for SEO Ecommerce Product Descriptions](https://www.indiehackers.com/post/show-ih-copylio-an-ai-tool-to-generate-seo-optimized-ecommerce-product-descriptions-from-a-product-link-c5cd295d14)\n- [Most Founders Have a Timing Problem, Not a Product Problem](https://www.indiehackers.com/product/leadsynthai)\n\n---\n*Generated by OpenClaw - February 19, 2026*\n",
|
||||||
|
"timestamp": 1771506266870
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "1771435073243",
|
"id": "1771435073243",
|
||||||
"date": "2026-02-18",
|
"date": "2026-02-18",
|
||||||
|
|||||||
1518
package-lock.json
generated
1518
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,10 +9,13 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"date-fns": "^4.1.0",
|
"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",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "@tailwindcss/typography";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
|
|||||||
154
src/app/page.tsx
154
src/app/page.tsx
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
@ -10,65 +12,6 @@ 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('- **')) {
|
|
||||||
// Match: - **[Title with optional link](url)**: Summary
|
|
||||||
// or: - **Title**: Summary
|
|
||||||
const match = line.match(/- \*\*\[?(.+?)\]?\*\*:\s*(.+)/);
|
|
||||||
if (match) {
|
|
||||||
let entryTitle = match[1];
|
|
||||||
let entrySummary = match[2];
|
|
||||||
let entryUrl: string | undefined;
|
|
||||||
|
|
||||||
// Check if title contains a markdown link: [Title](url)
|
|
||||||
const linkMatch = entryTitle.match(/\[(.+?)\]\((.+?)\)/);
|
|
||||||
if (linkMatch) {
|
|
||||||
entryTitle = linkMatch[1]; // Just the link text
|
|
||||||
entryUrl = linkMatch[2]; // The URL
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
category: currentCategory,
|
|
||||||
title: entryTitle,
|
|
||||||
summary: entrySummary,
|
|
||||||
url: entryUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (line.startsWith(' - ')) {
|
|
||||||
// Fallback: look for URLs on indented lines
|
|
||||||
const urlMatch = line.match(/\[.+?\]\((.+?)\)/);
|
|
||||||
if (urlMatch && entries.length > 0 && !entries[entries.length - 1].url) {
|
|
||||||
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("");
|
||||||
@ -77,7 +20,6 @@ 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();
|
||||||
@ -146,7 +88,7 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if message is a digest
|
// Check if message is a digest
|
||||||
const isDigest = (content: string) => content.includes("## Daily Digest");
|
const isDigest = (content: string) => content.includes("##");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-950 text-zinc-100">
|
<div className="min-h-screen bg-zinc-950 text-zinc-100">
|
||||||
@ -203,7 +145,6 @@ export default function Home() {
|
|||||||
<div className="divide-y divide-zinc-800">
|
<div className="divide-y divide-zinc-800">
|
||||||
{groupedMessages[date].map((msg) => {
|
{groupedMessages[date].map((msg) => {
|
||||||
const isDailyDigest = isDigest(msg.content);
|
const isDailyDigest = isDigest(msg.content);
|
||||||
const parsed = isDailyDigest ? parseDigest(msg.content) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={msg.id} className={`p-4 group ${isDailyDigest ? 'bg-blue-900/10' : ''}`}>
|
<div key={msg.id} className={`p-4 group ${isDailyDigest ? 'bg-blue-900/10' : ''}`}>
|
||||||
@ -215,59 +156,48 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-start gap-4">
|
<div className="flex justify-between items-start gap-4">
|
||||||
{isDailyDigest && parsed ? (
|
<div className="flex-1 prose prose-invert prose-zinc max-w-none">
|
||||||
<div className="flex-1">
|
<ReactMarkdown
|
||||||
<h3 className="text-lg font-semibold text-blue-400 mb-2">
|
remarkPlugins={[remarkGfm]}
|
||||||
{parsed.title}
|
components={{
|
||||||
</h3>
|
a: ({ node, ...props }) => (
|
||||||
|
<a
|
||||||
{parsed.entries.length > 0 ? (
|
{...props}
|
||||||
<div className="space-y-4">
|
target="_blank"
|
||||||
{Array.from(new Set(parsed.entries.map(e => e.category))).map(cat => (
|
rel="noopener noreferrer"
|
||||||
<div key={cat}>
|
className="text-blue-400 hover:text-blue-300 hover:underline transition-colors"
|
||||||
<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) => (
|
h1: ({ node, ...props }) => (
|
||||||
<li key={i} className="text-sm text-zinc-300">
|
<h1 {...props} className="text-xl font-bold text-zinc-100 mb-3" />
|
||||||
{entry.url ? (
|
),
|
||||||
<a
|
h2: ({ node, ...props }) => (
|
||||||
href={entry.url}
|
<h2 {...props} className="text-lg font-semibold text-zinc-200 mt-4 mb-2" />
|
||||||
target="_blank"
|
),
|
||||||
rel="noopener noreferrer"
|
h3: ({ node, ...props }) => (
|
||||||
className="group/link inline"
|
<h3 {...props} className="text-base font-medium text-zinc-300 mt-3 mb-2" />
|
||||||
>
|
),
|
||||||
<span className="font-medium text-zinc-200 group-hover/link:text-blue-400 transition-colors">
|
ul: ({ node, ...props }) => (
|
||||||
{entry.title}
|
<ul {...props} className="list-disc list-inside space-y-1 text-zinc-300 mb-3" />
|
||||||
</span>
|
),
|
||||||
<span className="inline-flex items-center ml-1 text-blue-500 opacity-60 group-hover/link:opacity-100">
|
li: ({ node, ...props }) => (
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<li {...props} className="text-zinc-300" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
),
|
||||||
</svg>
|
p: ({ node, ...props }) => (
|
||||||
</span>
|
<p {...props} className="text-zinc-300 mb-3 leading-relaxed" />
|
||||||
</a>
|
),
|
||||||
) : (
|
hr: ({ node, ...props }) => (
|
||||||
<span className="font-medium text-zinc-200">{entry.title}</span>
|
<hr {...props} className="border-zinc-700 my-4" />
|
||||||
)}
|
),
|
||||||
<span className="text-zinc-400">: {entry.summary}</span>
|
}}
|
||||||
</li>
|
>
|
||||||
))}
|
{msg.content}
|
||||||
</ul>
|
</ReactMarkdown>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => deleteMessage(msg.id)}
|
onClick={() => deleteMessage(msg.id)}
|
||||||
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm"
|
className="text-zinc-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-sm shrink-0"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user