| import { useEffect, useState, useMemo } from 'react'; |
| import { motion, useScroll, useSpring } from 'motion/react'; |
| import { ChevronLeft, Loader2, Book, Clock, HelpCircle, Bot, Sparkles, Table2 } from 'lucide-react'; |
| import ReactMarkdown from 'react-markdown'; |
| import remarkGfm from 'remark-gfm'; |
| import rehypeHighlight from 'rehype-highlight'; |
| import { fetchDBMSTopicContent, type DBMSTopicContent } from '@/lib/dbmsClient'; |
| import { fixMarkdownBold, cleanMarkdown, processAsciiTableToMarkdown } from '@/lib/markdownUtils'; |
| import 'highlight.js/styles/github-dark.css'; |
| import { cn } from '@/lib/utils'; |
| import DBMSArchitectureDiagram from './DBMSArchitectureDiagram'; |
| import DBMSAdvantagesDiagram from './DBMSAdvantagesDiagram'; |
| import DBMSTierArchitectureDiagram from './DBMSTierArchitectureDiagram'; |
| import DBMSTypesDiagram from './DBMSTypesDiagram'; |
| import ERModelDiagram from './ERModelDiagram'; |
| import OODMDiagram from './OODMDiagram'; |
| import RelationalModelDiagram from './RelationalModelDiagram'; |
| import DataAbstractionLevelsDiagram from './DataAbstractionLevelsDiagram'; |
| import SQLTableOutput from './SQLTableOutput'; |
| import RelationalTableConceptsDiagram from './RelationalTableConceptsDiagram'; |
| import ConstraintsDiagram from './ConstraintsDiagram'; |
| import DenormalizationDiagram from './DenormalizationDiagram'; |
| import KeysInDBMSDiagram from './KeysInDBMSDiagram'; |
| import NormalizationDiagram from './NormalizationDiagram'; |
| import RelationshipsInDBMSDiagram from './RelationshipsInDBMSDiagram'; |
| import ACIDTransactionsDiagram from './ACIDTransactionsDiagram'; |
| import IndexingDiagram from './IndexingDiagram'; |
| import JoinsDiagram from './JoinsDiagram'; |
| import ViewsTriggersDiagram from './ViewsTriggersDiagram'; |
| import ConcurrencyControlDiagram from './ConcurrencyControlDiagram'; |
|
|
| interface DBMSContentPageProps { |
| topicNo: number; |
| onBack: () => void; |
| } |
|
|
| interface QABlock { |
| id: string; |
| question: string; |
| answerMarkdown: string; |
| } |
|
|
|
|
|
|
| |
| |
| |
| |
|
|
| function parseDBMSContent(raw: string): QABlock[] { |
| let cleaned = raw.replace(/	/g, '').replace(/\\&/g, '&'); |
| cleaned = fixMarkdownBold(cleaned); |
| cleaned = processAsciiTableToMarkdown(cleaned); |
| |
| |
| const chunks = cleaned.split(/\*\*Q[:\.\-]?\s*/i); |
| |
| const blocks: QABlock[] = []; |
| |
| chunks.forEach((chunk, index) => { |
| if (!chunk.trim()) return; |
| |
| const parts = chunk.split(/\*\*/); |
| let question = parts[0].trim(); |
| let answer = parts.slice(1).join('**').trim(); |
| |
| |
| answer = answer.replace(/^\s*\**\s*Ans(?:wer)?\s*[:\-\.]?\s*\**\s*/i, '').trim(); |
| |
| |
| answer = answer.replace(/^[_*]+\s*?\n+/g, '').trim(); |
| answer = answer.replace(/^_\*\*\s*/g, '').trim(); |
|
|
| |
| answer = answer.replace(/^(?:\*\*|)(\d+[.\)]?\s*.+?)(?:--\*\*|\*\*)\s*$/gm, '$1').trim(); |
| |
| |
| answer = answer.replace(/^(\d+)[\\\.]\s*(.+)$/gm, '### $1. $2'); |
|
|
| |
| answer = cleanMarkdown(answer); |
| |
| blocks.push({ |
| id: `qa-${index}`, |
| question, |
| answerMarkdown: answer |
| }); |
| }); |
| |
| return blocks; |
| } |
|
|
| export default function DBMSContentPage({ topicNo, onBack }: DBMSContentPageProps) { |
| const [content, setContent] = useState<DBMSTopicContent | null>(null); |
| const [loading, setLoading] = useState(true); |
| const [error, setError] = useState<string | null>(null); |
| const { scrollYProgress } = useScroll(); |
| const scaleX = useSpring(scrollYProgress, { |
| stiffness: 100, |
| damping: 30, |
| restDelta: 0.001 |
| }); |
|
|
| useEffect(() => { |
| const loadContent = async () => { |
| setLoading(true); |
| setError(null); |
| try { |
| const data = await fetchDBMSTopicContent(topicNo); |
| setContent(data); |
| } catch (err) { |
| console.error('[DBMSContentPage] fetch error:', err); |
| setError('Failed to load topic content. Please try again.'); |
| } finally { |
| setLoading(false); |
| } |
| }; |
| void loadContent(); |
| window.scrollTo(0, 0); |
| }, [topicNo]); |
|
|
| const qaBlocks = useMemo(() => { |
| if (!content?.content) return []; |
| return parseDBMSContent(content.content); |
| }, [content]); |
|
|
| if (loading) { |
| return ( |
| <div className="flex h-[80vh] flex-col items-center justify-center gap-4"> |
| <Loader2 className="h-10 w-10 animate-spin text-indigo-500" /> |
| <span className="text-sm font-medium text-slate-400">Fetching topic content...</span> |
| </div> |
| ); |
| } |
|
|
| if (error || !content) { |
| return ( |
| <div className="mx-auto max-w-2xl py-20 text-center"> |
| <div className="rounded-2xl border border-red-500/20 bg-red-500/5 p-8 shadow-2xl"> |
| <p className="text-sm font-medium text-red-400">{error || 'Topic not found.'}</p> |
| <button |
| onClick={onBack} |
| className="mt-6 inline-flex items-center gap-2 text-sm font-bold text-slate-400 hover:text-white transition-colors" |
| > |
| <ChevronLeft size={16} /> Back to Topics |
| </button> |
| </div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="relative min-h-screen pb-32"> |
| {/* Top Reading Progress Bar */} |
| <motion.div |
| className="fixed left-0 right-0 top-0 z-50 h-1.5 origin-left bg-gradient-to-r from-indigo-500 via-blue-500 to-cyan-400" |
| style={{ scaleX }} |
| /> |
| |
| {/* Floating Back Button (Desktop) */} |
| <div className="fixed left-8 top-8 z-40 hidden lg:block"> |
| <button |
| onClick={onBack} |
| className="group flex h-12 w-12 items-center justify-center rounded-full border border-white/10 bg-zinc-950/80 text-slate-400 shadow-xl backdrop-blur-md transition-all hover:scale-110 hover:border-indigo-500/50 hover:bg-indigo-500/10 hover:text-indigo-400" |
| title="Back to Topics" |
| > |
| <ChevronLeft size={20} className="transition-transform group-hover:-translate-x-0.5" /> |
| </button> |
| </div> |
| |
| <div className="mx-auto max-w-[1150px] px-4 md:px-8"> |
| |
| {/* Sticky Header */} |
| <div className="sticky top-0 z-30 -mx-4 mb-8 px-4 py-4 md:-mx-8 md:px-8 backdrop-blur-xl bg-zinc-950/70 border-b border-white/5 shadow-[0_10px_30px_-10px_rgba(0,0,0,0.5)]"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-4"> |
| <button |
| onClick={onBack} |
| className="lg:hidden flex h-10 w-10 items-center justify-center rounded-full bg-white/5 text-slate-400 hover:bg-white/10 hover:text-white" |
| > |
| <ChevronLeft size={18} /> |
| </button> |
| <div> |
| <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-indigo-400/80"> |
| <span>Topic #{content.topic_no}</span> |
| <span className="h-1 w-1 rounded-full bg-indigo-500/50" /> |
| <span>DBMS Fundamentals</span> |
| </div> |
| <h1 className="mt-1 text-lg font-bold text-white line-clamp-1">{content.topic_name}</h1> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Title Section */} |
| <motion.div |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="mb-16 mt-8" |
| > |
| <h1 className="text-4xl font-black tracking-tight text-white sm:text-5xl leading-[1.15]"> |
| {content.topic_name} |
| </h1> |
| <div className="mt-6 flex flex-wrap items-center gap-4 text-sm font-medium text-slate-500"> |
| <span className="flex items-center gap-1.5"> |
| <Clock size={16} /> ~{Math.max(1, Math.ceil(content.content.length / 1500))} min read |
| </span> |
| <span className="flex items-center gap-1.5"> |
| <Book size={16} /> {qaBlocks.length} Sections |
| </span> |
| </div> |
| </motion.div> |
| |
| {/* Q&A Blocks */} |
| <div className="space-y-12"> |
| {qaBlocks.map((block, idx) => { |
| return ( |
| <motion.div |
| key={block.id} |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ delay: idx * 0.1 }} |
| className="overflow-hidden rounded-3xl border border-white/5 bg-zinc-950 shadow-2xl shadow-black/50" |
| > |
| {/* Question Header */} |
| {block.question && ( |
| <div className="relative border-b border-indigo-500/20 bg-gradient-to-r from-indigo-900/40 to-blue-900/20 px-6 py-8 sm:px-10 overflow-hidden group/header cursor-default"> |
| <div className="absolute inset-0 bg-indigo-500/0 transition-colors duration-500 group-hover/header:bg-indigo-500/10" /> |
| <div className="flex items-start gap-4 relative z-10"> |
| <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-indigo-500 shadow-lg shadow-indigo-500/30 transition-transform duration-500 group-hover/header:scale-110 group-hover/header:rotate-[10deg]"> |
| <HelpCircle size={22} className="text-white" /> |
| </div> |
| <h2 className="mt-1 text-2xl font-bold leading-snug text-white sm:text-3xl transition-all duration-500 group-hover/header:text-indigo-100 group-hover/header:drop-shadow-[0_0_10px_rgba(129,140,248,0.3)]"> |
| {block.question} |
| </h2> |
| </div> |
| </div> |
| )} |
| |
| {/* Answer Body */} |
| <div className={cn("px-6 sm:px-10", block.question ? "py-8" : "py-10")}> |
| <div className="prose prose-invert max-w-none |
| prose-p:text-[17px] prose-p:leading-[1.75] prose-p:text-slate-300 prose-p:mb-6 |
| prose-strong:text-indigo-300 prose-strong:font-bold |
| prose-ul:my-6 prose-ul:text-slate-300 prose-li:my-2 prose-li:leading-relaxed |
| "> |
| <ReactMarkdown |
| remarkPlugins={[remarkGfm]} |
| rehypePlugins={[rehypeHighlight]} |
| components={{ |
| p: ({ node, children, ...props }) => { |
| const text = String(children); |
| // Check if this is a Rule paragraph |
| if (/^\*\*Rule \d+:/.test(text)) { |
| return ( |
| <p className="my-4 flex items-start gap-3 text-[15px] font-semibold text-indigo-200" {...props}> |
| <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg bg-indigo-500/20 text-indigo-300 text-xs font-bold"> |
| {text.match(/\d+/)?.[0]} |
| </span> |
| <span className="mt-0.5">{text.replace(/^\*\*Rule \d+:\*\*\s*/, '')}</span> |
| </p> |
| ); |
| } |
| return <p className="transition-all duration-300 hover:text-slate-200" {...props}>{children}</p>; |
| }, |
| strong: ({ node, ...props }) => ( |
| <strong className="font-bold text-indigo-300 transition-all duration-300 hover:text-indigo-200 hover:drop-shadow-[0_0_8px_rgba(165,180,252,0.5)] cursor-default" {...props} /> |
| ), |
| a: ({ node, ...props }) => ( |
| <a className="font-semibold text-indigo-400 underline decoration-indigo-500/30 underline-offset-4 transition-all duration-300 hover:text-indigo-300 hover:decoration-indigo-400 hover:drop-shadow-[0_0_8px_rgba(129,140,248,0.5)]" {...props} /> |
| ), |
| em: ({ node, ...props }) => ( |
| <em className="italic text-indigo-200/80 transition-colors duration-300 hover:text-indigo-100" {...props} /> |
| ), |
| h3: ({ node, ...props }) => ( |
| <h3 className="group mt-10 mb-6 flex items-center gap-3 text-[19px] font-bold text-white transition-all duration-300 hover:text-indigo-300 hover:translate-x-2 cursor-default before:h-6 before:w-1.5 before:rounded-full before:bg-indigo-500 before:transition-all before:duration-300 hover:before:bg-indigo-400 hover:before:shadow-[0_0_12px_rgba(99,102,241,0.8)] hover:before:scale-y-125" {...props} /> |
| ), |
| h4: ({ node, ...props }) => ( |
| <h4 className="mt-8 mb-4 text-[15px] font-black uppercase tracking-wider text-indigo-500/80 transition-all duration-300 hover:text-indigo-400 hover:tracking-[0.1em] cursor-default" {...props} /> |
| ), |
| code: ({ node, className, children, ...props }) => { |
| const match = /language-(\w+)/.exec(className || ''); |
| const isInline = !match && !String(children).includes('\n'); |
| if (isInline) { |
| return <code className="rounded bg-indigo-500/15 px-1.5 py-0.5 font-mono text-[14px] text-indigo-300" {...props}>{children}</code>; |
| } |
| return ( |
| <div className="my-6 overflow-hidden rounded-2xl border border-white/10 bg-[#0d1117] shadow-xl transition-all duration-500 hover:border-indigo-500/30 hover:shadow-[0_8px_40px_rgba(99,102,241,0.15)]"> |
| <div className="flex items-center justify-between border-b border-white/5 bg-white/[0.02] px-4 py-2"> |
| <span className="text-[10px] font-bold uppercase tracking-wider text-slate-500">{match?.[1] || 'Code'}</span> |
| </div> |
| <div className="overflow-x-auto p-4 text-[13px] leading-relaxed"> |
| <code className={className} {...props}> |
| {children} |
| </code> |
| </div> |
| </div> |
| ); |
| }, |
| table: ({ node, ...props }) => ( |
| <motion.div |
| initial={{ opacity: 0, y: 24, scale: 0.98 }} |
| whileInView={{ opacity: 1, y: 0, scale: 1 }} |
| viewport={{ once: true, margin: "-60px" }} |
| transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }} |
| className="my-10 w-full overflow-hidden rounded-2xl border border-indigo-500/20 bg-gradient-to-b from-[#0f1219] to-[#0a0b10] shadow-[0_8px_40px_rgba(99,102,241,0.12)] relative group/table transition-all duration-500 hover:border-indigo-500/40 hover:shadow-[0_16px_60px_rgba(99,102,241,0.2)]" |
| > |
| {/* Glowing top accent */} |
| <div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-indigo-500 to-transparent opacity-60 group-hover/table:opacity-100 transition-opacity duration-500" /> |
| {/* Table title bar */} |
| <div className="flex items-center gap-2.5 border-b border-white/[0.06] bg-white/[0.02] px-5 py-3"> |
| <Table2 size={14} className="text-indigo-400" /> |
| <span className="text-[11px] font-black uppercase tracking-[0.15em] text-indigo-400/80">Data Table</span> |
| <div className="ml-auto flex gap-1.5"> |
| <div className="h-2 w-2 rounded-full bg-indigo-500/30" /> |
| <div className="h-2 w-2 rounded-full bg-purple-500/30" /> |
| <div className="h-2 w-2 rounded-full bg-blue-500/30" /> |
| </div> |
| </div> |
| <div className="overflow-x-auto"> |
| <table className="w-full border-collapse text-left text-[14px]" {...props} /> |
| </div> |
| </motion.div> |
| ), |
| thead: ({ node, ...props }) => ( |
| <thead className="bg-gradient-to-r from-indigo-500/[0.08] via-purple-500/[0.06] to-indigo-500/[0.08] border-b-2 border-indigo-500/20" {...props} /> |
| ), |
| th: ({ node, ...props }) => ( |
| <th className="px-5 py-4 text-[11.5px] font-black uppercase tracking-[0.12em] text-indigo-300 whitespace-nowrap" {...props} /> |
| ), |
| tbody: ({ node, ...props }) => <tbody className="divide-y divide-white/[0.05]" {...props} />, |
| tr: ({ node, ...props }) => ( |
| <motion.tr |
| initial={{ opacity: 0, x: -12 }} |
| whileInView={{ opacity: 1, x: 0 }} |
| viewport={{ once: true }} |
| transition={{ duration: 0.35, ease: "easeOut" }} |
| className="group/row relative transition-all duration-300 hover:bg-indigo-500/[0.07] even:bg-white/[0.015]" |
| {...(props as any)} |
| /> |
| ), |
| td: ({ node, children, ...props }) => { |
| const text = String(children ?? '').trim(); |
| const isNA = text === '' || text === 'N/A'; |
| return ( |
| <td className={cn( |
| "px-5 py-4 align-top leading-relaxed transition-colors duration-300 group-hover/row:text-indigo-100 text-[13.5px]", |
| isNA ? "text-zinc-600 italic" : "text-slate-300" |
| )} {...props}> |
| {isNA ? <span className="opacity-50">N/A</span> : children} |
| </td> |
| ); |
| }, |
| blockquote: ({ node, ...props }) => ( |
| <blockquote className="my-6 border-l-4 border-indigo-500/40 bg-indigo-500/5 py-4 px-6 text-[15.5px] text-slate-300 not-italic rounded-r-xl transition-all duration-500 hover:border-indigo-400 hover:bg-indigo-500/10 hover:text-white hover:shadow-[inset_0_0_20px_rgba(99,102,241,0.05)]" {...props} /> |
| ), |
| ul: ({ node, ...props }) => <ul className="my-6 space-y-3 pl-2" {...props} />, |
| li: ({ node, ...props }) => ( |
| <li className="group relative pl-7 leading-relaxed transition-all duration-300 hover:text-indigo-50 hover:translate-x-1 before:absolute before:left-0 before:top-[10px] before:h-1.5 before:w-1.5 before:rounded-full before:bg-indigo-500/50 before:transition-all before:duration-300 hover:before:bg-indigo-400 hover:before:scale-[1.3] hover:before:shadow-[0_0_10px_rgba(99,102,241,0.6)]" {...props} /> |
| ) |
| }} |
| > |
| {block.answerMarkdown} |
| </ReactMarkdown> |
| </div> |
| |
| {block.question && /what is dbms/i.test(block.question) && ( |
| <DBMSArchitectureDiagram /> |
| )} |
| |
| {block.question && /advantages/i.test(block.question) && ( |
| <DBMSAdvantagesDiagram /> |
| )} |
| |
| {block.question && /architecture|tier/i.test(block.question) && ( |
| <DBMSTierArchitectureDiagram /> |
| )} |
| |
| {block.question && /types/i.test(block.question) && ( |
| <DBMSTypesDiagram /> |
| )} |
| |
| {block.question && /er model|entity.?relationship/i.test(block.question) && ( |
| <ERModelDiagram /> |
| )} |
| |
| {block.question && /object.?oriented|oodm/i.test(block.question) && ( |
| <OODMDiagram /> |
| )} |
| |
| {block.question && /relational model/i.test(block.question) && ( |
| <RelationalModelDiagram /> |
| )} |
| |
| {block.question && /abstraction|logical|physical|conceptual/i.test(block.question) && ( |
| <DataAbstractionLevelsDiagram /> |
| )} |
| |
| {block.question && /sql|query|output|foreign key|select/i.test(block.question) && ( |
| <SQLTableOutput /> |
| )} |
| |
| {block.question && /table|row|column|tuple|attribute/i.test(block.question) && ( |
| <RelationalTableConceptsDiagram /> |
| )} |
| |
| {block.question && /constraint|not null|unique|default|check/i.test(block.question) && ( |
| <ConstraintsDiagram /> |
| )} |
| |
| {block.question && /denormalization|denormalize|redundant/i.test(block.question) && ( |
| <DenormalizationDiagram /> |
| )} |
| |
| {block.question && /primary key|foreign key|candidate key|super key|keys in dbms/i.test(block.question) && ( |
| <KeysInDBMSDiagram /> |
| )} |
| |
| {block.question && /normalization|1nf|2nf|3nf|bcnf|normal form/i.test(block.question) && ( |
| <NormalizationDiagram /> |
| )} |
| |
| {block.question && /relationship|one to one|one to many|many to many|1-1|1-m|m-m/i.test(block.question) && ( |
| <RelationshipsInDBMSDiagram /> |
| )} |
| |
| {block.question && /transaction|acid|commit|rollback|atomicity|consistency|isolation|durability/i.test(block.question) && ( |
| <ACIDTransactionsDiagram /> |
| )} |
| |
| {block.question && /index|b\+tree|btree|b tree|hash index|clustered|non.clustered|secondary index/i.test(block.question) && ( |
| <IndexingDiagram /> |
| )} |
| |
| {block.question && /join|inner join|outer join|left join|right join|cross join|self join/i.test(block.question) && ( |
| <JoinsDiagram /> |
| )} |
| |
| {block.question && /view|trigger|stored procedure|virtual table|materialized/i.test(block.question) && ( |
| <ViewsTriggersDiagram /> |
| )} |
| |
| {block.question && /concurrency|lock|two.phase|schedule|serializable|mvcc|deadlock control/i.test(block.question) && ( |
| <ConcurrencyControlDiagram /> |
| )} |
| </div> |
| </motion.div> |
| ); |
| })} |
| </div> |
| |
| {/* Footer info */} |
| <div className="mt-16 text-center"> |
| <div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-white/5 text-slate-500 mb-6"> |
| <Book size={20} /> |
| </div> |
| <h3 className="text-xl font-bold text-white mb-2">Topic Completed</h3> |
| <p className="text-slate-400 mb-8 max-w-sm mx-auto"> |
| You've reached the end of this topic. Head back to the main menu to track your progress and continue learning. |
| </p> |
| <button |
| onClick={onBack} |
| className="inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-indigo-500 px-8 text-sm font-bold text-white shadow-lg shadow-indigo-500/25 transition-all hover:-translate-y-0.5 hover:bg-indigo-600" |
| > |
| <ChevronLeft size={16} /> Return to Topics |
| </button> |
| </div> |
| |
| </div> |
| </div> |
| ); |
| } |
|
|