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:
parent
8fdb7f8350
commit
6c7dfd8b40
1
data/messages.json
Normal file
1
data/messages.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@ -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;
|
||||||
|
|||||||
72
src/app/api/messages/route.ts
Normal file
72
src/app/api/messages/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
217
src/app/page.tsx
217
src/app/page.tsx
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user