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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
|
||||
12
src/components/track-note-view.tsx
Normal file
12
src/components/track-note-view.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user