new landing completed (#583)

new landing

new landing update

styling updates

update

more updates

marquee section

updates

buttons update

landing email sign up

update

mobile update

hanashi hide small screen

removed unused package

updates

mobile video size fix

mobile remove laptop because video too small

cta for logged in users with access
This commit is contained in:
BFlat authored and GitHub committed 2024-06-18 21:08:48 -04:00
1 parent ee2226a16d
commit dde3dfa2df
31 files changed
+978 -43

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@
"dependencies": {
"@react-spring/types": "^9.7.3",
"@typeform/embed": "^4.8.1",
"@typeform/embed-react": "^3.15.0",
"framer-motion": "6.5.1",
"local-storage": "^2.0.0",
"moment": "^2.29.4",
"universal-cookie": "^6.1.1",
+2
View File
@@ -23,6 +23,7 @@
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@tippyjs/react": "^4.2.6",
"@typeform/embed-react": "^3.17.0",
"@types/axios": "^0.14.0",
"@types/deep-equal": "^1.0.3",
"@types/google.analytics": "^0.0.42",
@@ -64,6 +65,7 @@
"react-device-detect": "^2.2.3",
"react-dom": "^16.13.1",
"react-drag-drop-files": "^2.3.9",
"react-fast-marquee": "^1.6.4",
"react-i18next": "^11.15.4",
"react-iframe": "^1.8.5",
"react-infinite-scroll-component": "^6.1.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

-1
View File
@@ -55,7 +55,6 @@ $headings-font-family: "Fira Sans", sans-serif;
#main {
position: relative; // Footer attachment.
min-height: calc(100vh - 66px);
overflow: hidden;
}
.social {
@@ -113,6 +113,10 @@
border: none;
}
.button-font-large {
font-size: 18px !important;
}
.button-link {
border: none;
border-radius: 0;
@@ -24,6 +24,7 @@ export default function Button({
isLoading = false,
isActive,
type = "button",
fontLarge,
...rest
}: ButtonProps) {
let variant = variantProps;
@@ -60,6 +61,8 @@ export default function Button({
const commonProps = {
className: `${externalClass} button ${small ? "button-small" : ""} ${
fontLarge ? "button-font-large" : ""
} ${
square ? (small ? "button-square-small" : "button-square") : ""
} button-${variant} ${full ? "w-100" : ""}`,
disabled: disabled || isLoading,
@@ -1,7 +1,8 @@
import { ButtonHTMLAttributes } from "react";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
export default interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
export default interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement> {
buttonRef?: any;
label?: string | null;
icon?: IconDefinition;
@@ -19,4 +20,5 @@ export default interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElem
disabled?: boolean;
isLoading?: boolean;
isActive?: boolean;
}
fontLarge?: boolean;
}
@@ -1,7 +1,8 @@
@import "scss/colours.scss";
.topbar-bg-transparent {
background-color: transparent;
background-color: transparent !important;
border-bottom: 1px solid transparent !important;
}
.topbar-bg-dark {
@@ -215,9 +215,9 @@ export default function TopNav({
domain.titlePart === "Storyteller AI" &&
isOnLandingPage
) {
topBarWrapper.classList.add("topbar-hide-top");
topBarWrapper.classList.add("topbar-bg-transparent");
} else {
topBarWrapper?.classList.remove("topbar-hide-top");
topBarWrapper?.classList.remove("topbar-bg-transparent");
}
useEffect(() => {
@@ -227,10 +227,10 @@ export default function TopNav({
domain.titlePart === "Storyteller AI" &&
isOnLandingPage
) {
if (window.scrollY > 100) {
topBarWrapper.classList.remove("topbar-hide-top");
if (window.scrollY > 500) {
topBarWrapper.classList.remove("topbar-bg-transparent");
} else {
topBarWrapper.classList.add("topbar-hide-top");
topBarWrapper.classList.add("topbar-bg-transparent");
}
}
};
@@ -349,7 +349,7 @@ export default function TopNav({
return (
<>
{domain.titlePart === "Storyteller AI" &&
{/* {domain.titlePart === "Storyteller AI" &&
isOnLandingPage &&
!isScrolled && (
<div
@@ -394,7 +394,7 @@ export default function TopNav({
</>
)}
</div>
)}
)} */}
<div
id="topbar-wrapper"
className={`position-fixed ${
@@ -478,17 +478,18 @@ export default function TopNav({
</div>
<div className="topbar-nav-right">
{domain.titlePart === "Storyteller AI" && (
<div className="d-none d-lg-block">
<Button
icon={faPortalEnter}
label="Enter Storyteller Studio"
href="https://studio.storyteller.ai/"
small={true}
className="me-2"
/>
</div>
)}
{domain.titlePart === "Storyteller AI" &&
sessionWrapper.canAccessStudio() && (
<div className="d-none d-lg-block">
<Button
icon={faPortalEnter}
label="Enter Storyteller Studio"
href="https://studio.storyteller.ai/"
small={true}
className="me-2"
/>
</div>
)}
<div className="d-flex align-items-center gap-2">
<div className="d-none d-lg-flex gap-2">
+4
View File
@@ -298,4 +298,8 @@ $topbar-height: 66px;
.sidebar-nav {
width: 100%;
}
#page-content-wrapper {
overflow: hidden;
}
}
@@ -91,9 +91,9 @@ styles with important tags:
width: 10px;
}
body {
overflow-y: scroll;
}
// body {
// overflow-y: scroll;
// }
p {
font-weight: 400;
@@ -12,7 +12,7 @@ import {
faSignOutAlt,
faBook,
faStar,
faWandMagicSparkles
faWandMagicSparkles,
} from "@fortawesome/free-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { Logout } from "@storyteller/components/src/api/session/Logout";
@@ -160,7 +160,7 @@ function TopNav(props: Props) {
}
return (
<div className="d-none d-lg-block">
<div className="d-none d-lg-block overflow-hidden vw-100">
<nav
className="navbar navbar-expand-lg navbar-dark pt-3"
aria-label="Offcanvas navbar large"
@@ -18,7 +18,8 @@ import {
} from "@storyteller/components/src/env/GetWebsite";
// import OnboardingSelection from "./storyteller/OnboardingSelection";
// import TtsDemoSection from "./components/TtsDemoSection/TtsDemoSection";
import PrelaunchLanding from "./storyteller/PrelaunchLanding/PrelaunchLanding";
// import PrelaunchLanding from "./storyteller/PrelaunchLanding/PrelaunchLanding";
import PostlaunchLanding from "./storyteller/PostlaunchLanding/PostlaunchLanding";
interface Props {
sessionWrapper: SessionWrapper;
@@ -54,7 +55,8 @@ function LandingPage(props: Props) {
<>
{domain.website === Website.StorytellerAi && (
// <LandingVideoReel sessionWrapper={props.sessionWrapper} />
<PrelaunchLanding sessionWrapper={props.sessionWrapper} />
// <PrelaunchLanding sessionWrapper={props.sessionWrapper} />
<PostlaunchLanding sessionWrapper={props.sessionWrapper} />
)}
<Container type="panel">
{
@@ -0,0 +1,50 @@
import React, { useState } from "react";
import { Widget } from "@typeform/embed-react";
interface EmailSignUpProps {
mobile?: boolean;
}
export default function EmailSignUp({ mobile }: EmailSignUpProps) {
const [isHanashiHovered, setIsHanashiHovered] = useState(false);
return (
<div>
<h1 className="text-center mb-5 fw-bold display-5">
Join the waitlist today!
</h1>
<div
style={{
paddingTop: mobile ? "0px" : "60px",
backgroundColor: "#242433",
borderRadius: "1rem",
borderTop: "3px solid #e66462",
position: "relative",
}}
>
{!mobile && (
<img
src={
isHanashiHovered
? "/images/landing/hanashi-demo-2.webp"
: "/images/landing/hanashi-demo-1.webp"
}
alt="Hanashi Demo"
onMouseEnter={() => setIsHanashiHovered(true)}
onMouseLeave={() => setIsHanashiHovered(false)}
draggable="false"
style={{
top: "-49%",
right: "3%",
width: "390px",
position: "absolute",
}}
className="d-none d-xxl-block"
/>
)}
<Widget id="TKSc5ImN" style={{ width: "100%", height: "300px" }} />
</div>
</div>
);
}
@@ -0,0 +1,96 @@
import React from "react";
import { useFeatureStore } from "./store";
interface FeatureCardProps extends CardProps {
children: React.ReactNode;
}
interface CardProps {
id: string;
}
function FeatureCard({ children, id }: FeatureCardProps) {
const inViewFeature = useFeatureStore((state: any) => state.inViewFeature);
return (
<div
className="h-100 w-100 overflow-hidden"
style={{
opacity: inViewFeature === id ? 1 : 0,
transition: "all 0.25s",
borderRadius: "1rem",
border: "2px solid rgba(255, 255, 255, 0.06)",
}}
>
{children}
</div>
);
}
export function BuildScene({ id }: CardProps) {
return (
<FeatureCard id={id}>
<video
src="/videos/landing/build_scene.mp4"
className="object-fit-contain w-100 h-100"
preload="metadata"
muted={true}
autoPlay={true}
controls={false}
loop={true}
playsInline={true}
/>
</FeatureCard>
);
}
export function Animate({ id }: CardProps) {
return (
<FeatureCard id={id}>
<video
src="/videos/landing/animate_scene.mp4"
className="object-fit-contain w-100 h-100"
preload="metadata"
muted={true}
autoPlay={true}
controls={false}
loop={true}
playsInline={true}
/>
</FeatureCard>
);
}
export function SelectStyle({ id }: CardProps) {
return (
<FeatureCard id={id}>
<video
src="/videos/landing/select_style.mp4"
className="object-fit-contain w-100 h-100"
preload="metadata"
muted={true}
autoPlay={true}
controls={false}
loop={true}
playsInline={true}
/>
</FeatureCard>
);
}
export function GenerateMovie({ id }: CardProps) {
return (
<FeatureCard id={id}>
<video
src="/videos/landing/generate_movie.mp4"
className="object-fit-contain w-100 h-100"
preload="metadata"
muted={true}
autoPlay={true}
controls={false}
loop={true}
playsInline={true}
/>
</FeatureCard>
);
}
@@ -0,0 +1,76 @@
import React, { useEffect, useRef } from "react";
import { useInView } from "framer-motion";
import { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useFeatureStore } from "./store";
interface FeatureTitleProps {
title: string;
description: string;
icon: IconDefinition;
id: string;
}
export default function FeatureTitle({
title,
description,
icon,
id,
}: FeatureTitleProps) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { margin: "-50% 0px -50% 0px" });
const setInViewFeature = useFeatureStore(
(state: any) => state.setInViewFeature
);
const inViewFeature = useFeatureStore((state: any) => state.inViewFeature);
useEffect(() => {
if (isInView) {
setInViewFeature(id);
}
if (!isInView && inViewFeature === id) {
setInViewFeature(null);
}
}, [isInView, id, setInViewFeature, inViewFeature]);
return (
<div ref={ref} style={{ paddingBottom: "120px", position: "relative" }}>
<div
style={{
padding: "2.5rem",
transition: "all 0.25s",
opacity: !isInView ? 0.2 : 1,
transform: !isInView ? "scale(0.90)" : "scale(1)",
transformOrigin: "right center",
backgroundColor: "#242433",
border: !isInView
? "2px solid rgba(255, 255, 255, 0.04)"
: "2px solid #e66462",
borderRadius: "1rem",
position: "relative",
zIndex: 2,
}}
>
<h2 className="fs-2 fw-bold mb-4">
<FontAwesomeIcon icon={icon} className="me-3" />
{title}
</h2>
<p className="opacity-75" style={{ fontSize: "18px" }}>
{description}
</p>
</div>
{/* <div
style={{
width: "10px",
height: "140px",
position: "absolute",
left: "50%",
bottom: "-10px",
backgroundColor: "rgba(255, 255, 255, 0.05)",
zIndex: 1,
display: isLastChild ? "none" : "block", // Conditionally show/hide based on being the last child
}}
/> */}
</div>
);
}
@@ -0,0 +1,483 @@
import {
Website,
WebsiteConfig,
} from "@storyteller/components/src/env/GetWebsite";
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
import { Button } from "components/common";
import { useDomainConfig } from "context/DomainConfigContext";
import React, { useEffect, useRef, useState } from "react";
import { useScroll, useTransform, motion } from "framer-motion";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import FeatureTitle from "./FeatureTitle";
import { Animate, BuildScene, GenerateMovie, SelectStyle } from "./FeatureCard";
import {
faArrowRight,
faCube,
faFilm,
faPaintbrushPencil,
faPersonRunning,
} from "@fortawesome/pro-solid-svg-icons";
import ScrollingSceneCarousel from "./ScrollingSceneCarousel";
import EmailSignUp from "./EmailSignUp";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface PostlaunchLandingProps {
sessionWrapper: SessionWrapper;
}
export default function PostlaunchLanding(props: PostlaunchLandingProps) {
const [imageHeight, setImageHeight] = useState("100vh");
const domain: WebsiteConfig = useDomainConfig();
const webpageTitle =
domain.website === Website.FakeYou
? "FakeYou Celebrity Voice Generator"
: "AI Creation Engine";
usePrefixedDocumentTitle(webpageTitle);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 992);
const handleResize = () => {
setIsMobile(window.innerWidth <= 992);
};
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
useEffect(() => {
const updateImageSize = () => {
const headerHeight =
document.querySelector(".header-container")?.clientHeight || 0;
const topPadding = 20;
const bottomPadding = 50;
const windowHeight = window.innerHeight;
const availableHeight =
windowHeight - headerHeight - topPadding - bottomPadding;
setImageHeight(`${availableHeight}px`);
};
window.addEventListener("resize", updateImageSize);
updateImageSize();
return () => window.removeEventListener("resize", updateImageSize);
}, []);
const [isSmallScreen, setIsSmallScreen] = useState(window.innerHeight > 1000);
useEffect(() => {
const handleResize = () => {
setIsSmallScreen(window.innerHeight > 1000);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
let scaleFactor;
if (isSmallScreen) {
scaleFactor = 3.5;
} else {
scaleFactor = 4.5;
}
const container = useRef(null);
const { scrollYProgress } = useScroll({
target: container,
offset: ["start start", "end end"],
});
const scaleFull = useTransform(scrollYProgress, [0, 1], [1, scaleFactor]);
const opacityLaptop = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const opacityTitle = useTransform(scrollYProgress, [0, 0.2], [1, 0]);
const opacityOverlay = useTransform(scrollYProgress, [0.6, 1], [0, 1]);
const ctaButton = props.sessionWrapper.canAccessStudio() ? (
<div className="d-flex">
<Button
label="Enter Storyteller Studio"
className="mt-4"
fontLarge={true}
icon={faArrowRight}
iconFlip={true}
href="https://studio.storyteller.ai/"
/>
</div>
) : (
<div className="d-flex">
<Button
label="Join the Waitlist"
className="mt-4"
fontLarge={true}
icon={faArrowRight}
iconFlip={true}
href="https://7mjlxvmjq8u.typeform.com/to/ZQTkv9ha"
/>
</div>
);
return (
<>
{isMobile ? (
// MOBILE VIEW
<div className="container">
<div
className="header-container text-center d-flex flex-column align-items-center justify-content-center mb-5"
style={{ paddingTop: "80px" }}
>
<h1 className="display-1 fw-bold mt-4">{firstTitle}</h1>
<p className="lead fw-medium fs-4 opacity-75">{firstSubtext}</p>
{ctaButton}
</div>
<div
className="d-flex justify-content-center align-items-center position-relative"
style={{
width: "100%",
zIndex: 10,
}}
>
<video
src="/videos/landing/hero_video.mp4"
preload="metadata"
style={{
width: "100%",
maxWidth: "900px",
borderRadius: "0.75rem",
overflow: "hidden",
border: "2px solid rgba(255, 255, 255, 0.1)",
}}
autoPlay={true}
controls={false}
muted={true}
loop={true}
playsInline={true}
/>
</div>
<div style={{ marginTop: "60px", marginBottom: "60px" }}>
<ScrollingSceneCarousel small={true} />
</div>
<div
className="container d-flex flex-column text-center justify-content-center align-items-center w-100"
style={{ marginBottom: "70px" }}
>
<div className="d-flex flex-column align-items-center">
<h2 className="display-4 fw-bold mt-4">{secondTitle}</h2>
<p className="lead fw-normal fs-5 opacity-75">{secondSubtext}</p>
{ctaButton}
</div>
</div>
<div className="container d-flex flex-column gap-5">
{features.map(feature => (
<li key={feature.id} className="list-unstyled d-flex flex-column">
<video
src={feature.video}
className="object-fit-contain w-100 h-100 mb-4"
preload="metadata"
muted={true}
autoPlay={true}
controls={false}
loop={true}
playsInline={true}
style={{ borderRadius: "1rem" }}
/>
<h2 className="fs-2 fw-bold mb-3">
<FontAwesomeIcon icon={feature.icon} className="me-3" />
{feature.title}
</h2>
<p className="opacity-75 mb-5">{feature.description}</p>
</li>
))}
</div>
<div
className="container d-flex flex-column align-items-center"
style={{ marginTop: "60px" }}
>
<h1 className="display-5 fw-bold">Showcase</h1>
<p className="lead text-center fw-medium opacity-75 fs-5 mb-5">
Videos created with Storyteller Studio.
</p>
<video
src="/videos/landing/landing_reel.mp4"
poster="/images/landing/storyteller/Landing_Reel_Poster.png"
preload="metadata"
style={{
width: "100%",
maxWidth: "900px",
borderRadius: "0.75rem",
overflow: "hidden",
border: "2px solid rgba(255, 255, 255, 0.1)",
}}
autoPlay={true}
controls={false}
muted={true}
loop={true}
playsInline={true}
/>
{props.sessionWrapper.isLoggedIn() && ctaButton}
</div>
{!props.sessionWrapper.canAccessStudio() ? (
<div style={{ marginTop: "100px" }}>
<EmailSignUp mobile={true} />
</div>
) : null}
</div>
) : (
// DESKTOP VIEW
<div>
<div
ref={container}
style={{
height: "150vh",
position: "relative",
}}
>
<div
className="vh-100 w-100"
style={{ position: "sticky", top: 0, overflow: "hidden" }}
>
<motion.div
className="header-container text-center d-flex flex-column align-items-center justify-content-center"
style={{ paddingTop: "50px", opacity: opacityTitle }}
>
<h1 className="display-1 fw-bold mt-4">{firstTitle}</h1>
<p className="lead fw-medium fs-4 opacity-75">{firstSubtext}</p>
{ctaButton}
</motion.div>
<div
style={{
flexGrow: 1,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "32px 0",
}}
>
<motion.div
className="d-flex justify-content-center align-items-center position-relative"
style={{
width: "100%",
maxHeight: imageHeight,
scale: scaleFull,
zIndex: 10,
}}
>
<video
src="/videos/landing/hero_video.mp4"
className="position-absolute"
preload="metadata"
style={{
height: "52%",
top: "20.55%",
zIndex: 10,
borderRadius: "0.25rem",
}}
autoPlay={true}
controls={false}
muted={true}
loop={true}
playsInline={true}
/>
<motion.div
className="position-absolute"
style={{
width: "40%",
height: "52%",
top: "20.55%",
zIndex: 11,
backgroundColor: "rgba(0, 0, 0, 0.7)",
opacity: opacityOverlay,
borderRadius: "0.25rem",
}}
/>
<motion.img
src="/images/landing/storyteller/Laptop_Storyteller_2.png"
alt="Laptop"
style={{
maxHeight: imageHeight,
objectFit: "contain",
width: "auto",
opacity: opacityLaptop,
userSelect: "none",
pointerEvents: "none",
}}
/>
</motion.div>
</div>
</div>
<div
className="position-absolute w-100"
style={{ bottom: isSmallScreen ? "240px" : "150px" }}
>
<div className="d-flex flex-column justify-content-center align-items-center w-100">
<div>
<h2
className="display-4 fw-bold mt-4"
style={{ textShadow: "2px 2px 10px rgba(0, 0, 0, 0.3)" }}
>
{secondTitle}
</h2>
<p
className="lead fw-medium fs-5 opacity-75"
style={{ textShadow: "2px 2px 10px rgba(0, 0, 0, 0.3)" }}
>
{secondSubtext}
</p>
{ctaButton}
</div>
</div>
</div>
</div>
{/* SCENE CAROUSEL SECTION */}
<ScrollingSceneCarousel />
<div className="container">
<div className="text-center">
<h1 className="fw-bold display-4" style={{ marginTop: "140px" }}>
{thirdTitle}
</h1>
<p
className="lead fw-medium opacity-75 fs-4"
style={{ marginBottom: !isSmallScreen ? "-10%" : "-22%" }}
>
{thirdSubtext}
</p>
</div>
<div
className="d-flex w-100 align-items-start"
style={{ gap: "40px" }}
>
<div className="w-100">
<ul
className="list-unstyled"
style={{ paddingTop: "50vh", paddingBottom: "33vh" }}
>
{features.map(feature => (
<li key={feature.id}>
<FeatureTitle
id={feature.id}
title={feature.title}
icon={feature.icon}
description={feature.description}
/>
</li>
))}
</ul>
</div>
<div
className="w-100 position-sticky top-0 d-flex align-items-center justify-content-center"
style={{ height: "100vh", marginTop: "120px" }}
>
<div
className="ratio ratio-1x1"
style={{
width: !isSmallScreen ? "600px" : "650px",
backgroundColor: "#242433",
borderRadius: "1rem",
}}
>
{features.map(feature => (
<feature.card id={feature.id} key={feature.id} />
))}
</div>
</div>
</div>
<div
className="container d-flex flex-column align-items-center"
style={{ marginTop: "-50px" }}
>
<h1 className="display-5 fw-bold">Showcase</h1>
<p className="lead fw-medium opacity-75 fs-4 mb-5">
Videos created with Storyteller Studio.
</p>
<video
src="/videos/landing/landing_reel.mp4"
poster="/images/landing/storyteller/Landing_Reel_Poster.png"
preload="metadata"
style={{
width: "100%",
maxWidth: "900px",
borderRadius: "1rem",
overflow: "hidden",
border: "2px solid rgba(255, 255, 255, 0.1)",
}}
autoPlay={true}
controls={false}
muted={true}
loop={true}
playsInline={true}
/>
{props.sessionWrapper.isLoggedIn() && ctaButton}
</div>
{!props.sessionWrapper.canAccessStudio() ? (
<div style={{ marginTop: "260px" }}>
<EmailSignUp />
</div>
) : null}
</div>
</div>
)}
</>
);
}
const firstTitle = "Control Your Movie";
const firstSubtext =
"Effortlessly create your movie in any style you want with AI.";
const secondTitle = "Your AI 3D Creation Engine";
const secondSubtext =
"Turn your creative ideas into stunning visuals, by simply building a 3D scene and generate.";
const thirdTitle = "Filmmaking Made Easy";
const thirdSubtext = "Just a few simple steps to creating your movie.";
const features = [
{
title: "Build your 3D scene",
icon: faCube,
description:
" Storyteller Studio allows you to create and customize your 3D environment. Add characters, objects, and fine-tune details to craft the perfect scene for your movie.",
id: "build-scene",
card: BuildScene,
video: "/videos/landing/build_scene.mp4",
},
{
title: "Animate your scene",
icon: faPersonRunning,
description:
"Bring your scene to life by adding animations to your characters and objects. Control movements to create dynamic visuals that engage your audience.",
id: "animate-scene",
card: Animate,
video: "/videos/landing/animate_scene.mp4",
},
{
title: "Select a style",
icon: faPaintbrushPencil,
description:
"Choose from a variety of artistic styles to transform your 3D scene. Whether you prefer a realistic look or a more abstract approach, our AI can apply the style seamlessly.",
id: "select-style",
card: SelectStyle,
video: "/videos/landing/select_style.mp4",
},
{
title: "Generate your movie",
icon: faFilm,
description:
"Let Storyteller Studio's AI process your scene and selected style to produce a stunning video. Sit back and watch as your 3D creation comes to life with the chosen visual art style.",
id: "generate-movie",
card: GenerateMovie,
video: "/videos/landing/generate_movie.mp4",
},
];
@@ -0,0 +1,72 @@
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "components/common";
import { AnimatePresence, motion } from "framer-motion";
import React, { useState } from "react";
interface SceneCardProps {
image: string;
alt: string;
title?: string;
token?: string;
small?: boolean;
}
export default function SceneCard({
image,
alt,
title,
token,
small,
}: SceneCardProps) {
const [showOverlay, setShowOverlay] = useState(false);
return (
<motion.div
className="position-relative overflow-hidden rounded"
style={{ width: "max-content", marginRight: small ? "24px" : "32px" }}
onHoverStart={() => setShowOverlay(true)}
onHoverEnd={() => setShowOverlay(false)}
>
<AnimatePresence>
{showOverlay && (
<motion.div
className="position-absolute w-100 h-100 p-2"
style={{ backgroundColor: "rgba(0, 0, 0, 0.3)" }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="d-flex justify-content-end w-100">
<Button
label="View"
variant="action"
small={true}
icon={faArrowRight}
iconFlip={true}
to={`/media/${token}`}
/>
</div>
<motion.p
className="position-absolute bottom-0 left-0 ps-2 pb-2 fw-medium"
style={{
textShadow: "2px 2px 10px rgba(0, 0, 0, 0.7)",
maxWidth: "240px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
initial={{ y: 10 }}
animate={{ y: 0 }}
exit={{ y: 10 }}
transition={{ duration: 0.2 }}
>
{title}
</motion.p>
</motion.div>
)}
</AnimatePresence>
<img src={image} alt={alt} width={small ? 180 : 340} />
</motion.div>
);
}
@@ -0,0 +1,110 @@
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import React, { useEffect, useState } from "react";
import SceneCard from "./SceneCard";
import Marquee from "react-fast-marquee";
interface ScrollingSceneCarouselProps {
small?: boolean;
}
interface MediaItem {
token: string;
public_bucket_path: string;
maybe_creator?: {
username: string;
};
maybe_title?: string;
}
export default function ScrollingSceneCarousel({
small,
}: ScrollingSceneCarouselProps) {
const [mediaItems, setMediaItems] = useState<MediaItem[]>([]);
const bucketConfig = new BucketConfig();
useEffect(() => {
fetchMediaItems();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const fetchMediaItems = async () => {
try {
const response = await fetch(
"https://api.storyteller.ai/v1/media_files/list_featured?page_size=100&filter_media_classes=video"
);
const data = await response.json();
if (data.success) {
setMediaItems(shuffleArray(data.results));
}
} catch (error) {
console.error("Error fetching media items:", error);
}
};
const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
const halfLength = Math.ceil(mediaItems.length / 2);
const firstHalf = mediaItems.slice(0, halfLength);
const secondHalf = mediaItems.slice(halfLength);
return (
<div className="d-flex gap-5 mt-5 overflow-hidden flex-column">
<Marquee
gradient={true}
gradientColor="#1a1a27"
gradientWidth={small ? 80 : 200}
speed={small ? 100 : 50}
pauseOnHover={true}
direction="left"
>
{firstHalf.map((item, index) => (
<div key={index}>
<SceneCard
image={bucketConfig.getCdnUrl(
item.public_bucket_path + "-thumb.gif",
360,
20
)}
alt={`Scene ${index}`}
title={item.maybe_title || "Scene"}
token={item.token}
small={small}
/>
</div>
))}
</Marquee>
{!small && (
<Marquee
gradient={true}
gradientColor="#1a1a27"
gradientWidth={small ? 80 : 200}
speed={small ? 100 : 50}
pauseOnHover={true}
direction="right"
>
{secondHalf.map((item, index) => (
<div key={index}>
<SceneCard
image={bucketConfig.getCdnUrl(
item.public_bucket_path + "-thumb.gif",
360,
20
)}
alt={`Scene ${index}`}
title={item.maybe_title || "Scene"}
token={item.token}
small={small}
/>
</div>
))}
</Marquee>
)}
</div>
);
}
@@ -0,0 +1,11 @@
import { create } from "zustand";
type FeatureStore = {
inViewFeature: string | null;
setInViewFeature: (feature: string | null) => void;
};
export const useFeatureStore = create<FeatureStore>(set => ({
inViewFeature: null,
setInViewFeature: (feature: string | null) => set({ inViewFeature: feature }),
}));
@@ -160,7 +160,7 @@ function PrelaunchLanding(props: Props) {
) : mediaItem ? (
<img
onClick={() =>
window.location.href = `/media/${mediaItem.token}`
(window.location.href = `/media/${mediaItem.token}`)
}
src={gifUrl}
className="w-100 h-100 object-fit-cover"
@@ -174,6 +174,6 @@ function PrelaunchLanding(props: Props) {
};
return <div className="prelaunch-grid-container">{renderGridItems()}</div>;
};
}
export default PrelaunchLanding;
+30 -11
View File
@@ -4399,6 +4399,7 @@ __metadata:
"@testing-library/react": ^9.5.0
"@testing-library/user-event": ^7.2.1
"@tippyjs/react": ^4.2.6
"@typeform/embed-react": ^3.17.0
"@types/axios": ^0.14.0
"@types/deep-equal": ^1.0.3
"@types/google.analytics": ^0.0.42
@@ -4444,6 +4445,7 @@ __metadata:
react-device-detect: ^2.2.3
react-dom: ^16.13.1
react-drag-drop-files: ^2.3.9
react-fast-marquee: ^1.6.4
react-i18next: ^11.15.4
react-iframe: ^1.8.5
react-infinite-scroll-component: ^6.1.0
@@ -4474,9 +4476,9 @@ __metadata:
dependencies:
"@react-spring/types": ^9.7.3
"@typeform/embed": ^4.8.1
"@typeform/embed-react": ^3.15.0
craco: ^0.0.3
cross-env: ^7.0.3
framer-motion: 6.5.1
husky: ^8.0.3
lerna: ^4.0.0
local-storage: ^2.0.0
@@ -4937,22 +4939,22 @@ __metadata:
languageName: node
linkType: hard
"@typeform/embed-react@npm:^3.15.0":
version: 3.15.0
resolution: "@typeform/embed-react@npm:3.15.0"
"@typeform/embed-react@npm:^3.17.0":
version: 3.17.0
resolution: "@typeform/embed-react@npm:3.17.0"
dependencies:
"@typeform/embed": 4.8.1
"@typeform/embed": 4.10.0
fast-deep-equal: ^3.1.3
peerDependencies:
react: ">=16.8.0"
checksum: d5e5d4b1cdfc128de2ce6c17ba648adbe98bfecc5a2c296fbedc8fb32da0c302e813baa6ce3ecc3de9dc84f36ccfd42250a4693f846d84ce2574e6fedcaa86e5
checksum: 90d58df3124de46eda7db5f71fd478d4b57ee952b7da04b3eb64c74036e4830a8e5ea7f4bce67eb55827af91cdda0fa107fc2aa13801b0890c8c611af742584a
languageName: node
linkType: hard
"@typeform/embed@npm:4.8.1, @typeform/embed@npm:^4.8.1":
version: 4.8.1
resolution: "@typeform/embed@npm:4.8.1"
checksum: 1e7ac3d0d95c1ea823160f1478a973f871550a4bab7de1501f557b1e18966ab5a740b6a913d9436b5ee59d0f25d707b66582a8c41dea195383cca3afb1955b23
"@typeform/embed@npm:4.10.0":
version: 4.10.0
resolution: "@typeform/embed@npm:4.10.0"
checksum: 388c409428af05c8db7066b07ab85b78ce2ed3de9ed9f020c464a92d5fc2c7fb4c6081cb9311d93dd18d251381401be6ab427f1ec44aeca548e27d9b5f454780
languageName: node
linkType: hard
@@ -4963,6 +4965,13 @@ __metadata:
languageName: node
linkType: hard
"@typeform/embed@npm:^4.8.1":
version: 4.8.1
resolution: "@typeform/embed@npm:4.8.1"
checksum: 1e7ac3d0d95c1ea823160f1478a973f871550a4bab7de1501f557b1e18966ab5a740b6a913d9436b5ee59d0f25d707b66582a8c41dea195383cca3afb1955b23
languageName: node
linkType: hard
"@types/aria-query@npm:^4.2.0":
version: 4.2.2
resolution: "@types/aria-query@npm:4.2.2"
@@ -11084,7 +11093,7 @@ __metadata:
languageName: node
linkType: hard
"framer-motion@npm:^6.5.1":
"framer-motion@npm:6.5.1, framer-motion@npm:^6.5.1":
version: 6.5.1
resolution: "framer-motion@npm:6.5.1"
dependencies:
@@ -17800,6 +17809,16 @@ __metadata:
languageName: node
linkType: hard
"react-fast-marquee@npm:^1.6.4":
version: 1.6.4
resolution: "react-fast-marquee@npm:1.6.4"
peerDependencies:
react: ">= 16.8.0 || 18.0.0"
react-dom: ">= 16.8.0 || 18.0.0"
checksum: 30bb2c967b5162e7de130cccfcf51654ac1e29ecbf82116225a13958a05a79bc0ec11d72c49f172fa8c4d1f226b51cc93f9db5cb595c5a3529de8b41f60cbc2c
languageName: node
linkType: hard
"react-i18next@npm:^11.15.4":
version: 11.15.4
resolution: "react-i18next@npm:11.15.4"