Add BMAD framework, authentication, and new features
This commit is contained in:
29
keep-notes/app/actions/auth.ts
Normal file
29
keep-notes/app/actions/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'use server';
|
||||
|
||||
import { signIn } from '@/auth';
|
||||
import { AuthError } from 'next-auth';
|
||||
|
||||
export async function authenticate(
|
||||
prevState: string | undefined,
|
||||
formData: FormData,
|
||||
) {
|
||||
try {
|
||||
await signIn('credentials', formData);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
console.error('AuthError details:', error.type, error.message);
|
||||
switch (error.type) {
|
||||
case 'CredentialsSignin':
|
||||
return 'Invalid credentials.';
|
||||
default:
|
||||
return `Auth error: ${error.type}`;
|
||||
}
|
||||
}
|
||||
// IMPORTANT: Next.js redirects throw a special error that must be rethrown
|
||||
if (error instanceof Error && error.message === 'NEXT_REDIRECT') {
|
||||
throw error;
|
||||
}
|
||||
console.error('Non-AuthError during signin:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Note, CheckItem } from '@/lib/types'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
// Helper function to parse JSON strings from database
|
||||
function parseNote(dbNote: any): Note {
|
||||
@@ -11,14 +12,21 @@ function parseNote(dbNote: any): Note {
|
||||
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
|
||||
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
|
||||
images: dbNote.images ? JSON.parse(dbNote.images) : null,
|
||||
links: dbNote.links ? JSON.parse(dbNote.links) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all notes (non-archived by default)
|
||||
export async function getNotes(includeArchived = false) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return [];
|
||||
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: includeArchived ? {} : { isArchived: false },
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: includeArchived ? {} : { isArchived: false },
|
||||
},
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ order: 'asc' },
|
||||
@@ -35,9 +43,15 @@ export async function getNotes(includeArchived = false) {
|
||||
|
||||
// Get archived notes only
|
||||
export async function getArchivedNotes() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return [];
|
||||
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { isArchived: true },
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: true
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
})
|
||||
|
||||
@@ -50,6 +64,9 @@ export async function getArchivedNotes() {
|
||||
|
||||
// Search notes
|
||||
export async function searchNotes(query: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return [];
|
||||
|
||||
try {
|
||||
if (!query.trim()) {
|
||||
return await getNotes()
|
||||
@@ -57,6 +74,7 @@ export async function searchNotes(query: string) {
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: false,
|
||||
OR: [
|
||||
{ title: { contains: query } },
|
||||
@@ -72,7 +90,7 @@ export async function searchNotes(query: string) {
|
||||
})
|
||||
|
||||
// Enhanced ranking: prioritize title matches
|
||||
const rankedNotes = notes.map(note => {
|
||||
const rankedNotes = notes.map((note: any) => {
|
||||
const parsedNote = parseNote(note)
|
||||
let score = 0
|
||||
|
||||
@@ -101,8 +119,8 @@ export async function searchNotes(query: string) {
|
||||
|
||||
// Sort by score descending, then by existing order (pinned/updated)
|
||||
return rankedNotes
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map(item => item.note)
|
||||
.sort((a: any, b: any) => b.score - a.score)
|
||||
.map((item: any) => item.note)
|
||||
} catch (error) {
|
||||
console.error('Error searching notes:', error)
|
||||
return []
|
||||
@@ -118,13 +136,18 @@ export async function createNote(data: {
|
||||
checkItems?: CheckItem[]
|
||||
labels?: string[]
|
||||
images?: string[]
|
||||
links?: any[]
|
||||
isArchived?: boolean
|
||||
reminder?: Date | null
|
||||
isMarkdown?: boolean
|
||||
}) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
title: data.title || null,
|
||||
content: data.content,
|
||||
color: data.color || 'default',
|
||||
@@ -132,6 +155,7 @@ export async function createNote(data: {
|
||||
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
|
||||
labels: data.labels ? JSON.stringify(data.labels) : null,
|
||||
images: data.images ? JSON.stringify(data.images) : null,
|
||||
links: data.links ? JSON.stringify(data.links) : null,
|
||||
isArchived: data.isArchived || false,
|
||||
reminder: data.reminder || null,
|
||||
isMarkdown: data.isMarkdown || false,
|
||||
@@ -157,9 +181,13 @@ export async function updateNote(id: string, data: {
|
||||
checkItems?: CheckItem[] | null
|
||||
labels?: string[] | null
|
||||
images?: string[] | null
|
||||
links?: any[] | null
|
||||
reminder?: Date | null
|
||||
isMarkdown?: boolean
|
||||
}) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
// Stringify JSON fields if they exist
|
||||
const updateData: any = { ...data }
|
||||
@@ -172,10 +200,16 @@ export async function updateNote(id: string, data: {
|
||||
if ('images' in data) {
|
||||
updateData.images = data.images ? JSON.stringify(data.images) : null
|
||||
}
|
||||
if ('links' in data) {
|
||||
updateData.links = data.links ? JSON.stringify(data.links) : null
|
||||
}
|
||||
updateData.updatedAt = new Date()
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id },
|
||||
where: {
|
||||
id,
|
||||
userId: session.user.id
|
||||
},
|
||||
data: updateData
|
||||
})
|
||||
|
||||
@@ -189,9 +223,15 @@ export async function updateNote(id: string, data: {
|
||||
|
||||
// Delete a note
|
||||
export async function deleteNote(id: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
await prisma.note.delete({
|
||||
where: { id }
|
||||
where: {
|
||||
id,
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
@@ -230,7 +270,7 @@ export async function getAllLabels() {
|
||||
})
|
||||
|
||||
const labelsSet = new Set<string>()
|
||||
notes.forEach(note => {
|
||||
notes.forEach((note: any) => {
|
||||
const labels = note.labels ? JSON.parse(note.labels) : null
|
||||
if (labels) {
|
||||
labels.forEach((label: string) => labelsSet.add(label))
|
||||
@@ -246,11 +286,14 @@ export async function getAllLabels() {
|
||||
|
||||
// Reorder notes (drag and drop)
|
||||
export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
console.log('[REORDER-DEBUG] reorderNotes called:', { draggedId, targetId })
|
||||
|
||||
try {
|
||||
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
|
||||
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
|
||||
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId, userId: session.user.id } })
|
||||
const targetNote = await prisma.note.findUnique({ where: { id: targetId, userId: session.user.id } })
|
||||
|
||||
console.log('[REORDER-DEBUG] Notes found:', {
|
||||
draggedNote: draggedNote ? { id: draggedNote.id, title: draggedNote.title, isPinned: draggedNote.isPinned, order: draggedNote.order } : null,
|
||||
@@ -265,23 +308,24 @@ export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
// Get all notes in the same category (pinned or unpinned)
|
||||
const allNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isPinned: draggedNote.isPinned,
|
||||
isArchived: false
|
||||
},
|
||||
orderBy: { order: 'asc' }
|
||||
})
|
||||
|
||||
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map(n => ({ id: n.id, title: n.title, order: n.order })))
|
||||
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map((n: any) => ({ id: n.id, title: n.title, order: n.order })))
|
||||
|
||||
// Create new order array
|
||||
const reorderedNotes = allNotes.filter(n => n.id !== draggedId)
|
||||
const targetIndex = reorderedNotes.findIndex(n => n.id === targetId)
|
||||
const reorderedNotes = allNotes.filter((n: any) => n.id !== draggedId)
|
||||
const targetIndex = reorderedNotes.findIndex((n: any) => n.id === targetId)
|
||||
reorderedNotes.splice(targetIndex, 0, draggedNote)
|
||||
|
||||
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n, i) => ({ id: n.id, title: n.title, newOrder: i })))
|
||||
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n: any, i: number) => ({ id: n.id, title: n.title, newOrder: i })))
|
||||
|
||||
// Update all notes with new order
|
||||
const updates = reorderedNotes.map((note, index) =>
|
||||
const updates = reorderedNotes.map((note: any, index: number) =>
|
||||
prisma.note.update({
|
||||
where: { id: note.id },
|
||||
data: { order: index }
|
||||
@@ -299,3 +343,27 @@ export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
throw new Error('Failed to reorder notes')
|
||||
}
|
||||
}
|
||||
|
||||
// Update full order of notes
|
||||
export async function updateFullOrder(ids: string[]) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
try {
|
||||
const updates = ids.map((id: string, index: number) =>
|
||||
prisma.note.update({
|
||||
where: { id, userId },
|
||||
data: { order: index }
|
||||
})
|
||||
)
|
||||
|
||||
await prisma.$transaction(updates)
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error updating full order:', error)
|
||||
throw new Error('Failed to update order')
|
||||
}
|
||||
}
|
||||
|
||||
54
keep-notes/app/actions/register.ts
Normal file
54
keep-notes/app/actions/register.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
'use server';
|
||||
|
||||
import bcrypt from 'bcryptjs';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { z } from 'zod';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const RegisterSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
name: z.string().min(2),
|
||||
});
|
||||
|
||||
export async function register(prevState: string | undefined, formData: FormData) {
|
||||
const validatedFields = RegisterSchema.safeParse({
|
||||
email: formData.get('email'),
|
||||
password: formData.get('password'),
|
||||
name: formData.get('name'),
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return 'Invalid fields. Failed to register.';
|
||||
}
|
||||
|
||||
const { email, password, name } = validatedFields.data;
|
||||
|
||||
try {
|
||||
const existingUser = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
||||
if (existingUser) {
|
||||
return 'User already exists.';
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email: email.toLowerCase(),
|
||||
password: hashedPassword,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
// Attempt to sign in immediately after registration
|
||||
// We cannot import signIn here directly if it causes circular deps or issues,
|
||||
// but usually it works. If not, redirecting to login is fine.
|
||||
// Let's stick to redirecting to login but with a clear success message?
|
||||
// Or better: lowercase the email to fix the potential bug.
|
||||
} catch (error) {
|
||||
console.error('Registration Error:', error);
|
||||
return 'Database Error: Failed to create user.';
|
||||
}
|
||||
|
||||
redirect('/login');
|
||||
}
|
||||
54
keep-notes/app/actions/scrape.ts
Normal file
54
keep-notes/app/actions/scrape.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
'use server'
|
||||
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
export interface LinkMetadata {
|
||||
url: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
imageUrl?: string;
|
||||
siteName?: string;
|
||||
}
|
||||
|
||||
export async function fetchLinkMetadata(url: string): Promise<LinkMetadata | null> {
|
||||
try {
|
||||
// Add protocol if missing
|
||||
let targetUrl = url;
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
targetUrl = 'https://' + url;
|
||||
}
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; Memento/1.0; +http://localhost:3000)',
|
||||
},
|
||||
next: { revalidate: 3600 } // Cache for 1 hour
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const getMeta = (prop: string) =>
|
||||
$(`meta[property="${prop}"]`).attr('content') ||
|
||||
$(`meta[name="${prop}"]`).attr('content');
|
||||
|
||||
const title = getMeta('og:title') || $('title').text() || '';
|
||||
const description = getMeta('og:description') || getMeta('description') || '';
|
||||
const imageUrl = getMeta('og:image');
|
||||
const siteName = getMeta('og:site_name');
|
||||
|
||||
return {
|
||||
url: targetUrl,
|
||||
title: title.substring(0, 100), // Truncate if too long
|
||||
description: description.substring(0, 200),
|
||||
imageUrl,
|
||||
siteName
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching link metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
31
keep-notes/app/api/admin/randomize-labels/route.ts
Normal file
31
keep-notes/app/api/admin/randomize-labels/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { LABEL_COLORS } from '@/lib/types';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const labels = await prisma.label.findMany();
|
||||
const colors = Object.keys(LABEL_COLORS).filter(c => c !== 'gray'); // Exclude gray to force colors
|
||||
|
||||
const updates = labels.map((label: any) => {
|
||||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
return prisma.label.update({
|
||||
where: { id: label.id },
|
||||
data: { color: randomColor }
|
||||
});
|
||||
});
|
||||
|
||||
await prisma.$transaction(updates);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
updated: updates.length,
|
||||
message: "All labels have been assigned a random non-gray color."
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
57
keep-notes/app/api/admin/sync-labels/route.ts
Normal file
57
keep-notes/app/api/admin/sync-labels/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. Get all notes
|
||||
const notes = await prisma.note.findMany({
|
||||
select: { labels: true }
|
||||
});
|
||||
|
||||
// 2. Extract all unique labels from JSON
|
||||
const uniqueLabels = new Set<string>();
|
||||
notes.forEach((note: any) => {
|
||||
if (note.labels) {
|
||||
try {
|
||||
const parsed = JSON.parse(note.labels);
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed.forEach((l: string) => uniqueLabels.add(l));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Get existing labels in DB
|
||||
const existingDbLabels = await prisma.label.findMany();
|
||||
const existingNames = new Set(existingDbLabels.map((l: any) => l.name));
|
||||
|
||||
// 4. Create missing labels
|
||||
const created = [];
|
||||
for (const name of uniqueLabels) {
|
||||
if (!existingNames.has(name)) {
|
||||
const newLabel = await prisma.label.create({
|
||||
data: {
|
||||
name,
|
||||
color: 'gray' // Default color
|
||||
}
|
||||
});
|
||||
created.push(newLabel);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
foundInNotes: uniqueLabels.size,
|
||||
alreadyInDb: existingNames.size,
|
||||
created: created.length,
|
||||
createdLabels: created
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
2
keep-notes/app/api/auth/[...nextauth]/route.ts
Normal file
2
keep-notes/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { handlers } from "@/auth"
|
||||
export const { GET, POST } = handlers
|
||||
62
keep-notes/app/api/cron/reminders/route.ts
Normal file
62
keep-notes/app/api/cron/reminders/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // No caching
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
// 1. Find all due reminders that haven't been processed
|
||||
const dueNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
reminder: {
|
||||
lte: now, // Less than or equal to now
|
||||
},
|
||||
isReminderDone: false,
|
||||
isArchived: false, // Optional: exclude archived notes
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
reminder: true,
|
||||
// Add other fields useful for notification
|
||||
},
|
||||
});
|
||||
|
||||
if (dueNotes.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: 0,
|
||||
message: 'No due reminders found'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Mark them as done (Atomic operation logic would be better but simple batch update is fine here)
|
||||
const noteIds = dueNotes.map((n: any) => n.id);
|
||||
|
||||
await prisma.note.updateMany({
|
||||
where: {
|
||||
id: { in: noteIds }
|
||||
},
|
||||
data: {
|
||||
isReminderDone: true
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Return the notes to N8N so it can send emails/messages
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: dueNotes.length,
|
||||
reminders: dueNotes
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing cron reminders:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
|
||||
const COLORS = ['red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray'];
|
||||
|
||||
// GET /api/labels - Get all labels
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const labels = await prisma.label.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { name: 'asc' }
|
||||
})
|
||||
|
||||
@@ -23,6 +32,11 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// POST /api/labels - Create a new label
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { name, color } = body
|
||||
@@ -34,9 +48,14 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if label already exists
|
||||
// Check if label already exists for this user
|
||||
const existing = await prisma.label.findUnique({
|
||||
where: { name: name.trim() }
|
||||
where: {
|
||||
name_userId: {
|
||||
name: name.trim(),
|
||||
userId: session.user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
@@ -49,7 +68,8 @@ export async function POST(request: NextRequest) {
|
||||
const label = await prisma.label.create({
|
||||
data: {
|
||||
name: name.trim(),
|
||||
color: color || 'gray'
|
||||
color: color || COLORS[Math.floor(Math.random() * COLORS.length)],
|
||||
userId: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ function parseNote(dbNote: any) {
|
||||
// GET /api/notes/[id] - Get a single note
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: params.id }
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
@@ -43,9 +44,10 @@ export async function GET(
|
||||
// PUT /api/notes/[id] - Update a note
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const updateData: any = { ...body }
|
||||
|
||||
@@ -59,7 +61,7 @@ export async function PUT(
|
||||
updateData.updatedAt = new Date()
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id: params.id },
|
||||
where: { id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
@@ -79,11 +81,12 @@ export async function PUT(
|
||||
// DELETE /api/notes/[id] - Delete a note
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.note.delete({
|
||||
where: { id: params.id }
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
39
keep-notes/app/api/upload/route.ts
Normal file
39
keep-notes/app/api/upload/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFile, mkdir } from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file uploaded' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer())
|
||||
const filename = `${randomUUID()}${path.extname(file.name)}`
|
||||
|
||||
// Ensure directory exists
|
||||
const uploadDir = path.join(process.cwd(), 'public/uploads/notes')
|
||||
await mkdir(uploadDir, { recursive: true })
|
||||
|
||||
const filePath = path.join(uploadDir, filename)
|
||||
await writeFile(filePath, buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: `/uploads/notes/${filename}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to upload file' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getArchivedNotes } from '@/app/actions/notes'
|
||||
import { NoteGrid } from '@/components/note-grid'
|
||||
import { MasonryGrid } from '@/components/masonry-grid'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -9,7 +9,7 @@ export default async function ArchivePage() {
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
<h1 className="text-3xl font-bold mb-8">Archive</h1>
|
||||
<NoteGrid notes={notes} />
|
||||
<MasonryGrid notes={notes} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,48 @@
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
[data-theme='midnight'] {
|
||||
--background: oklch(0.18 0.04 260);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.22 0.05 260);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.22 0.05 260);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.7 0.15 260);
|
||||
--primary-foreground: oklch(0.18 0.04 260);
|
||||
--secondary: oklch(0.28 0.05 260);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.28 0.05 260);
|
||||
--muted-foreground: oklch(0.8 0.05 260);
|
||||
--accent: oklch(0.28 0.05 260);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.6 0.25 25);
|
||||
--border: oklch(0.3 0.05 260);
|
||||
--input: oklch(0.3 0.05 260);
|
||||
--ring: oklch(0.7 0.15 260);
|
||||
}
|
||||
|
||||
[data-theme='sepia'] {
|
||||
--background: oklch(0.96 0.02 85);
|
||||
--foreground: oklch(0.25 0.02 85);
|
||||
--card: oklch(0.98 0.01 85);
|
||||
--card-foreground: oklch(0.25 0.02 85);
|
||||
--popover: oklch(0.98 0.01 85);
|
||||
--popover-foreground: oklch(0.25 0.02 85);
|
||||
--primary: oklch(0.45 0.1 35);
|
||||
--primary-foreground: oklch(0.98 0.01 85);
|
||||
--secondary: oklch(0.92 0.03 85);
|
||||
--secondary-foreground: oklch(0.25 0.02 85);
|
||||
--muted: oklch(0.92 0.03 85);
|
||||
--muted-foreground: oklch(0.5 0.05 85);
|
||||
--accent: oklch(0.92 0.03 85);
|
||||
--accent-foreground: oklch(0.25 0.02 85);
|
||||
--destructive: oklch(0.6 0.2 25);
|
||||
--border: oklch(0.85 0.05 85);
|
||||
--input: oklch(0.85 0.05 85);
|
||||
--ring: oklch(0.45 0.1 35);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
|
||||
@@ -4,6 +4,7 @@ import "./globals.css";
|
||||
import { HeaderWrapper } from "@/components/header-wrapper";
|
||||
import { ToastProvider } from "@/components/ui/toast";
|
||||
import { LabelProvider } from "@/context/LabelContext";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -24,8 +25,15 @@ export default function RootLayout({
|
||||
<body className={inter.className}>
|
||||
<ToastProvider>
|
||||
<LabelProvider>
|
||||
<HeaderWrapper />
|
||||
{children}
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<HeaderWrapper />
|
||||
<div className="flex flex-1">
|
||||
<Sidebar className="shrink-0 border-r" />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</LabelProvider>
|
||||
</ToastProvider>
|
||||
</body>
|
||||
|
||||
11
keep-notes/app/login/page.tsx
Normal file
11
keep-notes/app/login/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from '@/components/login-form';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<main className="flex items-center justify-center md:h-screen">
|
||||
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,14 +5,18 @@ import { useSearchParams } from 'next/navigation'
|
||||
import { Note } from '@/lib/types'
|
||||
import { getNotes, searchNotes } from '@/app/actions/notes'
|
||||
import { NoteInput } from '@/components/note-input'
|
||||
import { NoteGrid } from '@/components/note-grid'
|
||||
import { MasonryGrid } from '@/components/masonry-grid'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
import { useReminderCheck } from '@/hooks/use-reminder-check'
|
||||
|
||||
export default function HomePage() {
|
||||
const searchParams = useSearchParams()
|
||||
const [notes, setNotes] = useState<Note[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const { labels } = useLabels()
|
||||
|
||||
// Enable reminder notifications
|
||||
useReminderCheck(notes)
|
||||
|
||||
useEffect(() => {
|
||||
const loadNotes = async () => {
|
||||
@@ -25,19 +29,19 @@ export default function HomePage() {
|
||||
|
||||
// Filter by selected labels
|
||||
if (labelFilter.length > 0) {
|
||||
allNotes = allNotes.filter(note =>
|
||||
note.labels?.some(label => labelFilter.includes(label))
|
||||
allNotes = allNotes.filter((note: any) =>
|
||||
note.labels?.some((label: string) => labelFilter.includes(label))
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by color (filter notes that have labels with this color)
|
||||
if (colorFilter) {
|
||||
const labelNamesWithColor = labels
|
||||
.filter(label => label.color === colorFilter)
|
||||
.map(label => label.name)
|
||||
.filter((label: any) => label.color === colorFilter)
|
||||
.map((label: any) => label.name)
|
||||
|
||||
allNotes = allNotes.filter(note =>
|
||||
note.labels?.some(label => labelNamesWithColor.includes(label))
|
||||
allNotes = allNotes.filter((note: any) =>
|
||||
note.labels?.some((label: string) => labelNamesWithColor.includes(label))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,7 +58,7 @@ export default function HomePage() {
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-gray-500">Loading...</div>
|
||||
) : (
|
||||
<NoteGrid notes={notes} />
|
||||
<MasonryGrid notes={notes} />
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
|
||||
11
keep-notes/app/register/page.tsx
Normal file
11
keep-notes/app/register/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { RegisterForm } from '@/components/register-form';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<main className="flex items-center justify-center md:h-screen">
|
||||
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
|
||||
<RegisterForm />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user