新增 sticky banner

This commit is contained in:
moux1024 2025-10-21 21:50:49 +08:00
parent 70198d3f43
commit cd132c670e
6 changed files with 60 additions and 5 deletions

View File

@ -6,6 +6,8 @@ import { X } from "lucide-react"
import TemplatePreviewModal from "@/components/common/TemplatePreviewModal" import TemplatePreviewModal from "@/components/common/TemplatePreviewModal"
import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal" import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal"
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService" import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"
import { useAppDispatch } from "@/lib/store/hooks"
import { selectTemplateById } from "@/lib/store/creationTemplateSlice"
/** /**
* A compact template showcase with a header and link to all templates. * A compact template showcase with a header and link to all templates.
@ -14,6 +16,7 @@ import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateS
*/ */
const FamousTemplate: React.FC = () => { const FamousTemplate: React.FC = () => {
const { templateStoryList, getTemplateStoryList, isLoading } = useTemplateStoryServiceHook() const { templateStoryList, getTemplateStoryList, isLoading } = useTemplateStoryServiceHook()
const dispatch = useAppDispatch()
const [isModalOpen, setIsModalOpen] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false)
const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined) const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined)
@ -127,8 +130,8 @@ const FamousTemplate: React.FC = () => {
title={active.name} title={active.name}
description={active.generateText || active.name} description={active.generateText || active.name}
onPrimaryAction={() => { onPrimaryAction={() => {
setInitialTemplateId(active.id || active.template_id) const id = active.id || active.template_id
setIsModalOpen(true) dispatch(selectTemplateById(id))
setActiveTemplateId(null) setActiveTemplateId(null)
}} }}
primaryLabel="Try it Free" primaryLabel="Try it Free"

View File

@ -159,11 +159,13 @@ export default function HomeBanner() {
} }
return ( return (
<div data-alt="home-banner-wrapper" className="relative w-full mx-auto p-0 overflow-hidden"> <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 */} {/* Banner overlay - stacked above */}
<section <section
data-alt="home-banner" 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 hover:border-custom-blue/50 transition-all duration-400 ease-in-out ${ 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 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" ? 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" : "translate-x-0 translate-y-0 scale-100 opacity-100 rotate-0"

View File

@ -28,6 +28,8 @@ import { useRouter } from 'next/navigation';
import { useTemplateStoryServiceHook } from '@/app/service/Interaction/templateStoryService'; import { useTemplateStoryServiceHook } from '@/app/service/Interaction/templateStoryService';
import { StoryTemplateEntity } from '@/app/service/domain/Entities'; import { StoryTemplateEntity } from '@/app/service/domain/Entities';
import { useUploadFile } from '@/app/service/domain/service'; import { useUploadFile } from '@/app/service/domain/service';
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
import { clearSelection } from '@/lib/store/creationTemplateSlice';
export default function VideoCreationForm() { export default function VideoCreationForm() {
const [photos, setPhotos] = useState<PhotoItem[]>([]); const [photos, setPhotos] = useState<PhotoItem[]>([]);
@ -83,6 +85,8 @@ export default function VideoCreationForm() {
const propInputRef = useRef<HTMLInputElement>(null); const propInputRef = useRef<HTMLInputElement>(null);
const mainTextInputRef = useRef<HTMLTextAreaElement>(null); const mainTextInputRef = useRef<HTMLTextAreaElement>(null);
const { uploadFile } = useUploadFile(); const { uploadFile } = useUploadFile();
const dispatch = useAppDispatch();
const selectedTemplateId = useAppSelector(state => state.creationTemplate.selectedTemplateId);
/** Clear current template related states */ /** Clear current template related states */
const clearTemplateSelection = () => { const clearTemplateSelection = () => {
@ -111,6 +115,16 @@ export default function VideoCreationForm() {
}, 0); }, 0);
}; };
/** Apply template selected from global store (e.g., FamousTemplate) */
useEffect(() => {
if (!selectedTemplateId) return;
const selected = templateStoryList.find(t => (t.id || t.template_id) === selectedTemplateId);
if (!selected) return;
clearTemplateSelection();
applyTemplateSelection(selected as StoryTemplateEntity);
dispatch(clearSelection());
}, [selectedTemplateId, templateStoryList]);
/** Handle file upload */ /** Handle file upload */
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: PhotoType) => { const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: PhotoType) => {
const files = event.target.files; const files = event.target.files;
@ -387,7 +401,7 @@ export default function VideoCreationForm() {
{shouldShowInput && ( {shouldShowInput && (
<div data-alt="text-input-wrapper" className="flex-1 flex px-4 py-2"> <div data-alt="text-input-wrapper" className="flex-1 flex px-4 py-2">
{isTemplateSelected?.freeInput[0].input_name && ( {isTemplateSelected?.freeInput[0].input_name && (
<div data-alt="template-description-text" className="text-custom-blue text-sm pr-2">{isTemplateSelected?.freeInput[0].input_name}</div> <div data-alt="template-description-text" className="text-white/60 text-sm pr-2">{isTemplateSelected?.freeInput[0].input_name}</div>
)} )}
<textarea <textarea
data-alt="main-text-input" data-alt="main-text-input"

View File

@ -0,0 +1,29 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CreationTemplateState {
/** The selected template id to be applied by the creation form */
selectedTemplateId: string | null;
}
const initialState: CreationTemplateState = {
selectedTemplateId: null,
};
export const creationTemplateSlice = createSlice({
name: 'creationTemplate',
initialState,
reducers: {
selectTemplateById: (state, action: PayloadAction<string>) => {
state.selectedTemplateId = action.payload;
},
clearSelection: (state) => {
state.selectedTemplateId = null;
},
},
});
export const { selectTemplateById, clearSelection } = creationTemplateSlice.actions;
export default creationTemplateSlice.reducer;

View File

@ -1,11 +1,13 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import workflowReducer from './workflowSlice'; import workflowReducer from './workflowSlice';
import serverSettingReducer from './serverSettingSlice'; import serverSettingReducer from './serverSettingSlice';
import creationTemplateReducer from './creationTemplateSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
workflow: workflowReducer, workflow: workflowReducer,
serverSetting: serverSettingReducer, serverSetting: serverSettingReducer,
creationTemplate: creationTemplateReducer,
}, },
}); });

View File

@ -78,6 +78,10 @@ module.exports = {
'0%, 60%': { transform: 'rotate(0deg)' }, '0%, 60%': { transform: 'rotate(0deg)' },
'70%, 90%': { transform: 'rotate(-6deg)' }, '70%, 90%': { transform: 'rotate(-6deg)' },
'80%': { transform: 'rotate(6deg)' }, '80%': { transform: 'rotate(6deg)' },
},
'ring-breath': {
'0%, 100%': { '--tw-ring-opacity': '0' },
'50%': { '--tw-ring-opacity': '0.6' },
} }
}, },
animation: { animation: {
@ -85,6 +89,7 @@ module.exports = {
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"liquid-toggle": "liquid-toggle 2s ease-in-out infinite", "liquid-toggle": "liquid-toggle 2s ease-in-out infinite",
"wiggle": "wiggle 1s ease-in-out infinite", "wiggle": "wiggle 1s ease-in-out infinite",
"ring-breath": "ring-breath 1.8s ease-in-out infinite",
}, },
transitionDelay: { transitionDelay: {
'100': '100ms', '100': '100ms',