From 5ad40653abe8767d6cb4fb2ce92cd3bf31343d1d Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 17:51:19 +0000 Subject: [PATCH] feat(CC-089): add Learning Queue page (books/articles/courses/videos with type filter) --- src/app/dashboard/learning-queue/page.tsx | 172 ++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/app/dashboard/learning-queue/page.tsx diff --git a/src/app/dashboard/learning-queue/page.tsx b/src/app/dashboard/learning-queue/page.tsx new file mode 100644 index 0000000..000bb96 --- /dev/null +++ b/src/app/dashboard/learning-queue/page.tsx @@ -0,0 +1,172 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { id: string; title: string; description: string | null; status: string; tags: string[]; priority: string | null; createdAt: string; } + +const LEARN_TAGS = ['learning', 'to-read', 'to-watch', 'to-study', 'curs', 'carte', 'articol', 'video', 'podcast', 'tutorial']; +const TYPE_MAP: Record = { + 'to-read': { label: 'De citit', icon: '📖' }, + carte: { label: 'Carte', icon: '📚' }, + articol: { label: 'Articol', icon: '📰' }, + 'to-watch': { label: 'De urmărit', icon: '🎬' }, + video: { label: 'Video', icon: '🎥' }, + podcast: { label: 'Podcast', icon: '🎙️' }, + curs: { label: 'Curs', icon: '🎓' }, + tutorial: { label: 'Tutorial', icon: '💻' }, + 'to-study': { label: 'Studiu', icon: '🔬' }, + learning: { label: 'Altele', icon: '💡' }, +}; + +export default function LearningQueuePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [typeFilter, setTypeFilter] = useState('all'); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ title: '', url: '', type: 'to-read', priority: 'normal' }); + + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ['learning', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const queue = useMemo(() => + tasks + .filter((t) => t.tags.some((tag) => LEARN_TAGS.includes(tag.toLowerCase()))) + .sort((a, b) => { + const pOrder: Record = { urgent: 0, high: 1, normal: 2, low: 3 }; + return (pOrder[a.priority ?? 'normal'] ?? 2) - (pOrder[b.priority ?? 'normal'] ?? 2); + }), + [tasks]); + + const typeFilter2 = typeFilter === 'all' ? queue : queue.filter((t) => t.tags.some((tag) => tag === typeFilter)); + + const typeCounts = useMemo(() => { + const counts: Record = {}; + for (const t of queue) { + for (const tag of t.tags) { + if (LEARN_TAGS.includes(tag)) { counts[tag] = (counts[tag] ?? 0) + 1; } + } + } + return counts; + }, [queue]); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: form.title + (form.url ? ` — ${form.url}` : ''), + tags: ['learning', form.type], priority: form.priority, status: 'todo', + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['learning', tenantId] }); setShowAdd(false); setForm({ title: '', url: '', type: 'to-read', priority: 'normal' }); }, + }); + + const doneMut = useMutation({ + mutationFn: (id: string) => apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status: 'completed' } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['learning', tenantId] }), + }); + + const pending = queue.filter((t) => t.status !== 'completed' && t.status !== 'cancelled'); + const done = queue.filter((t) => t.status === 'completed'); + + return ( +
+
+
+

Learning Queue

+

+ {pending.length} de parcurs · {done.length} finalizate +

+
+ +
+ + {showAdd && ( +
+

Adaugă în coadă

+ setForm((p) => ({ ...p, title: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, url: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + +
+
+ + +
+
+ )} + + {/* Type filters */} +
+ + {Object.entries(typeCounts).map(([tag, count]) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : typeFilter2.length === 0 ? ( +
+

📚

+

Coada de learning e goală. Adaugă cărți, cursuri, articole.

+
+ ) : ( +
+ {typeFilter2.filter((t) => t.status !== 'completed' && t.status !== 'cancelled').map((t) => { + const typeTag = t.tags.find((tag) => TYPE_MAP[tag]) ?? 'learning'; + const tm = TYPE_MAP[typeTag] ?? { icon: '📌', label: typeTag }; + const pCls: Record = { urgent: 'text-signal-danger', high: 'text-warn', normal: '', low: 'text-ink-faint' }; + return ( +
+ {tm.icon} +
+

{t.title}

+

{tm.label}

+
+ +
+ ); + })} + {done.length > 0 && ( +
+

✓ {done.length} finalizate

+
+ )} +
+ )} +
+ ); +}