Files
quietthanks/src/app/api/entries/route.ts
Gemini Agent 504f07a106 Add multiple entries per day, user management, reminders, and AI reflections
- Multiple entries per day: Home page now starts fresh, Save & New button
- Admin user management: Add/delete users, reset passwords, toggle admin
- Daily reminders: Browser notifications at configurable time
- AI reflections: Generate insights from entries using Claude API
- Remove cloud sync placeholder (already have user accounts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 12:05:39 +00:00

196 lines
5.6 KiB
TypeScript

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 { getSession } from "@/lib/auth";
import type { CreateEntryRequest, EntryWithTags } from "@/lib/types";
// GET /api/entries - List entries with optional filters
export async function GET(request: NextRequest) {
const user = await getSession();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
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 - always filter by userId
const conditions = [eq(schema.entries.userId, user.id)];
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(and(...conditions))
.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 a new entry or update existing by id
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const body: CreateEntryRequest = await request.json();
const { id: existingId, date, text, mood, roughDay, tagNames } = body;
if (!date || !text) {
return NextResponse.json({ error: "Date and text are required" }, { status: 400 });
}
const now = Date.now();
let entryId: string;
if (existingId) {
// Update existing entry - verify ownership
const existing = await db
.select()
.from(schema.entries)
.where(
and(
eq(schema.entries.id, existingId),
eq(schema.entries.userId, user.id)
)
)
.limit(1);
if (existing.length === 0) {
return NextResponse.json({ error: "Entry not found" }, { status: 404 });
}
entryId = existingId;
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,
userId: user.id,
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 for this user
let tag = await db
.select()
.from(schema.tags)
.where(
and(
eq(schema.tags.userId, user.id),
eq(schema.tags.name, normalizedName)
)
)
.limit(1);
let tagId: string;
if (tag.length === 0) {
tagId = uuidv4();
await db.insert(schema.tags).values({
id: tagId,
userId: user.id,
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 });
}
}