diff --git a/public/empty_project.png b/public/empty_project.png new file mode 100644 index 00000000..978987c3 Binary files /dev/null and b/public/empty_project.png differ diff --git a/src/app/api/canvas-templates/categories/route.ts b/src/app/api/canvas-templates/categories/route.ts new file mode 100644 index 00000000..e5a5d5d8 --- /dev/null +++ b/src/app/api/canvas-templates/categories/route.ts @@ -0,0 +1,41 @@ +import { requireLogin } from "@/app/api/_auth"; +import { NextRequest, NextResponse } from "next/server"; + +const UPSTREAM_CATEGORY_LIST_PATH = "/api_client/canvas/template/category/list"; + +function getPopiBaseUrl() { + return process.env.POPIART_API_BASE_URL?.replace(/\/+$/, "") || ""; +} + +export async function GET(request: NextRequest) { + const baseUrl = getPopiBaseUrl(); + + if (!baseUrl) { + return NextResponse.json( + { status: "5000", message: "POPIART_API_BASE_URL is not configured" }, + { status: 500 }, + ); + } + + const upstreamUrl = new URL(UPSTREAM_CATEGORY_LIST_PATH, baseUrl); + upstreamUrl.searchParams.set( + "keyword", + request.nextUrl.searchParams.get("keyword") || "", + ); + const { token } = requireLogin(request); + const response = await fetch(upstreamUrl, { + method: "GET", + headers: { + authorization: `Bearer ${token}`, + }, + }); + const body = await response.text(); + + return new NextResponse(body, { + status: response.status, + headers: { + "Content-Type": + response.headers.get("Content-Type") || "application/json", + }, + }); +} diff --git a/src/app/api/canvas-templates/route.ts b/src/app/api/canvas-templates/route.ts new file mode 100644 index 00000000..650de442 --- /dev/null +++ b/src/app/api/canvas-templates/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; + +const UPSTREAM_TEMPLATE_LIST_PATH = "/api_client/canvas/template/list"; +import { requireLogin } from "@/app/api/_auth"; +function getPopiBaseUrl() { + return process.env.POPIART_API_BASE_URL?.replace(/\/+$/, "") || ""; +} + +export async function GET(request: NextRequest) { + const baseUrl = getPopiBaseUrl(); + + if (!baseUrl) { + return NextResponse.json( + { status: "5000", message: "POPIART_API_BASE_URL is not configured" }, + { status: 500 }, + ); + } + + const upstreamUrl = new URL(UPSTREAM_TEMPLATE_LIST_PATH, baseUrl); + upstreamUrl.searchParams.set( + "keyword", + request.nextUrl.searchParams.get("keyword") || "", + ); + upstreamUrl.searchParams.set( + "categoryId", + request.nextUrl.searchParams.get("categoryId") || "", + ); + upstreamUrl.searchParams.set( + "type", + request.nextUrl.searchParams.get("type") || "", + ); + upstreamUrl.searchParams.set( + "userId", + request.nextUrl.searchParams.get("userId") || "", + ); + const { token } = requireLogin(request); // Ensure the user is logged in before making the upstream request + const response = await fetch(upstreamUrl, { + method: "GET", + headers: { + authorization: `Bearer ${token}`, + }, + }); + const body = await response.text(); + return new NextResponse(body, { + status: response.status, + headers: { + "Content-Type": + response.headers.get("Content-Type") || "application/json", + }, + }); +} diff --git a/src/app/api/recommend-banners/route.ts b/src/app/api/recommend-banners/route.ts new file mode 100644 index 00000000..eafb933c --- /dev/null +++ b/src/app/api/recommend-banners/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireLogin } from "@/app/api/_auth"; +const UPSTREAM_RECOMMEND_BANNER_LIST_PATH = + "/api_client/anime/recommendBanner/list"; + +function getPopiBaseUrl() { + return process.env.POPIART_API_BASE_URL?.replace(/\/+$/, "") || ""; +} + +export async function GET(request: NextRequest) { + const baseUrl = getPopiBaseUrl(); + console.log("request", request); + if (!baseUrl) { + return NextResponse.json( + { status: "5000", message: "POPIART_API_BASE_URL is not configured" }, + { status: 500 }, + ); + } + const { token } = requireLogin(request); + console.log("token", token); + const upstreamUrl = new URL(UPSTREAM_RECOMMEND_BANNER_LIST_PATH, baseUrl); + upstreamUrl.searchParams.set( + "categoryCode", + request.nextUrl.searchParams.get("categoryCode") || "", + ); + + const response = await fetch(upstreamUrl, { + method: "GET", + headers: { + authorization: `Bearer ${token}`, + }, + }); + const body = await response.text(); + console.log("Upstream response body:", body); + return new NextResponse(body, { + status: response.status, + headers: { + "Content-Type": + response.headers.get("Content-Type") || "application/json", + }, + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2b8efcc4..826d7755 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,7 +16,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/src/app/page.tsx b/src/app/page.tsx index d2264f47..8365cb45 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,14 @@ +"use client"; import Link from "next/link"; -import type { ReactNode } from "react"; -import { ProjectActionsDropdown } from "@/components/ProjectActionsDropdown"; +import { useEffect, useState, type ReactNode } from "react"; +import { HomeProjectsContent } from "@/components/HomeProjectsContent"; +import { authFetch } from "@/utils/authFetch"; import { HelpIcon, GlobeIcon, CloseIcon, - SearchIcon, - ArrowIcon, WeChatIcon, SparklingIcon, - StartCreation } from "@/components/icons"; const assets = { @@ -45,67 +44,68 @@ const text = { help: "帮助", language: "语言", search: "搜索", - moreActions: "更多操作", }; -const heroCards = [ - { title: "Seedance 2.0", subtitle: "火热上线", image: assets.heroSeedance }, - { - title: "做一条VLOG分享生活", - subtitle: "精选案例账号", - image: assets.heroVlog, - }, - { - title: "Popi.TV 测试上线", - subtitle: "帮助人类更好的表达", - image: assets.heroPopi, - }, -]; +type HeroCardData = { + title: string; + subtitle: string; + image: string; +}; + +type RecommendBannerItem = { + id: number; + cover: string; + title: string; + prompt: string; + sort: number; + status: number; + deleted: boolean; +}; + +type RecommendBannerListResponse = { + data?: { + list?: RecommendBannerItem[]; + }; + status?: string; +}; + +function isVideoUrl(url: string) { + try { + const pathname = new URL(url, "http://localhost").pathname.toLowerCase(); + return /\.(mp4|webm|ogg|ogv|mov|m4v)$/i.test(pathname); + } catch { + return /\.(mp4|webm|ogg|ogv|mov|m4v)(\?|#|$)/i.test(url.toLowerCase()); + } +} + +async function getHeroCards(): Promise { + try { + const params = new URLSearchParams({ categoryCode: "" }); + const response = await authFetch(`/api/recommend-banners?${params.toString()}`, { + method: "GET", + }); + if (!response.ok) return []; -const projects = [ - { title: "爱丽丝初稿", date: "2026/06/01", image: assets.projectAlice }, - { title: "喝咖啡Vlog", date: "2026/05/30", image: assets.projectCoffee }, - { title: "赏樱花测试", date: "2026/05/29", image: assets.projectSakura }, - { title: "未命名", date: "2026/05/28", image: assets.projectUntitled }, -]; + const result = (await response.json()) as RecommendBannerListResponse; + const list = result.data?.list; -const tabs = [ - "全部", - "精选画布", - "生活Vlog", - "短剧漫剧", - "专业影视", - "商业广告", -]; + if (!Array.isArray(list) || list.length === 0) return []; -const shows = [ - { - title: "PopiTV", - subtitle: "和爱丽丝一起郊游-Vlog", - image: assets.showPopi, - avatar: "👾", - vip: true, - }, - { - title: "爱丽丝大神", - subtitle: "和爱丽丝一起郊游-Vlog", - image: assets.showAlice, - avatar: "🌸", - vip: true, - }, - { - title: "叮叮频道", - subtitle: "和爱丽丝一起郊游-Vlog", - image: assets.showDing, - avatar: "🌊", - }, - { - title: "Aido", - subtitle: "和爱丽丝一起郊游-Vlog", - image: assets.showAido, - avatar: "🌿", - }, -]; + const cards = list + .filter((item) => item.status === 1 && !item.deleted) + .sort((a, b) => a.sort - b.sort) + .slice(0, 3) + .map((item, index) => ({ + title: item.title || "", + subtitle: item.prompt || "", + image: item.cover || "", + })); + + return cards; + } catch { + return []; + } +} function DiamondLogo({ label }: { label: string }) { return ( @@ -237,13 +237,26 @@ function HeroCard({ subtitle: string; image: string; }) { + const isVideo = isVideoUrl(image); + return (
- + {isVideo ? ( +