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:
ar.badiei
2026-09-03 21:01:52 +03:30
co-authored by Cursor
parent aa9236fb92
commit 370ac15471
193 changed files with 25997 additions and 1991 deletions
@@ -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>
);
}
+43
View File
@@ -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 };