diff --git a/src/app/api/messages/route.ts b/src/app/api/messages/route.ts index 81085ba..946269c 100644 --- a/src/app/api/messages/route.ts +++ b/src/app/api/messages/route.ts @@ -16,7 +16,7 @@ async function getUser() { export async function GET() { const { data: messages, error } = await supabase .from("blog_messages") - .select("*") + .select("id, date, content, timestamp, tags") .order("timestamp", { ascending: false }); if (error) { @@ -34,7 +34,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { content, date } = await request.json(); + const { content, date, tags } = await request.json(); if (!content || !date) { return NextResponse.json({ error: "Content and date required" }, { status: 400 }); @@ -45,6 +45,7 @@ export async function POST(request: Request) { date, content, timestamp: Date.now(), + tags: tags || [], }; const { error } = await supabase diff --git a/src/app/page.tsx b/src/app/page.tsx index ae24e3f..1f7fbd7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,306 +1,335 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useMemo } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; import { format } from "date-fns"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import Head from "next/head"; +import Link from "next/link"; interface Message { id: string; date: string; content: string; timestamp: number; + tags?: string[]; } export default function BlogPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const selectedTag = searchParams.get("tag"); + const [messages, setMessages] = useState([]); - 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; + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); 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); + console.error("Failed to fetch:", err); } finally { - setInitialLoading(false); + setLoading(false); } } - function loadMore() { - if (loading || !hasMore) return; - - 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); - } + // Get all unique tags + const allTags = useMemo(() => { + const tags = new Set(); + messages.forEach((m) => m.tags?.forEach((t) => tags.add(t))); + return Array.from(tags).sort(); + }, [messages]); - // Parse digest content for preview - function getDigestPreview(content: string) { + // Filter messages + const filteredMessages = useMemo(() => { + let filtered = messages; + + if (selectedTag) { + filtered = filtered.filter((m) => m.tags?.includes(selectedTag)); + } + + if (searchQuery) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter( + (m) => + m.content.toLowerCase().includes(query) || + m.tags?.some((t) => t.toLowerCase().includes(query)) + ); + } + + return filtered.sort((a, b) => b.timestamp - a.timestamp); + }, [messages, selectedTag, searchQuery]); + + // Get featured post (most recent) + const featuredPost = filteredMessages[0]; + const regularPosts = filteredMessages.slice(1); + + // Parse title from content + function getTitle(content: string): string { const lines = content.split("\n"); - const titleLine = lines.find((line) => line.startsWith("## ")); - return titleLine?.replace("## ", "") || "Daily Digest"; + const titleLine = lines.find((l) => l.startsWith("# ") || l.startsWith("## ")); + return titleLine?.replace(/#{1,2}\s/, "") || "Daily Update"; } - // Get first few lines for excerpt - function getExcerpt(content: string, maxLength: number = 200) { - const plainText = content.replace(/#{1,6}\s/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); + // Get excerpt + function getExcerpt(content: string, maxLength: number = 150): string { + const plainText = content + .replace(/#{1,6}\s/g, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/\*/g, "") + .replace(/\n/g, " "); if (plainText.length <= maxLength) return plainText; - return plainText.substring(0, maxLength) + "..."; + return plainText.substring(0, maxLength).trim() + "..."; } - const isDigest = (content: string) => content.includes("##"); + if (loading) { + return ( +
+
+
+ ); + } return ( <> - Daily Digest & Blog | OpenClaw - + Daily Digest | OpenClaw Blog + -
+
{/* Header */} -
-
-
-
-
+
+
+
+ +
๐Ÿ““
-
-

Daily Digest

-

AI Research & Indie Hacking

-
-
-
- {/* Hero Section */} -
-
-
- - ๐Ÿค– AI-Powered Research - -

- Daily insights on
- - iOS, AI & Indie Hacking - -

-

- Curated daily digests covering AI coding assistants, iOS development, - OpenClaw updates, and digital entrepreneurship. -

-
- iOS AI - Claude - OpenClaw - Indie Hacking -
-
-
-
- - {/* Blog Posts */} -
- {initialLoading ? ( -
-
-
- ) : ( - <> -
- {displayedMessages.map((msg, index) => { - const digest = isDigest(msg.content); - const isFirst = index === 0; - - return ( -
+
+ {/* Main Content */} +
+ {/* Page Title */} +
+

+ {selectedTag ? `Posts tagged "${selectedTag}"` : "Latest Posts"} +

+

+ {filteredMessages.length} post{filteredMessages.length !== 1 ? "s" : ""} + {selectedTag && ( + -

- - {/* Full Content (expandable) */} -
-
-

{children}

, - h2: ({ children }) =>

{children}

, - h3: ({ children }) =>

{children}

, - p: ({ children }) =>

{children}

, - ul: ({ children }) =>
    {children}
, - li: ({ children }) =>
  • {children}
  • , - a: ({ children, href }) => ( - - {children} - - ), - code: ({ children }) => ( - - {children} - - ), - hr: () =>
    , - }} - > - {msg.content} -
    -
    - -

    - Posted {format(new Date(msg.timestamp), "h:mm a")} -

    -
    -
    - ); - })} + Clear filter โ†’ + + )} +

    - {/* Load More Trigger */} - {hasMore && ( -
    - {loading ? ( -
    - ) : ( -
    Scroll for more
    + {/* Featured Post */} + {featuredPost && !selectedTag && !searchQuery && ( +
    + +
    +
    + {featuredPost.tags && featuredPost.tags.length > 0 && ( +
    + {featuredPost.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} +
    + )} + + Featured Post + +

    + {getTitle(featuredPost.content)} +

    +

    + {getExcerpt(featuredPost.content, 200)} +

    +
    + {format(new Date(featuredPost.date), "MMMM d, yyyy")} + ยท + 5 min read +
    +
    +
    + +
    + )} + + {/* Post List */} +
    + {regularPosts.map((post) => ( +
    + {/* Date Column */} +
    +
    + {format(new Date(post.date), "d")} +
    +
    + {format(new Date(post.date), "MMM")} +
    +
    + + {/* Content */} +
    + {post.tags && post.tags.length > 0 && ( +
    + {post.tags.slice(0, 2).map((tag) => ( + + ))} +
    + )} + +

    + {getTitle(post.content)} +

    + +

    + {getExcerpt(post.content)} +

    + +
    + {format(new Date(post.date), "MMMM d, yyyy")} + ยท + 3 min read +
    +
    +
    + ))} +
    + + {filteredMessages.length === 0 && ( +
    +

    No posts found.

    + {(selectedTag || searchQuery) && ( + )}
    )} +
    - {!hasMore && messages.length > 0 && ( -
    -

    You've reached the end ๐ŸŽ‰

    + {/* Sidebar */} +
    + {/* Search */} +
    + + setSearchQuery(e.target.value)} + placeholder="Search posts..." + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
    + + {/* Tags */} +
    +

    Tags

    +
    + {allTags.map((tag) => ( + + ))}
    - )} +
    - {messages.length === 0 && ( -
    -
    - ๐Ÿ“ + {/* About */} +
    +

    About

    +

    + Daily curated digests covering AI coding assistants, iOS development, + OpenClaw updates, and digital entrepreneurship. +

    +
    + + {/* Stats */} +
    +

    Stats

    +
    +
    + Total posts + {messages.length} +
    +
    + Tags + {allTags.length}
    -

    No posts yet

    -

    Start adding daily digests to build your archive.

    - )} - - )} -
    - - {/* Footer */} -
    + + {/* Footer */} +