feat: MVP-3 Sprint 1 - Usage tracking, smart dashboard, scoring boost

## Registro de Uso
- Nuevo modelo NoteUsage en Prisma
- Tipos de eventos: view, search_click, related_click, link_click, copy_command, copy_snippet
- Funciones: trackNoteUsage, getUsageStats, getRecentlyUsedNotes
- localStorage: recentlyViewed (últimas 10 notas)
- Rastreo de copias en markdown-content.tsx

## Dashboard Rediseñado
- 5 bloques: Recientes, Más usadas, Comandos recientes, Snippets recientes, Según actividad
- Nuevo src/lib/dashboard.ts con getDashboardData()
- Recomendaciones basadas en recentlyViewed

## Scoring con Uso Real
- search.ts: +1 per 5 views (max +3), +2 recency boost
- related.ts: mismo sistema de usage boost
- No eclipsa match textual fuerte

## Tests
- 110 tests pasando (usage, dashboard, related, search)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 16:03:14 -03:00
parent cc4b2453b1
commit 05b8f3910d
16 changed files with 2038 additions and 73 deletions

View File

@@ -5,13 +5,25 @@ import { Note } from '@/types/note'
import { NoteList } from './note-list'
import { Button } from '@/components/ui/button'
import { SearchBar } from './search-bar'
import { ArrowRight } from 'lucide-react'
import { ArrowRight, TrendingUp, Terminal, Code, Zap } from 'lucide-react'
export function Dashboard({ recentNotes, favoriteNotes, pinnedNotes }: {
interface DashboardProps {
recentNotes: Note[]
favoriteNotes: Note[]
pinnedNotes: Note[]
}) {
mostUsedNotes: Note[]
recentCommands: Note[]
recentSnippets: Note[]
activityBasedNotes: Note[]
hasActivity: boolean
}
export function Dashboard({
recentNotes,
mostUsedNotes,
recentCommands,
recentSnippets,
activityBasedNotes,
hasActivity,
}: DashboardProps) {
return (
<>
<div className="flex justify-end mb-3">
@@ -19,27 +31,12 @@ export function Dashboard({ recentNotes, favoriteNotes, pinnedNotes }: {
</div>
<div className="space-y-8">
{pinnedNotes.length > 0 && (
<section>
<h2 className="text-xl font-semibold mb-3 flex items-center gap-2">
📌 Pineadas
</h2>
<NoteList notes={pinnedNotes} />
</section>
)}
{favoriteNotes.length > 0 && (
<section>
<h2 className="text-xl font-semibold mb-3 flex items-center gap-2">
Favoritas
</h2>
<NoteList notes={favoriteNotes} />
</section>
)}
{/* Recientes */}
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-semibold">Recientes</h2>
<h2 className="text-xl font-semibold flex items-center gap-2">
<span>Recientes</span>
</h2>
<Link href="/notes">
<Button variant="ghost" size="sm" className="gap-1">
Ver todas <ArrowRight className="h-4 w-4" />
@@ -49,15 +46,89 @@ export function Dashboard({ recentNotes, favoriteNotes, pinnedNotes }: {
{recentNotes.length > 0 ? (
<NoteList notes={recentNotes} />
) : (
<div className="text-center py-8 text-gray-500">
<p>No hay notas todavía.</p>
<Link href="/new">
<Button className="mt-4">Crea tu primera nota</Button>
</Link>
</div>
<EmptyState />
)}
</section>
{/* Más usadas */}
{mostUsedNotes.length > 0 && (
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-semibold flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-orange-500" />
<span>Más usadas</span>
</h2>
<Link href="/notes">
<Button variant="ghost" size="sm" className="gap-1">
Ver todas <ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
<NoteList notes={mostUsedNotes} />
</section>
)}
{/* Comandos recientes */}
{recentCommands.length > 0 && (
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Terminal className="h-5 w-5 text-green-500" />
<span>Comandos recientes</span>
</h2>
<Link href="/notes?type=command">
<Button variant="ghost" size="sm" className="gap-1">
Ver todas <ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
<NoteList notes={recentCommands} />
</section>
)}
{/* Snippets recientes */}
{recentSnippets.length > 0 && (
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Code className="h-5 w-5 text-blue-500" />
<span>Snippets recientes</span>
</h2>
<Link href="/notes?type=snippet">
<Button variant="ghost" size="sm" className="gap-1">
Ver todas <ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
<NoteList notes={recentSnippets} />
</section>
)}
{/* Según tu actividad */}
{hasActivity && activityBasedNotes.length > 0 && (
<section>
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Zap className="h-5 w-5 text-purple-500" />
<span>Según tu actividad</span>
</h2>
</div>
<NoteList notes={activityBasedNotes} />
</section>
)}
</div>
</>
)
}
}
function EmptyState() {
return (
<div className="text-center py-8 text-gray-500">
<p>No hay notas todavía.</p>
<Link href="/new">
<Button className="mt-4">Crea tu primera nota</Button>
</Link>
</div>
)
}

View File

@@ -8,19 +8,24 @@ import { NoteType } from '@/types/note'
import { Copy, Check } from 'lucide-react'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { trackNoteUsage } from '@/lib/usage'
interface MarkdownContentProps {
content: string
className?: string
noteType?: NoteType
noteId?: string
}
function CopyButton({ text }: { text: string }) {
function CopyButton({ text, noteId, eventType }: { text: string; noteId?: string; eventType?: 'copy_command' | 'copy_snippet' }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
if (noteId && eventType) {
trackNoteUsage({ noteId, eventType })
}
setTimeout(() => setCopied(false), 2000)
}
@@ -88,7 +93,7 @@ function ProcedureCheckboxes({ content }: { content: string }) {
)
}
export function MarkdownContent({ content, className = '', noteType }: MarkdownContentProps) {
export function MarkdownContent({ content, className = '', noteType, noteId }: MarkdownContentProps) {
if (noteType === 'procedure') {
return (
<div className={cn("prose max-w-none", className)}>
@@ -118,7 +123,7 @@ export function MarkdownContent({ content, className = '', noteType }: MarkdownC
>
{codeString}
</SyntaxHighlighter>
<CopyButton text={codeString} />
<CopyButton text={codeString} noteId={noteId} eventType="copy_snippet" />
</div>
)
}
@@ -134,7 +139,7 @@ export function MarkdownContent({ content, className = '', noteType }: MarkdownC
>
{codeString}
</SyntaxHighlighter>
<CopyButton text={codeString} />
<CopyButton text={codeString} noteId={noteId} eventType="copy_command" />
</div>
)
}
@@ -157,7 +162,7 @@ export function MarkdownContent({ content, className = '', noteType }: MarkdownC
>
{codeString}
</SyntaxHighlighter>
<CopyButton text={codeString} />
<CopyButton text={codeString} noteId={noteId} eventType="copy_snippet" />
</div>
)
},

View File

@@ -0,0 +1,12 @@
'use client'
import { useEffect } from 'react'
import { addToRecentlyViewed } from '@/lib/usage'
export function TrackNoteView({ noteId }: { noteId: string }) {
useEffect(() => {
addToRecentlyViewed(noteId)
}, [noteId])
return null
}