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>
)
}