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",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"date-fns": "^4.1.0",
|
||||
"next": "16.1.6",
|
||||
"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": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "@tailwindcss/typography";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
|
||||
154
src/app/page.tsx
154
src/app/page.tsx
@ -2,6 +2,8 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { format } from "date-fns";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
@ -10,65 +12,6 @@ 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('- **')) {
|
||||
// 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() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [newMessage, setNewMessage] = useState("");
|
||||
@ -77,7 +20,6 @@ export default function Home() {
|
||||
return today.toISOString().split("T")[0];
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
@ -146,7 +88,7 @@ export default function Home() {
|
||||
);
|
||||
|
||||
// Check if message is a digest
|
||||
const isDigest = (content: string) => content.includes("## Daily Digest");
|
||||
const isDigest = (content: string) => content.includes("##");
|
||||
|
||||
return (
|
||||
<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">
|
||||
{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' : ''}`}>
|
||||
@ -215,59 +156,48 @@ export default function Home() {
|
||||
)}
|
||||
|
||||
<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">
|
||||
{entry.url ? (
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group/link inline"
|
||||
>
|
||||
<span className="font-medium text-zinc-200 group-hover/link:text-blue-400 transition-colors">
|
||||
{entry.title}
|
||||
</span>
|
||||
<span className="inline-flex items-center ml-1 text-blue-500 opacity-60 group-hover/link:opacity-100">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-medium text-zinc-200">{entry.title}</span>
|
||||
)}
|
||||
<span className="text-zinc-400">: {entry.summary}</span>
|
||||
</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>
|
||||
)}
|
||||
<div className="flex-1 prose prose-invert prose-zinc max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
a: ({ node, ...props }) => (
|
||||
<a
|
||||
{...props}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 hover:text-blue-300 hover:underline transition-colors"
|
||||
/>
|
||||
),
|
||||
h1: ({ node, ...props }) => (
|
||||
<h1 {...props} className="text-xl font-bold text-zinc-100 mb-3" />
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<h2 {...props} className="text-lg font-semibold text-zinc-200 mt-4 mb-2" />
|
||||
),
|
||||
h3: ({ node, ...props }) => (
|
||||
<h3 {...props} className="text-base font-medium text-zinc-300 mt-3 mb-2" />
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul {...props} className="list-disc list-inside space-y-1 text-zinc-300 mb-3" />
|
||||
),
|
||||
li: ({ node, ...props }) => (
|
||||
<li {...props} className="text-zinc-300" />
|
||||
),
|
||||
p: ({ node, ...props }) => (
|
||||
<p {...props} className="text-zinc-300 mb-3 leading-relaxed" />
|
||||
),
|
||||
hr: ({ node, ...props }) => (
|
||||
<hr {...props} className="border-zinc-700 my-4" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
✕
|
||||
|
||||
Loading…
Reference in New Issue
Block a user