(null);
const [dupForm, setDupForm] = useState({ aspect: "1:1", width: 1080, height: 1080, resolution: "FullHD", name: "" });
@@ -236,6 +238,7 @@ export function ProjectsAdmin() {
|
+
@@ -321,6 +324,20 @@ export function ProjectsAdmin() {
)}
+
+ {openStories && (
+ setOpenStories(null)}>
+ e.stopPropagation()}>
+
+ ویدیوهای نمونه — {openStories.name} ({openStories.container_name})
+
+
+
+
+
+ )}
);
}
diff --git a/src/components/templates/TemplateDetailContent.tsx b/src/components/templates/TemplateDetailContent.tsx
index 0af1f95..10f60c2 100644
--- a/src/components/templates/TemplateDetailContent.tsx
+++ b/src/components/templates/TemplateDetailContent.tsx
@@ -23,6 +23,10 @@ export function TemplateDetailContent({ template }: TemplateDetailContentProps)
aspects[0] ?? "16:9"
);
+ // The content project (variant) UUID for the selected aspect — keys preset stories.
+ const variantProjectId =
+ (template.variants?.find((v) => v.aspect === selectedAspect) ?? template.variants?.[0])?.projectId;
+
return (
@@ -36,7 +40,7 @@ export function TemplateDetailContent({ template }: TemplateDetailContentProps)
-
+
);
}
diff --git a/src/components/templates/TemplateDetailExamples.tsx b/src/components/templates/TemplateDetailExamples.tsx
index e7c5c5a..69315bc 100644
--- a/src/components/templates/TemplateDetailExamples.tsx
+++ b/src/components/templates/TemplateDetailExamples.tsx
@@ -1,36 +1,133 @@
-import { useTranslations } from "next-intl";
+"use client";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Loader2, Play } from "lucide-react";
+
+import { toast } from "@/components/ui/use-toast";
+import {
+ createProjectFromTemplate,
+ studioPathForProject,
+} from "@/lib/create-project-from-template";
import { getVideoTemplateExampleImageSrc } from "@/lib/video-templates-catalog";
+interface PresetStory {
+ id: string;
+ name: string;
+ description?: string | null;
+ demo?: string | null;
+ scene_count?: number;
+}
+
interface TemplateDetailExamplesProps {
templateId: string;
+ /** Content project (variant) UUID for the selected aspect — keys the preset stories. */
+ projectId?: string;
}
const EXAMPLE_COUNT = 5;
+const isVideo = (url: string) => /\.(mp4|webm|mov|m4v)(\?|$)/i.test(url);
-export function TemplateDetailExamples({ templateId }: TemplateDetailExamplesProps) {
+export function TemplateDetailExamples({ templateId, projectId }: TemplateDetailExamplesProps) {
const t = useTranslations("auto.componentsTemplatesTemplateDetailExamples");
+ const router = useRouter();
+ const [stories, setStories] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ const [busyId, setBusyId] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ if (!projectId) { setStories([]); setLoaded(true); return; }
+ setLoaded(false);
+ fetch(`/api/preset-stories?project_id=${projectId}`, { cache: "no-store" })
+ .then((r) => r.json())
+ .then((d) => { if (!cancelled) setStories(Array.isArray(d?.stories) ? d.stories : []); })
+ .catch(() => { if (!cancelled) setStories([]); })
+ .finally(() => { if (!cancelled) setLoaded(true); });
+ return () => { cancelled = true; };
+ }, [projectId]);
+
+ const startFromPreset = async (story: PresetStory) => {
+ setBusyId(story.id);
+ const result = await createProjectFromTemplate({
+ id: projectId ?? templateId,
+ name: story.name,
+ category: "Video",
+ presetStoryId: story.id,
+ });
+ if (!result.ok) {
+ setBusyId(null);
+ if (result.status === 401) {
+ router.push(`/auth?next=${encodeURIComponent(`/templates/${templateId}`)}`);
+ return;
+ }
+ toast({ title: result.error });
+ return;
+ }
+ router.push(studioPathForProject(result.project.id, result.project.type));
+ };
+
+ // While loading, or when an admin has published real example videos, show them.
+ const hasStories = loaded && stories.length > 0;
return (
-
- {t("heading")}
-
-
- {Array.from({ length: EXAMPLE_COUNT }).map((_, index) => (
-
- {/* eslint-disable-next-line @next/next/no-img-element */}
- 
-
- ))}
-
+ {t("heading")}
+
+ {hasStories ? (
+
+ {stories.map((story) => (
+
+
+ {story.demo ? (
+ isVideo(story.demo) ? (
+
+ {story.name}
+ {story.description && (
+ {story.description}
+ )}
+
+ ))}
+
+ ) : (
+ // Fallback: placeholder thumbnails when no preset stories are published yet.
+
+ {Array.from({ length: EXAMPLE_COUNT }).map((_, index) => (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ 
+
+ ))}
+
+ )}
);
}
diff --git a/src/lib/create-project-from-template.ts b/src/lib/create-project-from-template.ts
index fb4ddd7..8716441 100644
--- a/src/lib/create-project-from-template.ts
+++ b/src/lib/create-project-from-template.ts
@@ -26,6 +26,8 @@ export async function createProjectFromTemplate(input: {
id: string;
name: string;
category: TemplateCatalogCategory;
+ /** Start pre-bound to an admin-authored preset story (premade example video). */
+ presetStoryId?: string;
}): Promise {
const type = catalogCategoryToProjectType(input.category);
const scene_data = {
@@ -40,6 +42,7 @@ export async function createProjectFromTemplate(input: {
name: input.name,
type,
scene_data,
+ ...(input.presetStoryId ? { preset_story_id: input.presetStoryId } : {}),
}),
});
|