Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import type { CalloutBlockData } from '@/types/contentBlocks';
|
||||
|
||||
const variantClass: Record<NonNullable<CalloutBlockData['variant']>, string> = {
|
||||
info: 'border-blue-500/20 bg-blue-500/5 text-blue-900 dark:text-blue-200',
|
||||
warning: 'border-amber-500/20 bg-amber-500/5 text-amber-900 dark:text-amber-200',
|
||||
tip: 'border-green-500/20 bg-green-500/5 text-green-900 dark:text-green-200'
|
||||
};
|
||||
|
||||
export function CalloutBlock({ block }: { block: CalloutBlockData }) {
|
||||
const variant = block.variant ?? 'info';
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg border p-3 text-sm ${variantClass[variant]}`}>
|
||||
{block.title && <p className="mb-1 font-medium">{block.title}</p>}
|
||||
<p className="leading-relaxed">{block.body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DividerBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function DividerBlock(_props: { block: DividerBlockData }) {
|
||||
return <hr className="border-border" />;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Lightbulb } from 'lucide-react';
|
||||
import type { ExerciseBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function ExerciseBlock({ block }: { block: ExerciseBlockData }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-3">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Lightbulb className="h-4 w-4 text-amber-600" />
|
||||
<p className="text-sm font-medium text-amber-900 dark:text-amber-200">
|
||||
{block.title ?? 'تمرین'}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-amber-800 dark:text-amber-300">{block.prompt}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import type { FutureAIBlockData } from '@/types/contentBlocks';
|
||||
|
||||
/** Placeholder only — no AI behavior */
|
||||
export function FutureAIBlock({ block }: { block: FutureAIBlockData }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-muted-foreground/40 bg-muted/40 p-3">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
{block.label ?? 'قابلیت هوش مصنوعی (آینده)'}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{block.description ?? 'این بلوک فقط برای آمادهسازی معماری است و هنوز فعال نیست.'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { HeadingBlockData } from '@/types/contentBlocks';
|
||||
|
||||
const levelClass: Record<1 | 2 | 3, string> = {
|
||||
1: 'text-xl font-bold leading-snug',
|
||||
2: 'text-lg font-semibold leading-snug',
|
||||
3: 'text-base font-semibold leading-snug'
|
||||
};
|
||||
|
||||
export function HeadingBlock({ block }: { block: HeadingBlockData }) {
|
||||
const level = block.level ?? 3;
|
||||
const className = levelClass[level];
|
||||
|
||||
if (level === 1) return <h2 className={className}>{block.text}</h2>;
|
||||
if (level === 2) return <h3 className={className}>{block.text}</h3>;
|
||||
return <h4 className={className}>{block.text}</h4>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ImageBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function ImageBlock({ block }: { block: ImageBlockData }) {
|
||||
return (
|
||||
<figure className="space-y-2">
|
||||
<img
|
||||
src={block.src}
|
||||
alt={block.alt}
|
||||
className="w-full rounded-md object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{block.caption && (
|
||||
<figcaption className="text-center text-xs text-muted-foreground">{block.caption}</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { QuoteBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function QuoteBlock({ block }: { block: QuoteBlockData }) {
|
||||
return (
|
||||
<blockquote className="border-r-2 border-primary/40 pr-3 text-sm italic text-muted-foreground">
|
||||
<p>{block.text}</p>
|
||||
{block.attribution && (
|
||||
<footer className="mt-2 text-xs not-italic">— {block.attribution}</footer>
|
||||
)}
|
||||
</blockquote>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import type { ReflectionBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function ReflectionBlock({ block }: { block: ReflectionBlockData }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-blue-500/20 bg-blue-500/5 p-3">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-blue-600" />
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-200">سؤال تأملی</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{block.prompt}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { TextBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function TextBlock({ block }: { block: TextBlockData }) {
|
||||
if (block.format === 'html') {
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm dark:prose-invert max-w-none text-sm leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: block.body }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <p className="text-sm leading-relaxed whitespace-pre-wrap">{block.body}</p>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { VideoBlockData } from '@/types/contentBlocks';
|
||||
|
||||
export function VideoBlock({ block }: { block: VideoBlockData }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{block.title && <p className="text-sm font-medium">{block.title}</p>}
|
||||
<video
|
||||
className="w-full rounded-md"
|
||||
controls
|
||||
playsInline
|
||||
poster={block.poster}
|
||||
src={block.src}
|
||||
>
|
||||
مرورگر شما از پخش ویدیو پشتیبانی نمیکند.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { ContentBlock, ContentBlockType } from '@/types/contentBlocks';
|
||||
import { HeadingBlock } from './HeadingBlock';
|
||||
import { TextBlock } from './TextBlock';
|
||||
import { QuoteBlock } from './QuoteBlock';
|
||||
import { ImageBlock } from './ImageBlock';
|
||||
import { VideoBlock } from './VideoBlock';
|
||||
import { CalloutBlock } from './CalloutBlock';
|
||||
import { ExerciseBlock } from './ExerciseBlock';
|
||||
import { ReflectionBlock } from './ReflectionBlock';
|
||||
import { DividerBlock } from './DividerBlock';
|
||||
import { FutureAIBlock } from './FutureAIBlock';
|
||||
|
||||
type BlockComponent = ComponentType<{ block: ContentBlock }>;
|
||||
|
||||
/**
|
||||
* Registry maps block type → React component.
|
||||
* Future block types (audio, quiz, accordion, …) register here
|
||||
* without changing ContentRenderer control flow.
|
||||
*/
|
||||
const blockRegistry: Partial<Record<ContentBlockType | string, BlockComponent>> = {
|
||||
heading: HeadingBlock as BlockComponent,
|
||||
text: TextBlock as BlockComponent,
|
||||
quote: QuoteBlock as BlockComponent,
|
||||
image: ImageBlock as BlockComponent,
|
||||
video: VideoBlock as BlockComponent,
|
||||
callout: CalloutBlock as BlockComponent,
|
||||
exercise: ExerciseBlock as BlockComponent,
|
||||
reflection: ReflectionBlock as BlockComponent,
|
||||
divider: DividerBlock as BlockComponent,
|
||||
'future-ai': FutureAIBlock as BlockComponent
|
||||
};
|
||||
|
||||
/** Register or replace a block renderer at runtime / module init */
|
||||
export function registerBlock(type: string, component: BlockComponent) {
|
||||
blockRegistry[type] = component;
|
||||
}
|
||||
|
||||
export function getRegisteredBlock(type: string): BlockComponent | undefined {
|
||||
return blockRegistry[type];
|
||||
}
|
||||
|
||||
export { blockRegistry };
|
||||
Reference in New Issue
Block a user