RYP / src /components /DBMSContentPage.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
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;
}
/**
* fixMarkdownBold and cleanMarkdown are imported from @/lib/markdownUtils.
* See that file for full documentation.
*/
function parseDBMSContent(raw: string): QABlock[] {
let cleaned = raw.replace(/	/g, '').replace(/\\&/g, '&');
cleaned = fixMarkdownBold(cleaned);
cleaned = processAsciiTableToMarkdown(cleaned);
// Split by **Q or **Q: or **Q. or **Q:-
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();
// Remove "**Ans:-**" and any variations — handles **Ans:**, **Ans:-**, **Ans**, Ans:, etc.
answer = answer.replace(/^\s*\**\s*Ans(?:wer)?\s*[:\-\.]?\s*\**\s*/i, '').trim();
// Fix stray markdown artifacts like `_**` at the beginning of the answer
answer = answer.replace(/^[_*]+\s*?\n+/g, '').trim();
answer = answer.replace(/^_\*\*\s*/g, '').trim();
// Strip optional leading ** before numbered items and remove trailing --** or **
answer = answer.replace(/^(?:\*\*|)(\d+[.\)]?\s*.+?)(?:--\*\*|\*\*)\s*$/gm, '$1').trim();
// Convert "1. Title" / "1\. Title" / "1.Title" at start of lines into ### headings
answer = answer.replace(/^(\d+)[\\\.]\s*(.+)$/gm, '### $1. $2');
// ── FINAL PASS: clean all remaining stray artifacts ──
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>
);
}