+ {topTemplates.map((t) => {
+
+ return (
+
+
+
+

+
+
+
+
+
+
{
+ const id = t.id || t.template_id
+ if (t.show_url) {
+ if (activeTemplateId === id) {
+ setActiveTemplateId(null)
+ } else {
+ setIsPreviewReady(false)
+ setActiveTemplateId(id)
+ }
+ } else {
+ setInitialTemplateId(id)
+ setIsModalOpen(true)
+ }
+ }}
+ />
+
+ )
+ })}
+
+ )}
+
+ {/* Centered modal for active template preview */}
+ {activeTemplateId && (() => {
+ const active = topTemplates.find((x) => (x.id || x.template_id) === activeTemplateId)
+ if (!active || !active.show_url) return null
+ return (
+
setActiveTemplateId(null)}
+ >
+
e.stopPropagation()}
+ >
+ <>
+
+
+ )
+ })()}
+
+
setIsModalOpen(false)}
+ initialTemplateId={initialTemplateId}
+ configOptions={{ mode: "auto", resolution: "720p", language: "english", videoDuration: "auto" }}
+ />
+
+ )
+}
+
+export default FamousTemplate
diff --git a/components/HomeBanner.tsx b/components/HomeBanner.tsx
new file mode 100644
index 0000000..0943b0c
--- /dev/null
+++ b/components/HomeBanner.tsx
@@ -0,0 +1,159 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { fetchSettingByCode } from "@/api/serversetting";
+
+export const HOME_BANNER_CODE = "homeBanner";
+
+/** CTA config for banner */
+export interface HomeBannerCTAConfig {
+ label?: string;
+ href?: string;
+ [key: string]: unknown;
+}
+
+/** Configuration payload for `HomeBanner` */
+export interface HomeBannerConfig {
+ eyebrow?: string;
+ title?: string;
+ subtitle?: string;
+ description?: string;
+ backgroundImage?: string;
+ cta?: HomeBannerCTAConfig;
+ ctaText?: string;
+ ctaLink?: string;
+ [key: string]: unknown;
+}
+
+/**
+ * Renders the home banner with optional background image and CTA.
+ * Pulls configuration from server settings keyed by `HOME_BANNER_CODE`.
+ * @returns React.ReactElement | null
+ */
+export default function HomeBanner() {
+ const [config, setConfig] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let active = true;
+
+ async function load() {
+ setLoading(true);
+ setError(null);
+ try {
+ const value = await fetchSettingByCode(HOME_BANNER_CODE);
+ if (!active) return;
+ setConfig(value ?? null);
+ } catch (err) {
+ if (!active) return;
+ const message = err instanceof Error ? err.message : "Failed to load home banner";
+ setError(message);
+ setConfig(null);
+ } finally {
+ if (active) {
+ setLoading(false);
+ }
+ }
+ }
+
+ load();
+
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ if (loading || error || !config) {
+ // Render nothing until configuration schema is finalized.
+ return null;
+ }
+
+ const title = typeof config.title === "string" ? config.title : undefined;
+ const subtitle = typeof config.subtitle === "string" ? config.subtitle : undefined;
+ const description = typeof config.description === "string" ? config.description : undefined;
+ const eyebrow = typeof config.eyebrow === "string" ? config.eyebrow : undefined;
+ const backgroundImage = typeof config.backgroundImage === "string" ? config.backgroundImage : undefined;
+
+ let ctaLabel: string | undefined;
+ let ctaHref: string | undefined;
+
+ if (config.cta && typeof config.cta === "object") {
+ const { label, href } = config.cta as Record;
+ if (typeof label === "string") ctaLabel = label;
+ if (typeof href === "string") ctaHref = href;
+ }
+
+ // Support legacy field names until the API contract is confirmed.
+ if (!ctaLabel && typeof config.ctaText === "string") {
+ ctaLabel = config.ctaText;
+ }
+ if (!ctaHref && typeof config.ctaLink === "string") {
+ ctaHref = config.ctaLink;
+ }
+
+ const hasContent = Boolean(title || subtitle || description || ctaLabel || backgroundImage);
+ if (!hasContent) {
+ return null;
+ }
+
+ return (
+
+ {backgroundImage ? (
+
+ ) : null}
+ {backgroundImage ? (
+
+ ) : null}
+
+
+ {eyebrow ? (
+
+ {eyebrow}
+
+ ) : null}
+ {title ? (
+
+ {title}
+
+ ) : null}
+ {subtitle ? (
+
{subtitle}
+ ) : null}
+ {description ? (
+
{description}
+ ) : null}
+ {ctaLabel ? (
+ ctaHref ? (
+
+ {ctaLabel}
+
+ ) : (
+
+ )
+ ) : null}
+
+
+ );
+}
diff --git a/components/MyMovies.tsx b/components/MyMovies.tsx
new file mode 100644
index 0000000..f6acb7b
--- /dev/null
+++ b/components/MyMovies.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import React, { useEffect, useState } from 'react';
+import Link from 'next/link';
+import { getScriptEpisodeListNew, MovieProject } from '@/api/script_episode';
+import { getFirstFrame } from '@/utils/tools';
+import { useRouter } from 'next/navigation';
+import { motion } from 'framer-motion';
+
+/**
+ * A lightweight horizontal list of user's movie projects.
+ * Shows first 9 items, clipped horizontally, with a right-side gradient more button.
+ */
+const MyMovies: React.FC = () => {
+ const [projects, setProjects] = useState([]);
+ const router = useRouter();
+
+ const StatusBadge = (status: string): JSX.Element => {
+ return (
+
+ {status === 'pending' && (
+ <>
+
+ >
+ )}
+ {status === 'failed' && (
+ <>
+
+
+ FAILED
+
+ >
+ )}
+
+ );
+ };
+
+ useEffect(() => {
+ const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
+ const userId = String(user.id || '0');
+ getScriptEpisodeListNew({ user_id: userId, page: 1, per_page: 9 })
+ .then((res) => {
+ if ((res as any)?.code === 0) {
+ setProjects((res as any).data.movie_projects?.slice(0, 9) || []);
+ }
+ })
+ .catch(() => {});
+ }, []);
+
+ return (
+
+
+
My Movies
+ All movies →
+
+
+
+
+
+ {projects.map((p) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default MyMovies;
+
+
diff --git a/components/layout/type.ts b/components/layout/type.ts
index 9660c8b..b6d332b 100644
--- a/components/layout/type.ts
+++ b/components/layout/type.ts
@@ -1,4 +1,4 @@
-import { BookHeart, Gift } from "lucide-react";
+import { BookHeart, Gift, Home } from "lucide-react";
interface NavigationItem {
name: string;
@@ -15,6 +15,7 @@ export const navigationItems: Navigations[] = [
{
title: 'Main',
items: [
+ { name: 'Home', href: '/home', icon: Home },
{ name: 'My Portfolio', href: '/movies', icon: BookHeart },
{ name: 'Share', href: '/share', icon: Gift },
],
diff --git a/public/assets/home.mp4 b/public/assets/home.mp4
deleted file mode 100644
index 1f22552..0000000
Binary files a/public/assets/home.mp4 and /dev/null differ
diff --git a/public/assets/module2 (1).mp4 b/public/assets/module2 (1).mp4
deleted file mode 100644
index d4fe2a5..0000000
Binary files a/public/assets/module2 (1).mp4 and /dev/null differ
diff --git a/public/assets/module2 (2).mp4 b/public/assets/module2 (2).mp4
deleted file mode 100644
index 3055bd3..0000000
Binary files a/public/assets/module2 (2).mp4 and /dev/null differ
diff --git a/public/assets/module2 (3).mp4 b/public/assets/module2 (3).mp4
deleted file mode 100644
index 070f1ea..0000000
Binary files a/public/assets/module2 (3).mp4 and /dev/null differ
diff --git a/public/assets/module4 (1).mp4 b/public/assets/module4 (1).mp4
deleted file mode 100644
index cd969ab..0000000
Binary files a/public/assets/module4 (1).mp4 and /dev/null differ
diff --git a/public/assets/module4 (2).mp4 b/public/assets/module4 (2).mp4
deleted file mode 100644
index 7864a15..0000000
Binary files a/public/assets/module4 (2).mp4 and /dev/null differ
diff --git a/public/assets/module4 (3).mp4 b/public/assets/module4 (3).mp4
deleted file mode 100644
index 4de3e15..0000000
Binary files a/public/assets/module4 (3).mp4 and /dev/null differ
diff --git a/public/assets/module4 (4).mp4 b/public/assets/module4 (4).mp4
deleted file mode 100644
index 01f3bcd..0000000
Binary files a/public/assets/module4 (4).mp4 and /dev/null differ
diff --git a/utils/tools.ts b/utils/tools.ts
index b75fac8..26249ff 100644
--- a/utils/tools.ts
+++ b/utils/tools.ts
@@ -1,4 +1,6 @@
import { ScriptSlice, ScriptSliceType } from "@/app/service/domain/valueObject";
+import cover_image1 from '@/public/assets/cover_image3.jpg';
+import cover_image2 from '@/public/assets/cover_image_shu.jpg';
export function parseScriptEntity(text: string): ScriptSlice {
const scriptSlice = new ScriptSlice(
@@ -118,7 +120,10 @@ export const downloadAllVideos = async (urls: string[]) => {
* 发现 链接 包含 ‘aliyuncs.com’ 是阿里云地址
* @param url 视频URL
*/
-export const getFirstFrame = (url: string, width?: number) => {
+export const getFirstFrame = (url: string, width?: number, aspectRatio?: 'VIDEO_ASPECT_RATIO_PORTRAIT' | 'VIDEO_ASPECT_RATIO_LANDSCAPE') => {
+ if (!url) {
+ return `${aspectRatio === 'VIDEO_ASPECT_RATIO_PORTRAIT' ? cover_image2.src : cover_image1.src}`
+ }
if (url.includes('aliyuncs.com')) {
return url + '?x-oss-process=video/snapshot,t_1000,f_jpg' + `${width ? ',w_'+width : ''}`;
} else {