mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-24 21:31:41 +08:00
- 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>
196 lines
5.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|