Add working blog backup functionality

- Replaced default Next.js template with daily message backup UI
- Added API endpoint for CRUD operations on messages
- Created message form with date picker and textarea
- Grouped messages by date with expandable sections
- Data persists to data/messages.json
This commit is contained in:
Matt Bruce 2026-02-18 09:32:47 -06:00
parent 8fdb7f8350
commit 6c7dfd8b40
4 changed files with 235 additions and 57 deletions

1
data/messages.json Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -1,7 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: 'standalone',
}; };
export default nextConfig; export default nextConfig;

View File

@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
const DATA_FILE = join(process.cwd(), "data", "messages.json");
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));
}
export async function GET() {
const messages = getMessages();
return NextResponse.json(messages);
}
export async function POST(request: Request) {
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 = {
id: Date.now().toString(),
date,
content,
timestamp: Date.now(),
};
messages.unshift(newMessage);
saveMessages(messages);
return NextResponse.json(newMessage);
}
export async function DELETE(request: Request) {
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);
return NextResponse.json({ success: true });
}

View File

@ -1,65 +1,170 @@
import Image from "next/image"; "use client";
import { useState, useEffect } from "react";
interface Message {
id: string;
date: string;
content: string;
timestamp: number;
}
export default function Home() { export default function Home() {
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
const [selectedDate, setSelectedDate] = useState(() => {
const today = new Date();
return today.toISOString().split("T")[0];
});
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchMessages();
}, []);
async function fetchMessages() {
try {
const res = await fetch("/api/messages");
const data = await res.json();
setMessages(data);
} 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);
}
}
async function deleteMessage(id: string) {
if (!confirm("Delete this message?")) 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);
}
}
// 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<string, Message[]>);
const sortedDates = Object.keys(groupedMessages).sort((a, b) =>
new Date(b).getTime() - new Date(a).getTime()
);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="min-h-screen bg-zinc-950 text-zinc-100">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <div className="max-w-3xl mx-auto px-4 py-8">
<Image <header className="mb-8">
className="dark:invert" <h1 className="text-3xl font-bold mb-2">📓 Daily Blog Backup</h1>
src="/next.svg" <p className="text-zinc-400">Backup of your daily messages and thoughts</p>
alt="Next.js logo" </header>
width={100}
height={20} {/* Add new message */}
priority <form onSubmit={addMessage} className="mb-8 bg-zinc-900 rounded-lg p-4 border border-zinc-800">
/> <div className="mb-4">
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> <label className="block text-sm font-medium text-zinc-400 mb-1">Date</label>
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> <input
To get started, edit the page.tsx file. type="date"
</h1> value={selectedDate}
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> onChange={(e) => setSelectedDate(e.target.value)}
Looking for a starting point or more instructions? Head over to{" "} 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"
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/> />
Deploy Now </div>
</a> <div className="mb-4">
<a <label className="block text-sm font-medium text-zinc-400 mb-1">Message</label>
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" <textarea
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" value={newMessage}
target="_blank" onChange={(e) => setNewMessage(e.target.value)}
rel="noopener noreferrer" placeholder="Write your daily update here..."
rows={4}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-blue-500 resize-none"
/>
</div>
<button
type="submit"
disabled={loading || !newMessage.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-zinc-700 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded transition-colors"
> >
Documentation {loading ? "Saving..." : "Save Message"}
</a> </button>
</form>
{/* Messages list */}
<div className="space-y-6">
{sortedDates.length === 0 ? (
<div className="text-center py-12 text-zinc-500">
<p>No messages yet. Add your first entry above!</p>
</div>
) : (
sortedDates.map((date) => (
<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">
<h2 className="font-semibold text-zinc-300">
{new Date(date).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</h2>
</div>
<div className="divide-y divide-zinc-800">
{groupedMessages[date].map((msg) => (
<div key={msg.id} className="p-4 group">
<div className="flex justify-between items-start gap-4">
<p className="text-zinc-300 whitespace-pre-wrap">{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">
{new Date(msg.timestamp).toLocaleTimeString()}
</p>
</div>
))}
</div>
</div>
))
)}
</div> </div>
</main> </div>
</div> </div>
); );
} }