/** * Learn — Module-organized content hub * * 10 learning modules sourced from "The Ocean Doesn't Care About Your Swimming Lessons", * Mr. Grumpy's Investing Survival Guide, and three podcast interviews. * Features: attention-grabbing quote rotator, podcast video embeds, book excerpts by topic. */ import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Breadcrumb } from "@/components/Breadcrumb"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { ArrowLeft, BookOpen, ChevronRight, Clock, Play, Waves, Target, Shield, Settings, Building, Brain, AlertTriangle, TrendingUp, Scale, X, ExternalLink, Quote, Sparkles, Video, FileText, ChevronDown, ChevronUp, Volume2, Loader2, CheckCircle2, Bookmark, } from "lucide-react"; import { Link } from "wouter"; import { motion, AnimatePresence } from "framer-motion"; import { LazyStreamdown as Streamdown } from "@/components/LazyStreamdown"; import { SEOHead, pageSEO } from "@/components/SEOHead"; import { BreadcrumbJsonLd, breadcrumbs } from "@/components/BreadcrumbJsonLd"; import { formatReadingTime, getReadingStats, stripMarkdown, calculateReadingTime } from "@/lib/readingTime"; import { AudioPlayer, VoiceSelector } from "@/components/AudioPlayer"; import { BookmarkPopup } from "@/components/BookmarkPopup"; import { BookmarksPanel } from "@/components/BookmarksPanel"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; import { useAuth } from "@/_core/hooks/useAuth"; import { PageTransition } from "@/components/PageTransition"; import { QUERY_CACHE } from "@/lib/queryConfig"; import { EmptyState, ErrorState } from "@/components/EmptyState"; import { ScrollReveal } from "@/components/ScrollReveal"; // ─── Types ─────────────────────────────────────────────────────────────────── interface Module { id: string; number: number; title: string; tagline: string; description: string; icon: string; color: string; } interface ModuleEssay { id: string; title: string; category: string; module: string; description: string; readTime: string; source: string; content: string; } interface PodcastSegment { label: string; startSeconds: number; endSeconds: number; modules: number[]; } interface PodcastVideo { id: string; title: string; youtubeId: string; duration: string; description: string; segments: PodcastSegment[]; } interface InsightQuote { quote: string; source: string; module: number; } interface ModulesData { modules: Module[]; podcastVideos: PodcastVideo[]; insightOfTheDay: InsightQuote[]; } // ─── Icon Map ──────────────────────────────────────────────────────────────── const iconMap: Record = { Waves, Clock, Scale, TrendingUp, Target, Shield, Settings, Building, Brain, AlertTriangle, }; // ─── Helper: format seconds to mm:ss ───────────────────────────────────────── function formatTime(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; return `${m}:${String(s).padStart(2, "0")}`; } // ─── Quote Rotator ─────────────────────────────────────────────────────────── function QuoteRotator({ quotes, modules }: { quotes: InsightQuote[]; modules: Module[] }) { const [idx, setIdx] = useState(() => Math.floor(Math.random() * quotes.length)); useEffect(() => { const timer = setInterval(() => { setIdx((prev) => (prev + 1) % quotes.length); }, 8000); return () => clearInterval(timer); }, [quotes.length]); const q = quotes[idx]; const mod = modules.find((m) => m.number === q.module); return (
{/* Decorative */}

"{q.quote}"

{q.source} {mod && ( <> · Module {mod.number} )}
{/* Dots */}
{quotes.slice(0, Math.min(quotes.length, 20)).map((_, i) => (
); } // ─── YouTube Embed ─────────────────────────────────────────────────────────── function YouTubeEmbed({ videoId, startSeconds, title, }: { videoId: string; startSeconds?: number; title: string; }) { const src = startSeconds ? `https://www.youtube.com/embed/${videoId}?start=${startSeconds}&rel=0` : `https://www.youtube.com/embed/${videoId}?rel=0`; return (