mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-24 21:31:41 +08:00
Initial commit: Quiet Thanks gratitude app
A calm, private gratitude and mood log built with Next.js 16, TypeScript, Tailwind CSS, and SQLite/Drizzle ORM. Features: - Quick check-in with autosave (800ms debounce) - Optional mood selector (5 levels) with accessibility labels - Optional tags with tap-to-add from recent - Timeline with weekly reflection card - Filters by mood, tag, and rough day - Export to Markdown and JSON - Dark mode default - Delete with undo toast - Docker deployment ready Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
170
src/app/api/entries/route.ts
Normal file
170
src/app/api/entries/route.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db, schema } from "@/lib/db";
|
||||
import { eq, desc, and, inArray, gte } from "drizzle-orm";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { CreateEntryRequest, EntryWithTags } from "@/lib/types";
|
||||
|
||||
// GET /api/entries - List entries with optional filters
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const moods = searchParams.get("moods")?.split(",").map(Number).filter(Boolean);
|
||||
const tagId = searchParams.get("tagId");
|
||||
const roughDay = searchParams.get("roughDay");
|
||||
const since = searchParams.get("since"); // YYYY-MM-DD
|
||||
|
||||
try {
|
||||
// Build conditions
|
||||
const conditions = [];
|
||||
if (moods && moods.length > 0) {
|
||||
conditions.push(inArray(schema.entries.mood, moods));
|
||||
}
|
||||
if (roughDay === "true") {
|
||||
conditions.push(eq(schema.entries.roughDay, 1));
|
||||
} else if (roughDay === "false") {
|
||||
conditions.push(eq(schema.entries.roughDay, 0));
|
||||
}
|
||||
if (since) {
|
||||
conditions.push(gte(schema.entries.date, since));
|
||||
}
|
||||
|
||||
// Get entries
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(schema.entries)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(desc(schema.entries.date), desc(schema.entries.createdAt));
|
||||
|
||||
// Get tags for each entry
|
||||
const entriesWithTags: EntryWithTags[] = [];
|
||||
for (const entry of entries) {
|
||||
const entryTagRows = await db
|
||||
.select({ tag: schema.tags })
|
||||
.from(schema.entryTags)
|
||||
.innerJoin(schema.tags, eq(schema.entryTags.tagId, schema.tags.id))
|
||||
.where(eq(schema.entryTags.entryId, entry.id));
|
||||
|
||||
const tags = entryTagRows.map((row) => row.tag);
|
||||
|
||||
// Filter by tagId if specified
|
||||
if (tagId && !tags.some((t) => t.id === tagId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entriesWithTags.push({ ...entry, tags });
|
||||
}
|
||||
|
||||
return NextResponse.json(entriesWithTags);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch entries:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch entries" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/entries - Create or update today's entry
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: CreateEntryRequest = await request.json();
|
||||
const { date, text, mood, roughDay, tagNames } = body;
|
||||
|
||||
if (!date || !text) {
|
||||
return NextResponse.json({ error: "Date and text are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Check if entry exists for this date
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(schema.entries)
|
||||
.where(eq(schema.entries.date, date))
|
||||
.limit(1);
|
||||
|
||||
let entryId: string;
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing entry
|
||||
entryId = existing[0].id;
|
||||
await db
|
||||
.update(schema.entries)
|
||||
.set({
|
||||
text,
|
||||
mood: mood ?? null,
|
||||
roughDay: roughDay ? 1 : 0,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(schema.entries.id, entryId));
|
||||
} else {
|
||||
// Create new entry
|
||||
entryId = uuidv4();
|
||||
await db.insert(schema.entries).values({
|
||||
id: entryId,
|
||||
date,
|
||||
text,
|
||||
mood: mood ?? null,
|
||||
roughDay: roughDay ? 1 : 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
// Update tags
|
||||
// Remove existing tag associations
|
||||
await db.delete(schema.entryTags).where(eq(schema.entryTags.entryId, entryId));
|
||||
|
||||
// Add new tags
|
||||
if (tagNames && tagNames.length > 0) {
|
||||
for (const name of tagNames) {
|
||||
const normalizedName = name.toLowerCase().trim();
|
||||
if (!normalizedName) continue;
|
||||
|
||||
// Find or create tag
|
||||
let tag = await db
|
||||
.select()
|
||||
.from(schema.tags)
|
||||
.where(eq(schema.tags.name, normalizedName))
|
||||
.limit(1);
|
||||
|
||||
let tagId: string;
|
||||
if (tag.length === 0) {
|
||||
tagId = uuidv4();
|
||||
await db.insert(schema.tags).values({
|
||||
id: tagId,
|
||||
name: normalizedName,
|
||||
createdAt: now,
|
||||
});
|
||||
} else {
|
||||
tagId = tag[0].id;
|
||||
}
|
||||
|
||||
// Create association
|
||||
await db
|
||||
.insert(schema.entryTags)
|
||||
.values({ entryId, tagId })
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch and return the updated entry with tags
|
||||
const entry = await db
|
||||
.select()
|
||||
.from(schema.entries)
|
||||
.where(eq(schema.entries.id, entryId))
|
||||
.limit(1);
|
||||
|
||||
const entryTagRows = await db
|
||||
.select({ tag: schema.tags })
|
||||
.from(schema.entryTags)
|
||||
.innerJoin(schema.tags, eq(schema.entryTags.tagId, schema.tags.id))
|
||||
.where(eq(schema.entryTags.entryId, entryId));
|
||||
|
||||
const entryWithTags: EntryWithTags = {
|
||||
...entry[0],
|
||||
tags: entryTagRows.map((row) => row.tag),
|
||||
};
|
||||
|
||||
return NextResponse.json(entryWithTags);
|
||||
} catch (error) {
|
||||
console.error("Failed to create/update entry:", error);
|
||||
return NextResponse.json({ error: "Failed to save entry" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user