diff --git a/package-lock.json b/package-lock.json index 407ce76..a5f4b8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "blog-backup", "version": "0.1.0", "dependencies": { + "@supabase/supabase-js": "^2.97.0", "@tailwindcss/typography": "^0.5.19", "date-fns": "^4.1.0", "next": "16.1.6", @@ -1237,6 +1238,86 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz", + "integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz", + "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz", + "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz", + "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz", + "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz", + "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.97.0", + "@supabase/functions-js": "2.97.0", + "@supabase/postgrest-js": "2.97.0", + "@supabase/realtime-js": "2.97.0", + "@supabase/storage-js": "2.97.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1606,17 +1687,21 @@ "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1638,6 +1723,15 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -2793,7 +2887,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4170,6 +4263,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7430,7 +7532,6 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -7725,7 +7826,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -8040,6 +8140,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index d161916..8d636d8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@supabase/supabase-js": "^2.97.0", "@tailwindcss/typography": "^0.5.19", "date-fns": "^4.1.0", "next": "16.1.6", diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts index ba25234..81085ba 100644 --- a/src/app/api/messages/route.ts +++ b/src/app/api/messages/route.ts @@ -1,72 +1,86 @@ import { NextResponse } from "next/server"; -import { readFileSync, writeFileSync, existsSync } from "fs"; -import { join } from "path"; +import { createClient } from "@supabase/supabase-js"; -const DATA_FILE = join(process.cwd(), "data", "messages.json"); +const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! +); -interface Message { - id: string; - date: string; - content: string; - timestamp: number; -} - -function getMessages(): Message[] { - if (!existsSync(DATA_FILE)) { - return []; - } - try { - const data = readFileSync(DATA_FILE, "utf-8"); - return JSON.parse(data); - } catch { - return []; - } -} - -function saveMessages(messages: Message[]) { - const dir = join(process.cwd(), "data"); - if (!existsSync(dir)) { - require("fs").mkdirSync(dir, { recursive: true }); - } - writeFileSync(DATA_FILE, JSON.stringify(messages, null, 2)); +// Helper to check auth +async function getUser() { + const { data: { user } } = await supabase.auth.getUser(); + return user; } +// GET is public (read-only) export async function GET() { - const messages = getMessages(); - return NextResponse.json(messages); + const { data: messages, error } = await supabase + .from("blog_messages") + .select("*") + .order("timestamp", { ascending: false }); + + if (error) { + console.error("Error fetching messages:", error); + return NextResponse.json({ error: "Failed to fetch messages" }, { status: 500 }); + } + + return NextResponse.json(messages || []); } +// POST requires auth export async function POST(request: Request) { + const user = await getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { content, date } = await request.json(); if (!content || !date) { return NextResponse.json({ error: "Content and date required" }, { status: 400 }); } - const messages = getMessages(); - const newMessage: Message = { + const newMessage = { id: Date.now().toString(), date, content, timestamp: Date.now(), }; - messages.unshift(newMessage); - saveMessages(messages); + const { error } = await supabase + .from("blog_messages") + .insert(newMessage); + + if (error) { + console.error("Error saving message:", error); + return NextResponse.json({ error: "Failed to save message" }, { status: 500 }); + } return NextResponse.json(newMessage); } +// DELETE requires auth export async function DELETE(request: Request) { + const user = await getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { id } = await request.json(); if (!id) { return NextResponse.json({ error: "ID required" }, { status: 400 }); } - let messages = getMessages(); - messages = messages.filter((m) => m.id !== id); - saveMessages(messages); + const { error } = await supabase + .from("blog_messages") + .delete() + .eq("id", id); + + if (error) { + console.error("Error deleting message:", error); + return NextResponse.json({ error: "Failed to delete message" }, { status: 500 }); + } return NextResponse.json({ success: true }); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..5db7407 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,16 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Inter } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const inter = Inter({ subsets: ["latin"], + variable: "--font-inter", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Daily Digest | OpenClaw", + description: "AI-powered daily digests covering iOS development, AI coding assistants, OpenClaw updates, and indie hacking insights.", + keywords: ["iOS", "AI", "OpenClaw", "Indie Hacking", "Daily Digest", "Claude", "Cursor"], }; export default function RootLayout({ @@ -23,12 +19,8 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - {children} - + + {children} ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index f332e5e..ae24e3f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { format } from "date-fns"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import Head from "next/head"; interface Message { id: string; @@ -12,189 +13,296 @@ interface Message { timestamp: number; } -export default function Home() { +export default function BlogPage() { const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(""); - const [selectedDate, setSelectedDate] = useState(() => { - const today = new Date(); - return today.toISOString().split("T")[0]; - }); + const [displayedMessages, setDisplayedMessages] = useState([]); + const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); + const [initialLoading, setInitialLoading] = useState(true); + const observerRef = useRef(null); + const loadMoreRef = useRef(null); + + const ITEMS_PER_PAGE = 5; useEffect(() => { fetchMessages(); }, []); + useEffect(() => { + // Setup intersection observer for infinite scroll + if (loadMoreRef.current) { + observerRef.current = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + loadMore(); + } + }, + { threshold: 0.1 } + ); + observerRef.current.observe(loadMoreRef.current); + } + + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, [hasMore, loading, messages]); + async function fetchMessages() { try { + setInitialLoading(true); const res = await fetch("/api/messages"); const data = await res.json(); setMessages(data); + setDisplayedMessages(data.slice(0, ITEMS_PER_PAGE)); + setHasMore(data.length > ITEMS_PER_PAGE); } catch (err) { console.error("Failed to fetch messages:", err); - } - } - - async function addMessage(e: React.FormEvent) { - e.preventDefault(); - if (!newMessage.trim()) return; - - setLoading(true); - try { - const res = await fetch("/api/messages", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: newMessage, date: selectedDate }), - }); - - if (res.ok) { - setNewMessage(""); - fetchMessages(); - } - } catch (err) { - console.error("Failed to add message:", err); } finally { - setLoading(false); + setInitialLoading(false); } } - async function deleteMessage(id: string) { - if (!confirm("Delete this message?")) return; + function loadMore() { + if (loading || !hasMore) return; - try { - const res = await fetch("/api/messages", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id }), - }); - - if (res.ok) { - fetchMessages(); - } - } catch (err) { - console.error("Failed to delete message:", err); - } + setLoading(true); + const currentLength = displayedMessages.length; + const nextBatch = messages.slice(currentLength, currentLength + ITEMS_PER_PAGE); + + setTimeout(() => { + setDisplayedMessages((prev) => [...prev, ...nextBatch]); + setHasMore(currentLength + nextBatch.length < messages.length); + setLoading(false); + }, 300); } - // Group messages by date - const groupedMessages = messages.reduce((acc, msg) => { - if (!acc[msg.date]) acc[msg.date] = []; - acc[msg.date].push(msg); - return acc; - }, {} as Record); + // Parse digest content for preview + function getDigestPreview(content: string) { + const lines = content.split("\n"); + const titleLine = lines.find((line) => line.startsWith("## ")); + return titleLine?.replace("## ", "") || "Daily Digest"; + } - const sortedDates = Object.keys(groupedMessages).sort((a, b) => - new Date(b).getTime() - new Date(a).getTime() - ); + // Get first few lines for excerpt + function getExcerpt(content: string, maxLength: number = 200) { + const plainText = content.replace(/#{1,6}\s/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + if (plainText.length <= maxLength) return plainText; + return plainText.substring(0, maxLength) + "..."; + } - // Check if message is a digest const isDigest = (content: string) => content.includes("##"); return ( -
-
-
-

📓 Daily Blog & Digest

-

Daily digests and notes backup

-
+ <> + + Daily Digest & Blog | OpenClaw + + - {/* Add new message */} -
-
- - setSelectedDate(e.target.value)} - className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-zinc-100 focus:outline-none focus:border-blue-500" - /> -
-
- -