forked from 77media/video-flow
253 lines
8.9 KiB
TypeScript
253 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from "react";
|
|
import { fetchSettingByCode } from "@/api/serversetting";
|
|
import { X, ChevronUp, ChevronsDown } from "lucide-react";
|
|
import { ChatInputBox } from "@/components/ChatInputBox/ChatInputBox";
|
|
import { VideoCreationForm } from '@/components/pages/create-video/CreateInput';
|
|
import { useDeviceType } from '@/hooks/useDeviceType';
|
|
|
|
export const HOME_BANNER_CODE = "homeBanner";
|
|
const HOME_BANNER_COLLAPSE_KEY = "homeBannerCollapsedDate";
|
|
|
|
/** 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<HomeBannerConfig | null>(null);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isFlying, setIsFlying] = useState<boolean>(false);
|
|
const autoCollapseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const skipAutoCollapseRef = useRef<boolean>(false);
|
|
|
|
const { isMobile, isDesktop } = useDeviceType();
|
|
|
|
/** Returns YYYY-MM-DD for user's local timezone */
|
|
const getLocalDateKey = () => {
|
|
const now = new Date();
|
|
const y = now.getFullYear();
|
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
const d = String(now.getDate()).padStart(2, '0');
|
|
return `${y}-${m}-${d}`;
|
|
};
|
|
|
|
const handleDismiss = () => {
|
|
setIsFlying(true);
|
|
try {
|
|
localStorage.setItem(HOME_BANNER_COLLAPSE_KEY, getLocalDateKey());
|
|
} catch {}
|
|
};
|
|
|
|
const handleBannerClick = () => {
|
|
if (isFlying) {
|
|
setIsFlying(false);
|
|
}
|
|
};
|
|
|
|
// Initialize from persisted state: keep collapsed for the rest of the day
|
|
useEffect(() => {
|
|
try {
|
|
const saved = localStorage.getItem(HOME_BANNER_COLLAPSE_KEY);
|
|
if (saved && saved === getLocalDateKey()) {
|
|
setIsFlying(true);
|
|
skipAutoCollapseRef.current = true;
|
|
}
|
|
} catch {}
|
|
}, []);
|
|
|
|
// Auto collapse after mount (2s) unless already collapsed today
|
|
useEffect(() => {
|
|
if (skipAutoCollapseRef.current) return;
|
|
if (autoCollapseTimerRef.current) return;
|
|
autoCollapseTimerRef.current = setTimeout(() => {
|
|
setIsFlying(true);
|
|
try {
|
|
localStorage.setItem(HOME_BANNER_COLLAPSE_KEY, getLocalDateKey());
|
|
} catch {}
|
|
}, 2000);
|
|
return () => {
|
|
if (autoCollapseTimerRef.current) {
|
|
clearTimeout(autoCollapseTimerRef.current);
|
|
autoCollapseTimerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const value = await fetchSettingByCode<HomeBannerConfig>(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<string, unknown>;
|
|
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 (
|
|
<div data-alt="home-banner-wrapper" className="sticky top-0 z-50 w-full mx-auto p-0 overflow-hidden bg-gradient-to-b from-black/80 to-black/10">
|
|
{/* Banner overlay - stacked above */}
|
|
<section
|
|
data-alt="home-banner"
|
|
className={`absolute inset-0 z-10 isolate overflow-hidden rounded-3xl px-6 py-6 text-white border-2 border-transparent transition-all duration-400 ease-in-out ${
|
|
isFlying ? 'ring-2 ring-custom-blue ring-offset-0 [--tw-ring-color:theme(colors.custom-blue)] animate-ring-breath' : 'hover:border-custom-blue/50'
|
|
} ${
|
|
isFlying
|
|
? isDesktop ? "translate-x-[90%] -translate-y-[70%] scale-[0.85] opacity-95 rotate-3" : "translate-x-[80%] -translate-y-[70%] scale-[0.85] opacity-95 rotate-3"
|
|
: "translate-x-0 translate-y-0 scale-100 opacity-100 rotate-0"
|
|
}`}
|
|
aria-label="Home banner"
|
|
>
|
|
{/* 背景图层 - 完全铺满 */}
|
|
{backgroundImage ? (
|
|
<div data-alt="background-wrapper" className="absolute inset-0">
|
|
<img
|
|
data-alt="background-image"
|
|
src={backgroundImage}
|
|
className="h-full w-full object-cover block"
|
|
/>
|
|
<div data-alt="background-overlay" className="absolute inset-0 bg-black/40" />
|
|
</div>
|
|
) : null}
|
|
{/* Dismiss button */}
|
|
<div className={`absolute z-10 ${isDesktop ? 'right-4 top-4' : 'right-0 top-0'}`}>
|
|
<button
|
|
data-alt="banner-dismiss"
|
|
type="button"
|
|
onClick={handleDismiss}
|
|
className="text-white hover:bg-white/20 rounded-full p-2"
|
|
aria-label="Dismiss banner"
|
|
>
|
|
<ChevronUp className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
data-alt="banner-content"
|
|
className="relative flex flex-col items-center gap-4 text-center md:items-start md:text-left"
|
|
>
|
|
{eyebrow ? (
|
|
<span data-alt="banner-eyebrow" className="text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
|
|
{eyebrow}
|
|
</span>
|
|
) : null}
|
|
{title ? (
|
|
<h1 data-alt="banner-title" className="text-3xl font-semibold leading-tight md:text-5xl">
|
|
{title}
|
|
</h1>
|
|
) : null}
|
|
{(subtitle && isDesktop) ? (
|
|
<p data-alt="banner-subtitle" className="text-lg text-white/80 md:text-xl">{subtitle}</p>
|
|
) : null}
|
|
{(description && isDesktop) ? (
|
|
<p data-alt="banner-description" className="max-w-2xl text-base text-white/70">{description}</p>
|
|
) : null}
|
|
{ctaLabel ? (
|
|
ctaHref ? (
|
|
<a
|
|
data-alt="cta-link"
|
|
href={ctaHref}
|
|
className="inline-flex items-center justify-center rounded-full border border-white/30 bg-white/10 px-6 py-2 text-sm font-medium text-white transition hover:border-white hover:bg-white hover:text-slate-900"
|
|
>
|
|
{ctaLabel}
|
|
</a>
|
|
) : (
|
|
<button
|
|
data-alt="cta-button"
|
|
type="button"
|
|
className="inline-flex items-center justify-center rounded-full border border-white/30 bg-white/10 px-6 py-2 text-sm font-medium text-white transition hover:border-white hover:bg-white hover:text-slate-900"
|
|
>
|
|
{ctaLabel}
|
|
</button>
|
|
)
|
|
) : null}
|
|
</div>
|
|
|
|
{isFlying ? (
|
|
<button type="button" onClick={handleBannerClick} className="bg-white/50 rounded-full absolute left-4 bottom-3 h-5 w-5 animate-bounce">
|
|
<ChevronsDown className="inset-0 h-5 w-5" />
|
|
</button>
|
|
) : null}
|
|
</section>
|
|
|
|
{/* Base content - always present under the banner */}
|
|
<div data-alt="home-banner-base" className={`relative p-6 px-12 flex items-center justify-center bg-[radial-gradient(ellipse_at_center,rgba(106,244,249,0.28)_0%,rgba(106,244,249,0.14)_35%,transparent_70%)] ${isDesktop ? 'min-h-[300px]' : 'min-h-[200px] !p-0'}`}>
|
|
<VideoCreationForm />
|
|
</div>
|
|
</div>
|
|
);
|
|
} |