initial commit

This commit is contained in:
Nice Try
2026-06-26 02:51:08 +00:00
commit ad5810fba9
133 changed files with 28275 additions and 0 deletions
+424
View File
@@ -0,0 +1,424 @@
import type { Metadata } from 'next'
import type { ComponentType } from 'react'
import { Badge } from '@/components/ui/badge'
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import {
BadgeCheck,
BookOpen,
Bookmark,
CheckCircle2,
Clock3,
Database,
Download,
FileText,
Gauge,
HelpCircle,
Keyboard,
Layers,
Laptop,
Package,
PictureInPicture2,
Play,
ShieldCheck,
Smartphone,
Tag,
Upload,
Zap,
} from 'lucide-react'
export const metadata: Metadata = {
title: 'Progress',
description: 'Internal product status board for OfflineAcademy.',
}
type Status = 'Live' | 'Planned' | 'Future'
type FeatureGroup = {
title: string
status: Status
icon: ComponentType<{ className?: string }>
items: string[]
completion: number
}
type RoadmapItem = {
label: string
done: boolean
}
type RoadmapPhase = {
phase: string
title: string
items: RoadmapItem[]
completion: number
}
const liveGroups: FeatureGroup[] = [
{
title: 'Library & Scanning',
status: 'Live',
icon: Database,
completion: 100,
items: [
'Recursive scan of a local course root directory',
'Course, module, and lesson discovery',
'Natural sort for lesson ordering',
'Folder-driven import with local file access',
],
},
{
title: 'Playback & Resume',
status: 'Live',
icon: Play,
completion: 100,
items: [
'Lesson playback inside the app',
'Continue Watching and resume playback',
'Auto-next / playback flow support',
'Native HTML5 video player for speed and simplicity',
],
},
{
title: 'Playback Enhancements',
status: 'Live',
icon: Zap,
completion: 100,
items: [
'Variable playback speed (0.75x, 1x, 1.25x, 1.5x, 2.0x)',
'Global keyboard shortcuts (play/pause, seek, mute, fullscreen, speed, PiP, Escape)',
'Picture-in-Picture toggle',
],
},
{
title: 'Learning Tools',
status: 'Live',
icon: Bookmark,
completion: 100,
items: [
'Bookmarks with timestamp notes',
'Jump-to-time bookmark playback',
'Subtitle support for .srt and .vtt',
'Inline document viewing for lessons',
],
},
{
title: 'Course Management',
status: 'Live',
icon: ShieldCheck,
completion: 100,
items: [
'Persistent rename titles',
'Delete from library',
'Delete from disk',
'Archived/hidden courses stay hidden on rescans',
],
},
{
title: 'Progress Tracking UI',
status: 'Live',
icon: Gauge,
completion: 100,
items: [
'Progress bars on course cards',
'Lesson/module manual Done toggles',
'Local analytics dashboard (`/analytics`) with watch time, completion rates, weekly activity',
],
},
{
title: 'Built-in Quiz Interactivity',
status: 'Live',
icon: HelpCircle,
completion: 100,
items: [
'Quiz settings: Auto-Fetch toggle + QuizAPI / The Trivia API source selector',
'QuizPlayer: intro, active questions, results, 80% pass mark, ⚙️ Edit Topic override',
'Auto-fetch writes quiz_cache.json to module folders on module open',
'Scanner injects Quiz lesson type (🟣) into module sidebar',
'Quiz, Question, QuizAttempt models; score/passed tracked; green checkmark on completion',
],
},
{
title: 'Library Organization',
status: 'Live',
icon: Tag,
completion: 100,
items: [
'Custom tags & categories with color (Tag + CourseTag models)',
'Filter by tag, favorites, in-progress, completed, not-started',
'Sort by name, progress %, updatedAt; favorites pinned to top',
],
},
{
title: 'Content Types & Thumbnails',
status: 'Live',
icon: FileText,
completion: 100,
items: [
'PDF, TXT, MD, HTML, and JSON inline preview',
'Local thumbnail support',
'Safer thumbnail lookup to reduce broken image noise',
'Watch-page rendering for mixed lesson types',
],
},
{
title: 'Settings & Data Model',
status: 'Live',
icon: Laptop,
completion: 100,
items: [
'Configured course root directory',
'SQLite + Prisma local persistence',
'Progress and bookmark tables',
'No login, no accounts, no cloud storage',
],
},
]
const roadmapPhases: RoadmapPhase[] = [
{
phase: 'Phase 5',
title: 'Data Portability',
completion: 0,
items: [
{ label: 'Export local state to JSON (progress, bookmarks, notes, tags, course metadata, quiz attempts)', done: false },
{ label: 'Import and restore JSON state safely', done: false },
],
},
]
const futureIdeas = [
{
title: 'Reset Course',
icon: Gauge,
note: 'Clear progress and start a course from zero.',
},
{
title: 'Mobile App Wrapper',
icon: Smartphone,
note: 'Lightweight mobile wrapper or app packaging layer.',
},
{
title: 'Import / Export Course Metadata',
icon: Package,
note: 'Move course metadata in and out without touching core progress state.',
},
]
const stackBadges = [
'Next.js 14',
'React',
'TypeScript',
'Tailwind',
'shadcn/ui',
'Prisma',
'SQLite',
'Native Video',
]
const currentCompletion = Math.round((liveGroups.filter((group) => group.completion === 100).length / liveGroups.length) * 100)
export default function ProgressPage() {
return (
<main className="min-h-screen bg-background text-foreground">
<div className="container mx-auto px-4 py-8 sm:py-10 max-w-7xl">
<section className="glass-card rounded-2xl p-6 sm:p-8 mb-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-3xl">
<div className="flex flex-wrap items-center gap-2 mb-4">
<Badge variant="secondary" className="gap-1.5">
<ShieldCheck className="h-3.5 w-3.5" />
Internal route
</Badge>
<Badge variant="secondary" className="gap-1.5">
<Database className="h-3.5 w-3.5" />
Local-only
</Badge>
<Badge variant="secondary" className="gap-1.5">
<Zap className="h-3.5 w-3.5" />
Fast stack
</Badge>
</div>
<h1 className="text-3xl sm:text-5xl font-bold tracking-tight mb-3 text-gradient-primary">
OfflineAcademy Progress
</h1>
<p className="text-base sm:text-lg text-muted-foreground max-w-2xl">
A private status board for the apps live features, roadmap, and future ideas.
This page is intentionally not linked from the dashboard its an internal route
you can visit directly at <code className="bg-muted px-1.5 py-0.5 rounded">/progress</code>.
</p>
<div className="flex flex-wrap gap-2 mt-5">
{stackBadges.map((item) => (
<Badge key={item} variant="outline" className="bg-secondary/60">
{item}
</Badge>
))}
</div>
</div>
<Card className="w-full lg:w-[360px] glass-card-elevated">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Gauge className="h-5 w-5 text-primary" />
Current Build Progress
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-muted-foreground">Live core complete</span>
<span className="font-semibold text-primary">{currentCompletion}%</span>
</div>
<Progress value={currentCompletion} className="h-3 gradient-progress" />
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="rounded-xl border border-white/5 bg-secondary/30 p-3">
<div className="text-muted-foreground">Live feature groups</div>
<div className="text-2xl font-bold mt-1">{liveGroups.length}</div>
</div>
<div className="rounded-xl border border-white/5 bg-secondary/30 p-3">
<div className="text-muted-foreground">Roadmap phases</div>
<div className="text-2xl font-bold mt-1">{roadmapPhases.length}</div>
</div>
</div>
</CardContent>
</Card>
</div>
</section>
<section className="grid gap-6 lg:grid-cols-2">
<Card className="glass-card-elevated">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<CheckCircle2 className="h-5 w-5 text-primary" />
Whats Live Today
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
{liveGroups.map((group) => {
const Icon = group.icon
return (
<div key={group.title} className="rounded-xl border border-white/5 bg-secondary/30 p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-2">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/15 text-primary">
<Icon className="h-5 w-5" />
</span>
<div>
<h3 className="font-semibold">{group.title}</h3>
<p className="text-xs text-muted-foreground">{group.status}</p>
</div>
</div>
<Badge variant="secondary">{group.completion}%</Badge>
</div>
<Progress value={group.completion} className="h-2 gradient-progress" />
<ul className="mt-3 space-y-2 text-sm text-muted-foreground">
{group.items.map((item) => (
<li key={item} className="flex gap-2">
<BadgeCheck className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
<span>{item}</span>
</li>
))}
</ul>
</div>
)
})}
</CardContent>
</Card>
<Card className="glass-card-elevated">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<Layers className="h-5 w-5 text-accent" />
Roadmap Phases
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{roadmapPhases.map((phase) => (
<div key={phase.title} className="rounded-xl border border-white/5 bg-secondary/30 p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div>
<div className="text-xs uppercase tracking-[0.22em] text-muted-foreground">{phase.phase}</div>
<h3 className="font-semibold text-lg">{phase.title}</h3>
</div>
<Badge variant="outline">{phase.completion}%</Badge>
</div>
<Progress value={phase.completion} className="h-2 gradient-progress-accent" />
<ul className="mt-3 space-y-2 text-sm text-muted-foreground">
{phase.items.map((item) => (
<li key={item.label} className="flex gap-2">
{item.done ? (
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
) : (
<Clock3 className="mt-0.5 h-4 w-4 shrink-0 text-accent" />
)}
<span className={item.done ? 'text-foreground' : ''}>{item.label}</span>
</li>
))}
</ul>
</div>
))}
</CardContent>
</Card>
</section>
<section className="mt-6 grid gap-6 lg:grid-cols-2">
<Card className="glass-card">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<Tag className="h-5 w-5 text-primary" />
Future Ideas
</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2">
{futureIdeas.map((idea) => {
const Icon = idea.icon
return (
<div key={idea.title} className="rounded-xl border border-white/5 bg-secondary/25 p-4">
<div className="flex items-center gap-2 mb-2">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-accent/15 text-accent">
<Icon className="h-5 w-5" />
</span>
<h3 className="font-semibold">{idea.title}</h3>
</div>
<p className="text-sm text-muted-foreground">{idea.note}</p>
</div>
)
})}
</CardContent>
</Card>
<Card className="glass-card">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xl">
<BookOpen className="h-5 w-5 text-primary" />
Product Constraints
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<p className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />No authentication, no accounts, no user profiles.</p>
<p className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />Everything stays local: folder scanning, SQLite, and file serving.</p>
<p className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />The app is built to stay fast as more courses are added.</p>
<p className="flex gap-2"><CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />This route is a direct internal progress page, not a dashboard menu item.</p>
<div className="pt-3 flex flex-wrap gap-2">
<Badge variant="secondary" className="gap-1.5"><Zap className="h-3.5 w-3.5" />Playback speed & shortcuts: Live</Badge>
<Badge variant="secondary" className="gap-1.5"><PictureInPicture2 className="h-3.5 w-3.5" />PiP: Live</Badge>
<Badge variant="secondary" className="gap-1.5"><HelpCircle className="h-3.5 w-3.5" />Quizzes: Live</Badge>
<Badge variant="secondary" className="gap-1.5"><Download className="h-3.5 w-3.5" />Export planned</Badge>
<Badge variant="secondary" className="gap-1.5"><Upload className="h-3.5 w-3.5" />Import planned</Badge>
</div>
</CardContent>
</Card>
</section>
</div>
</main>
)
}