fix: Add debounced Undo/Redo system to avoid character-by-character history
- Add debounced state updates for title and content (500ms delay) - Immediate UI updates with delayed history saving - Prevent one-letter-per-undo issue - Add cleanup for debounce timers on unmount
This commit is contained in:
235
keep-notes/app/actions/notes.ts
Normal file
235
keep-notes/app/actions/notes.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Note, CheckItem } from '@/lib/types'
|
||||
|
||||
// Helper function to parse JSON strings from database
|
||||
function parseNote(dbNote: any): Note {
|
||||
return {
|
||||
...dbNote,
|
||||
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
|
||||
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
|
||||
images: dbNote.images ? JSON.parse(dbNote.images) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get all notes (non-archived by default)
|
||||
export async function getNotes(includeArchived = false) {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: includeArchived ? {} : { isArchived: false },
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error fetching notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Get archived notes only
|
||||
export async function getArchivedNotes() {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { isArchived: true },
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error fetching archived notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Search notes
|
||||
export async function searchNotes(query: string) {
|
||||
try {
|
||||
if (!query.trim()) {
|
||||
return await getNotes()
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
isArchived: false,
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ content: { contains: query, mode: 'insensitive' } }
|
||||
]
|
||||
},
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error searching notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new note
|
||||
export async function createNote(data: {
|
||||
title?: string
|
||||
content: string
|
||||
color?: string
|
||||
type?: 'text' | 'checklist'
|
||||
checkItems?: CheckItem[]
|
||||
labels?: string[]
|
||||
images?: string[]
|
||||
isArchived?: boolean
|
||||
}) {
|
||||
try {
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: data.title || null,
|
||||
content: data.content,
|
||||
color: data.color || 'default',
|
||||
type: data.type || 'text',
|
||||
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
|
||||
labels: data.labels ? JSON.stringify(data.labels) : null,
|
||||
images: data.images ? JSON.stringify(data.images) : null,
|
||||
isArchived: data.isArchived || false,
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return parseNote(note)
|
||||
} catch (error) {
|
||||
console.error('Error creating note:', error)
|
||||
throw new Error('Failed to create note')
|
||||
}
|
||||
}
|
||||
|
||||
// Update a note
|
||||
export async function updateNote(id: string, data: {
|
||||
title?: string | null
|
||||
content?: string
|
||||
color?: string
|
||||
isPinned?: boolean
|
||||
isArchived?: boolean
|
||||
type?: 'text' | 'checklist'
|
||||
checkItems?: CheckItem[] | null
|
||||
labels?: string[] | null
|
||||
images?: string[] | null
|
||||
}) {
|
||||
try {
|
||||
// Stringify JSON fields if they exist
|
||||
const updateData: any = { ...data }
|
||||
if ('checkItems' in data) {
|
||||
updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null
|
||||
}
|
||||
if ('labels' in data) {
|
||||
updateData.labels = data.labels ? JSON.stringify(data.labels) : null
|
||||
}
|
||||
if ('images' in data) {
|
||||
updateData.images = data.images ? JSON.stringify(data.images) : null
|
||||
}
|
||||
updateData.updatedAt = new Date()
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return parseNote(note)
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw new Error('Failed to update note')
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a note
|
||||
export async function deleteNote(id: string) {
|
||||
try {
|
||||
await prisma.note.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
throw new Error('Failed to delete note')
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle pin status
|
||||
export async function togglePin(id: string, isPinned: boolean) {
|
||||
return updateNote(id, { isPinned })
|
||||
}
|
||||
|
||||
// Toggle archive status
|
||||
export async function toggleArchive(id: string, isArchived: boolean) {
|
||||
return updateNote(id, { isArchived })
|
||||
}
|
||||
|
||||
// Update note color
|
||||
export async function updateColor(id: string, color: string) {
|
||||
return updateNote(id, { color })
|
||||
}
|
||||
|
||||
// Update note labels
|
||||
export async function updateLabels(id: string, labels: string[]) {
|
||||
return updateNote(id, { labels })
|
||||
}
|
||||
|
||||
// Get all unique labels
|
||||
export async function getAllLabels() {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
select: { labels: true }
|
||||
})
|
||||
|
||||
const labelsSet = new Set<string>()
|
||||
notes.forEach(note => {
|
||||
const labels = note.labels ? JSON.parse(note.labels) : null
|
||||
if (labels) {
|
||||
labels.forEach((label: string) => labelsSet.add(label))
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(labelsSet).sort()
|
||||
} catch (error) {
|
||||
console.error('Error fetching labels:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder notes (drag and drop)
|
||||
export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
try {
|
||||
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
|
||||
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
|
||||
|
||||
if (!draggedNote || !targetNote) {
|
||||
throw new Error('Notes not found')
|
||||
}
|
||||
|
||||
// Swap the order values
|
||||
await prisma.$transaction([
|
||||
prisma.note.update({
|
||||
where: { id: draggedId },
|
||||
data: { order: targetNote.order }
|
||||
}),
|
||||
prisma.note.update({
|
||||
where: { id: targetId },
|
||||
data: { order: draggedNote.order }
|
||||
})
|
||||
])
|
||||
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error reordering notes:', error)
|
||||
throw new Error('Failed to reorder notes')
|
||||
}
|
||||
}
|
||||
30
keep-notes/app/api/labels/route.ts
Normal file
30
keep-notes/app/api/labels/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/labels - Get all unique labels
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
select: { labels: true }
|
||||
})
|
||||
|
||||
const labelsSet = new Set<string>()
|
||||
notes.forEach(note => {
|
||||
const labels = note.labels ? JSON.parse(note.labels) : null
|
||||
if (labels) {
|
||||
labels.forEach((label: string) => labelsSet.add(label))
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: Array.from(labelsSet).sort()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('GET /api/labels error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch labels' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
100
keep-notes/app/api/notes/[id]/route.ts
Normal file
100
keep-notes/app/api/notes/[id]/route.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// Helper to parse JSON fields
|
||||
function parseNote(dbNote: any) {
|
||||
return {
|
||||
...dbNote,
|
||||
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
|
||||
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/notes/[id] - Get a single note
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: params.id }
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Note not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: parseNote(note)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('GET /api/notes/[id] error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/notes/[id] - Update a note
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const updateData: any = { ...body }
|
||||
|
||||
// Stringify JSON fields if they exist
|
||||
if ('checkItems' in body) {
|
||||
updateData.checkItems = body.checkItems ? JSON.stringify(body.checkItems) : null
|
||||
}
|
||||
if ('labels' in body) {
|
||||
updateData.labels = body.labels ? JSON.stringify(body.labels) : null
|
||||
}
|
||||
updateData.updatedAt = new Date()
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id: params.id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: parseNote(note)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('PUT /api/notes/[id] error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/notes/[id] - Delete a note
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
await prisma.note.delete({
|
||||
where: { id: params.id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Note deleted successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/notes/[id] error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
166
keep-notes/app/api/notes/route.ts
Normal file
166
keep-notes/app/api/notes/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { CheckItem } from '@/lib/types'
|
||||
|
||||
// Helper to parse JSON fields
|
||||
function parseNote(dbNote: any) {
|
||||
return {
|
||||
...dbNote,
|
||||
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
|
||||
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
|
||||
images: dbNote.images ? JSON.parse(dbNote.images) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/notes - Get all notes
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const includeArchived = searchParams.get('archived') === 'true'
|
||||
const search = searchParams.get('search')
|
||||
|
||||
let where: any = {}
|
||||
|
||||
if (!includeArchived) {
|
||||
where.isArchived = false
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ content: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ isPinned: 'desc' },
|
||||
{ order: 'asc' },
|
||||
{ updatedAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: notes.map(parseNote)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('GET /api/notes error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch notes' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/notes - Create a new note
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { title, content, color, type, checkItems, labels, images } = body
|
||||
|
||||
if (!content && type !== 'checklist') {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Content is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: title || null,
|
||||
content: content || '',
|
||||
color: color || 'default',
|
||||
type: type || 'text',
|
||||
checkItems: checkItems ? JSON.stringify(checkItems) : null,
|
||||
labels: labels ? JSON.stringify(labels) : null,
|
||||
images: images ? JSON.stringify(images) : null,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: parseNote(note)
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/notes error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/notes - Update an existing note
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, title, content, color, type, checkItems, labels, isPinned, isArchived, images } = body
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Note ID is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const updateData: any = {}
|
||||
|
||||
if (title !== undefined) updateData.title = title
|
||||
if (content !== undefined) updateData.content = content
|
||||
if (color !== undefined) updateData.color = color
|
||||
if (type !== undefined) updateData.type = type
|
||||
if (checkItems !== undefined) updateData.checkItems = checkItems ? JSON.stringify(checkItems) : null
|
||||
if (labels !== undefined) updateData.labels = labels ? JSON.stringify(labels) : null
|
||||
if (isPinned !== undefined) updateData.isPinned = isPinned
|
||||
if (isArchived !== undefined) updateData.isArchived = isArchived
|
||||
if (images !== undefined) updateData.images = images ? JSON.stringify(images) : null
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id },
|
||||
data: updateData
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: parseNote(note)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('PUT /api/notes error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/notes?id=xxx - Delete a note
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const id = searchParams.get('id')
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Note ID is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await prisma.note.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Note deleted successfully'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/notes error:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
15
keep-notes/app/archive/page.tsx
Normal file
15
keep-notes/app/archive/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getArchivedNotes } from '@/app/actions/notes'
|
||||
import { NoteGrid } from '@/components/note-grid'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function ArchivePage() {
|
||||
const notes = await getArchivedNotes()
|
||||
|
||||
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} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
BIN
keep-notes/app/favicon.ico
Normal file
BIN
keep-notes/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
125
keep-notes/app/globals.css
Normal file
125
keep-notes/app/globals.css
Normal file
@@ -0,0 +1,125 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
28
keep-notes/app/layout.tsx
Normal file
28
keep-notes/app/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Memento - Your Digital Notepad",
|
||||
description: "A beautiful note-taking app inspired by Google Keep, built with Next.js 16",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Header />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
23
keep-notes/app/page.tsx
Normal file
23
keep-notes/app/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getNotes, searchNotes } from '@/app/actions/notes'
|
||||
import { NoteInput } from '@/components/note-input'
|
||||
import { NoteGrid } from '@/components/note-grid'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function HomePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ search?: string }>
|
||||
}) {
|
||||
const params = await searchParams
|
||||
const notes = params.search
|
||||
? await searchNotes(params.search)
|
||||
: await getNotes()
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8 max-w-7xl">
|
||||
<NoteInput />
|
||||
<NoteGrid notes={notes} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user