forked from 77media/video-flow
新增 sticky banner
This commit is contained in:
parent
70198d3f43
commit
cd132c670e
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
29
lib/store/creationTemplateSlice.ts
Normal file
29
lib/store/creationTemplateSlice.ts
Normal 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;
|
||||||
|
|
||||||
|
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user