138 lines
5.4 KiB
TypeScript
138 lines
5.4 KiB
TypeScript
import React from 'react';
|
|
import { ContentBlock } from '../types';
|
|
import { TerminalBlock } from './TerminalBlock';
|
|
import { GitWorkflowDiagram } from './GitWorkflowDiagram';
|
|
import { AlertTriangle, Lightbulb, Info, CheckCircle2, ChevronDown } from 'lucide-react';
|
|
import gsap from 'gsap';
|
|
|
|
interface ContentRendererProps {
|
|
block: ContentBlock;
|
|
}
|
|
|
|
const Callout: React.FC<{ variant?: string; content: string }> = ({ variant = 'info', content }) => {
|
|
const getIcon = () => {
|
|
switch(variant) {
|
|
case 'warning': return <AlertTriangle className="text-destructive" />;
|
|
case 'tip': return <Lightbulb className="text-accent" />;
|
|
case 'success': return <CheckCircle2 className="text-primary" />;
|
|
default: return <Info className="text-blue-500" />;
|
|
}
|
|
};
|
|
|
|
const getStyles = () => {
|
|
switch(variant) {
|
|
case 'warning': return 'bg-destructive/10 border-destructive/20 text-destructive-foreground';
|
|
case 'tip': return 'bg-accent/10 border-accent/20 text-accent-foreground';
|
|
case 'success': return 'bg-primary/10 border-primary/20 text-primary-foreground';
|
|
default: return 'bg-blue-50 border-blue-200 text-blue-900';
|
|
}
|
|
};
|
|
|
|
// Adjusting styles to match new variables more closely for text colors where simple bg/text classes might fail due to variable usage
|
|
// The tailwind classes using vars (like text-primary) work if defined in config, which we did.
|
|
const getContainerClass = () => {
|
|
switch(variant) {
|
|
case 'warning': return 'bg-red-50 border-red-200 text-red-900';
|
|
case 'tip': return 'bg-amber-50 border-amber-200 text-amber-900';
|
|
case 'success': return 'bg-green-50 border-green-200 text-green-900';
|
|
default: return 'bg-blue-50 border-blue-200 text-blue-900';
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`flex gap-4 p-4 my-4 rounded-lg border-l-4 ${getContainerClass()}`}>
|
|
<div className="shrink-0 pt-0.5">{getIcon()}</div>
|
|
<p className="text-sm md:text-base">{content}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Collapsible: React.FC<{ content: any, variant?: string }> = ({ content, variant }) => {
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const bodyRef = React.useRef<HTMLDivElement>(null);
|
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!contentRef.current || !bodyRef.current) return;
|
|
|
|
if (isOpen) {
|
|
gsap.to(bodyRef.current, {
|
|
height: contentRef.current.scrollHeight,
|
|
duration: 0.4,
|
|
ease: "power2.out",
|
|
opacity: 1
|
|
});
|
|
} else {
|
|
gsap.to(bodyRef.current, {
|
|
height: 0,
|
|
duration: 0.3,
|
|
ease: "power2.in",
|
|
opacity: 0
|
|
});
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const isPractice = variant === 'success';
|
|
|
|
return (
|
|
<div className={`my-4 border rounded-lg overflow-hidden transition-all duration-300 ${isPractice ? 'border-primary/20 shadow-sm' : 'border-border bg-card'}`}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className={`w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors ${isPractice ? 'bg-primary/5' : ''}`}
|
|
>
|
|
<span className={`font-semibold ${isPractice ? 'text-primary' : 'text-card-foreground'}`}>
|
|
{content.title}
|
|
</span>
|
|
<ChevronDown
|
|
className={`transform transition-transform duration-300 text-muted-foreground ${isOpen ? 'rotate-180' : ''}`}
|
|
size={20}
|
|
/>
|
|
</button>
|
|
<div ref={bodyRef} className="h-0 opacity-0 overflow-hidden">
|
|
<div ref={contentRef} className="p-4 pt-0 border-t border-border">
|
|
{Array.isArray(content.body) && content.body.map((subBlock: ContentBlock, idx: number) => (
|
|
<ContentRenderer key={idx} block={subBlock} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ContentRenderer: React.FC<ContentRendererProps> = ({ block }) => {
|
|
switch (block.type) {
|
|
case 'paragraph':
|
|
return <p className="mb-4 text-muted-foreground leading-relaxed text-lg">{block.content as string}</p>;
|
|
case 'code':
|
|
return <TerminalBlock code={block.content as string} language={block.language} />;
|
|
case 'callout':
|
|
return <Callout variant={block.variant} content={block.content as string} />;
|
|
case 'list':
|
|
return (
|
|
<ul className="list-disc pl-6 mb-4 space-y-2 text-muted-foreground">
|
|
{(block.content as string[]).map((item, i) => (
|
|
<li key={i} className="pl-2">{item}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
case 'ordered-list':
|
|
return (
|
|
<ol className="list-decimal pl-6 mb-4 space-y-2 text-muted-foreground">
|
|
{(block.content as string[]).map((item, i) => (
|
|
<li key={i} className="pl-2">{item}</li>
|
|
))}
|
|
</ol>
|
|
);
|
|
case 'subheading':
|
|
return <h3 className="text-xl font-bold text-foreground mt-8 mb-4 flex items-center gap-2">
|
|
<span className="w-1.5 h-6 bg-primary rounded-full inline-block"></span>
|
|
{block.content as string}
|
|
</h3>;
|
|
case 'collapsible':
|
|
return <Collapsible content={block.content} variant={block.variant} />;
|
|
case 'diagram':
|
|
return <GitWorkflowDiagram />;
|
|
default:
|
|
return null;
|
|
}
|
|
}; |