mvp
This commit is contained in:
93
src/app/notes/[id]/page.tsx
Normal file
93
src/app/notes/[id]/page.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { RelatedNotes } from '@/components/related-notes'
|
||||
import { getRelatedNotes } from '@/lib/related'
|
||||
import { MarkdownContent } from '@/components/markdown-content'
|
||||
import { DeleteNoteButton } from '@/components/delete-note-button'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ArrowLeft, Edit, Heart, Pin } from 'lucide-react'
|
||||
import { NoteType } from '@/types/note'
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
command: 'bg-green-100 text-green-800',
|
||||
snippet: 'bg-blue-100 text-blue-800',
|
||||
decision: 'bg-purple-100 text-purple-800',
|
||||
recipe: 'bg-orange-100 text-orange-800',
|
||||
procedure: 'bg-yellow-100 text-yellow-800',
|
||||
inventory: 'bg-gray-100 text-gray-800',
|
||||
note: 'bg-slate-100 text-slate-800',
|
||||
}
|
||||
|
||||
export default async function NoteDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id },
|
||||
include: { tags: { include: { tag: true } } },
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const related = await getRelatedNotes(id, 5)
|
||||
const noteType = note.type as NoteType
|
||||
|
||||
return (
|
||||
<main className="container mx-auto py-8 px-4 max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<Link href="/notes">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" /> Volver
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">{note.title}</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className={typeColors[noteType] || typeColors.note}>
|
||||
{noteType}
|
||||
</Badge>
|
||||
{note.isFavorite && <Heart className="h-5 w-5 text-pink-500 fill-pink-500" />}
|
||||
{note.isPinned && <Pin className="h-5 w-5 text-amber-500" />}
|
||||
<span className="text-sm text-gray-500">
|
||||
Actualizada: {new Date(note.updatedAt).toLocaleDateString('en-CA')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/edit/${note.id}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Edit className="h-4 w-4 mr-1" /> Editar
|
||||
</Button>
|
||||
</Link>
|
||||
<DeleteNoteButton noteId={note.id} noteTitle={note.title} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{note.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{note.tags.map(({ tag }) => (
|
||||
<Link key={tag.id} href={`/notes?tag=${tag.name}`}>
|
||||
<Badge variant="outline" className="cursor-pointer hover:bg-gray-100">
|
||||
{tag.name}
|
||||
</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
<MarkdownContent content={note.content} className="bg-gray-50 p-4 rounded-lg border" />
|
||||
</div>
|
||||
|
||||
{related.length > 0 && (
|
||||
<RelatedNotes notes={related} />
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
92
src/app/notes/page.tsx
Normal file
92
src/app/notes/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { NoteList } from '@/components/note-list'
|
||||
import { SearchBar } from '@/components/search-bar'
|
||||
import { TagFilter } from '@/components/tag-filter'
|
||||
import { NoteType } from '@/types/note'
|
||||
|
||||
const NOTE_TYPES: NoteType[] = ['command', 'snippet', 'decision', 'recipe', 'procedure', 'inventory', 'note']
|
||||
|
||||
interface SearchParams {
|
||||
q?: string
|
||||
type?: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
async function searchNotes(searchParams: SearchParams) {
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (searchParams.q) {
|
||||
where.OR = [
|
||||
{ title: { contains: searchParams.q } },
|
||||
{ content: { contains: searchParams.q } },
|
||||
]
|
||||
}
|
||||
|
||||
if (searchParams.type && NOTE_TYPES.includes(searchParams.type as NoteType)) {
|
||||
where.type = searchParams.type
|
||||
}
|
||||
|
||||
if (searchParams.tag) {
|
||||
where.tags = {
|
||||
some: {
|
||||
tag: { name: searchParams.tag },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where,
|
||||
include: { tags: { include: { tag: true } } },
|
||||
orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
|
||||
})
|
||||
|
||||
return notes
|
||||
}
|
||||
|
||||
async function getAllTags() {
|
||||
const tags = await prisma.tag.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
return tags.map((t) => t.name)
|
||||
}
|
||||
|
||||
export default async function NotesPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
|
||||
const params = await searchParams
|
||||
const [notes, tags] = await Promise.all([searchNotes(params), getAllTags()])
|
||||
|
||||
const notesWithTags = notes.map(note => ({
|
||||
...note,
|
||||
type: note.type as NoteType,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
updatedAt: note.updatedAt.toISOString(),
|
||||
tags: note.tags.map(nt => ({ tag: nt.tag })),
|
||||
}))
|
||||
|
||||
const hasFilters = params.q || params.type || params.tag
|
||||
|
||||
return (
|
||||
<main className="container mx-auto py-8 px-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{hasFilters ? 'Resultados de búsqueda' : 'Todas las notas'}
|
||||
</h1>
|
||||
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center w-full sm:w-auto">
|
||||
<div className="w-full sm:w-auto">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<TagFilter tags={tags} selectedTag={params.tag || null} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasFilters && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{params.q && <span className="text-sm">Búsqueda: "{params.q}"</span>}
|
||||
{params.type && <span className="text-sm">Tipo: {params.type}</span>}
|
||||
{params.tag && <span className="text-sm">Tag: {params.tag}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NoteList notes={notesWithTags} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user