mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 02:00:46 +00:00
Merge branch 'feature/mvp' of https://github.com/storytold/storyteller-frontend into feature/mvp
This commit is contained in:
commit
258f76591c
38 files changed
+2651
-1976
No files matched your search
@@ -7,6 +7,7 @@ interface Props {
|
||||
username?: string;
|
||||
avatarIndex?: number;
|
||||
backgroundIndex?: number;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function Gravatar(props: Props) {
|
||||
@@ -35,6 +36,7 @@ function Gravatar(props: Props) {
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { GetSessionInfo, GetSessionInfoIsOk, SessionInfoSuccessResponse } from '../api/session/GetSessionInfo'
|
||||
import { StudioRolloutHostnameAllowed } from '../utils/StudioRolloutHostnameAllowed';
|
||||
import {
|
||||
GetSessionInfo,
|
||||
GetSessionInfoIsOk,
|
||||
SessionInfoSuccessResponse,
|
||||
} from "../api/session/GetSessionInfo";
|
||||
import { StudioRolloutHostnameAllowed } from "../utils/StudioRolloutHostnameAllowed";
|
||||
|
||||
// A lot of the APIs return null or leave values absent.
|
||||
// A lot of the APIs return null or leave values absent.
|
||||
// I need to pick a single strategy and stick with it.
|
||||
type MaybeString = string | null | undefined;
|
||||
|
||||
/**
|
||||
* SessionWrapper can wrap each of the following states:
|
||||
*
|
||||
*
|
||||
* 1) logged-in session
|
||||
* 2) non-existing session (logged out)
|
||||
* 3) lookup errors, server errors, 401, etc. (logged out)
|
||||
*
|
||||
*
|
||||
* It then makes frontend permission checking simple.
|
||||
*/
|
||||
export class SessionWrapper {
|
||||
@@ -24,7 +28,7 @@ export class SessionWrapper {
|
||||
}
|
||||
|
||||
// Perform lookup and return a wrapper of a valid, inval
|
||||
public static async lookupSession() : Promise<SessionWrapper> {
|
||||
public static async lookupSession(): Promise<SessionWrapper> {
|
||||
let response = await GetSessionInfo();
|
||||
if (GetSessionInfoIsOk(response)) {
|
||||
return SessionWrapper.wrapResponse(response);
|
||||
@@ -33,165 +37,198 @@ export class SessionWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public static emptySession() : SessionWrapper {
|
||||
public static emptySession(): SessionWrapper {
|
||||
return new SessionWrapper();
|
||||
}
|
||||
|
||||
public static wrapResponse(sessionStateResponse: SessionInfoSuccessResponse) : SessionWrapper {
|
||||
public static wrapResponse(
|
||||
sessionStateResponse: SessionInfoSuccessResponse
|
||||
): SessionWrapper {
|
||||
return new SessionWrapper(sessionStateResponse);
|
||||
}
|
||||
|
||||
public isLoggedIn() : boolean {
|
||||
public isLoggedIn(): boolean {
|
||||
return this.sessionStateResponse?.logged_in || false;
|
||||
}
|
||||
|
||||
// Feature flag rollout for Storyteller Studio
|
||||
// This depends on the domain name and the backend.
|
||||
public canAccessStudio() : boolean {
|
||||
public canAccessStudio(): boolean {
|
||||
const hostnameAllowed = StudioRolloutHostnameAllowed();
|
||||
const userAllowed = this.sessionStateResponse?.user?.can_access_studio || false;
|
||||
const userAllowed =
|
||||
this.sessionStateResponse?.user?.can_access_studio || false;
|
||||
return hostnameAllowed && userAllowed;
|
||||
}
|
||||
|
||||
// Username is all lowercase
|
||||
public getUsername() : string | undefined {
|
||||
public getUsername(): string | undefined {
|
||||
return this.sessionStateResponse?.user?.username;
|
||||
}
|
||||
|
||||
// Display name has capitalization
|
||||
public getDisplayName() : string | undefined {
|
||||
public getDisplayName(): string | undefined {
|
||||
return this.sessionStateResponse?.user?.display_name;
|
||||
}
|
||||
|
||||
public getEmailGravatarHash() : string | undefined {
|
||||
public getEmailGravatarHash(): string | undefined {
|
||||
return this.sessionStateResponse?.user?.email_gravatar_hash;
|
||||
}
|
||||
|
||||
public canEditUserProfile(username: string) : boolean {
|
||||
public canEditUserProfile(username: string): boolean {
|
||||
if (this.getUsername() === username) {
|
||||
return true;
|
||||
}
|
||||
return this.canEditOtherUsersProfiles();
|
||||
}
|
||||
|
||||
public canEditMediaFileByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken);
|
||||
public canEditMediaFileByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canEditWeightByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken);
|
||||
public canEditWeightByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canEditTtsModelByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken);
|
||||
public canEditTtsModelByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canEditOtherUsersTtsModels() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteTtsModelByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canDeleteOtherUsersTtsModels() || this.userTokenMatches(userToken);
|
||||
public canDeleteTtsModelByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canDeleteOtherUsersTtsModels() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteTtsResultByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canDeleteOtherUsersTtsResults() || this.userTokenMatches(userToken);
|
||||
public canDeleteTtsResultByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canDeleteOtherUsersTtsResults() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteW2lTemplateByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canDeleteOtherUsersW2lTemplates() || this.userTokenMatches(userToken);
|
||||
public canDeleteW2lTemplateByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canDeleteOtherUsersW2lTemplates() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteW2lResultByUserToken(userToken: MaybeString) : boolean {
|
||||
return this.canDeleteOtherUsersW2lResults() || this.userTokenMatches(userToken);
|
||||
public canDeleteW2lResultByUserToken(userToken: MaybeString): boolean {
|
||||
return (
|
||||
this.canDeleteOtherUsersW2lResults() || this.userTokenMatches(userToken)
|
||||
);
|
||||
}
|
||||
|
||||
public canUseTts() : boolean {
|
||||
public canUseTts(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_use_tts || true; // NB: Default true.
|
||||
}
|
||||
|
||||
public canUseW2l() : boolean {
|
||||
public canUseW2l(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_use_w2l || true; // NB: Default true
|
||||
}
|
||||
|
||||
public canUploadTtsModels() : boolean {
|
||||
public canUploadTtsModels(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_upload_tts_models || false;
|
||||
}
|
||||
|
||||
public canUploadW2lTemplates() : boolean {
|
||||
public canUploadW2lTemplates(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_upload_w2l_templates || false;
|
||||
}
|
||||
|
||||
public canBanUsers() : boolean {
|
||||
public canBanUsers(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_ban_users || false;
|
||||
}
|
||||
|
||||
public canEditCategories() : boolean {
|
||||
public canEditCategories(): boolean {
|
||||
// TODO / NB: There aren't fine-grained controls over categories.
|
||||
return this.canBanUsers();
|
||||
}
|
||||
|
||||
public canApproveW2lTemplates() : boolean {
|
||||
public canApproveW2lTemplates(): boolean {
|
||||
return this.sessionStateResponse?.user?.can_approve_w2l_templates || false;
|
||||
}
|
||||
|
||||
public canEditOtherUsersProfiles() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_edit_other_users_profiles || false;
|
||||
public canEditOtherUsersProfiles(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_edit_other_users_profiles || false
|
||||
);
|
||||
}
|
||||
|
||||
public canEditOtherUsersTtsModels() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_edit_other_users_tts_models || false;
|
||||
public canEditOtherUsersTtsModels(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_edit_other_users_tts_models || false
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteOtherUsersTtsModels() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_delete_other_users_tts_models || false;
|
||||
public canDeleteOtherUsersTtsModels(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_delete_other_users_tts_models ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteOtherUsersTtsResults() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_delete_other_users_tts_results || false;
|
||||
public canDeleteOtherUsersTtsResults(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_delete_other_users_tts_results ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteOtherUsersW2lTemplates() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_delete_other_users_w2l_templates || false;
|
||||
public canDeleteOtherUsersW2lTemplates(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_delete_other_users_w2l_templates ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public canDeleteOtherUsersW2lResults() : boolean {
|
||||
return this.sessionStateResponse?.user?.can_delete_other_users_w2l_results || false;
|
||||
public canDeleteOtherUsersW2lResults(): boolean {
|
||||
return (
|
||||
this.sessionStateResponse?.user?.can_delete_other_users_w2l_results ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public canEditTtsResultAsUserOrMod(creatorUserToken: MaybeString) : boolean {
|
||||
|
||||
public canEditTtsResultAsUserOrMod(creatorUserToken: MaybeString): boolean {
|
||||
if (this.userTokenMatches(creatorUserToken)) {
|
||||
return true;
|
||||
}
|
||||
return this.canDeleteOtherUsersTtsResults(); // TODO: There is no granular permission for this yet.
|
||||
}
|
||||
|
||||
public deleteTtsResultAsMod(creatorUserToken: MaybeString) : boolean {
|
||||
public deleteTtsResultAsMod(creatorUserToken: MaybeString): boolean {
|
||||
if (this.userTokenMatches(creatorUserToken)) {
|
||||
return true;
|
||||
}
|
||||
return this.canDeleteOtherUsersTtsResults();
|
||||
}
|
||||
|
||||
public deleteTtsModelAsMod(creatorUserToken: MaybeString) : boolean {
|
||||
public deleteTtsModelAsMod(creatorUserToken: MaybeString): boolean {
|
||||
if (this.userTokenMatches(creatorUserToken)) {
|
||||
return true;
|
||||
}
|
||||
return this.canDeleteOtherUsersTtsModels();
|
||||
}
|
||||
|
||||
public canEditW2lResultAsUserOrMod(creatorUserToken: MaybeString) : boolean {
|
||||
public canEditW2lResultAsUserOrMod(creatorUserToken: MaybeString): boolean {
|
||||
if (this.userTokenMatches(creatorUserToken)) {
|
||||
return true;
|
||||
}
|
||||
return this.canDeleteOtherUsersW2lResults(); // TODO: There is no granular permission for this yet.
|
||||
}
|
||||
|
||||
public canDeleteW2lResultAsUserOrMod(creatorUserToken: MaybeString) : boolean {
|
||||
public canDeleteW2lResultAsUserOrMod(creatorUserToken: MaybeString): boolean {
|
||||
if (this.userTokenMatches(creatorUserToken)) {
|
||||
return true;
|
||||
}
|
||||
return this.canDeleteOtherUsersW2lResults();
|
||||
}
|
||||
|
||||
public userTokenMatches(otherUserToken: MaybeString) : boolean {
|
||||
// Default to false if user token on either side is falsey.
|
||||
public userTokenMatches(otherUserToken: MaybeString): boolean {
|
||||
// Default to false if user token on either side is falsey.
|
||||
if (!otherUserToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -29,8 +29,7 @@ export default function PageContainer({
|
||||
}
|
||||
|
||||
// Merge the classNames
|
||||
const mergedClassNames =
|
||||
`${containerClass} ${className} min-height-100vh`.trim();
|
||||
const mergedClassNames = `${containerClass} ${className}`.trim();
|
||||
|
||||
return <div className={mergedClassNames}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
background-color: $panel-inner;
|
||||
border: 1px solid lighten($panel-inner, 8%);
|
||||
border-radius: 0.5rem;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fy-dropdown-item {
|
||||
padding: 10px 15px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: $white !important;
|
||||
transition: 0.15s all;
|
||||
cursor: pointer;
|
||||
font-size: 15px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.fy-dropdown-item:hover {
|
||||
background-color: lighten($panel-inner, 8%);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import "./DropdownMenu.scss";
|
||||
|
||||
interface DropdownItem {
|
||||
id: number;
|
||||
name: string;
|
||||
link?: string;
|
||||
onClick?: () => void;
|
||||
icon?: IconDefinition;
|
||||
}
|
||||
|
||||
interface DropdownMenuProps {
|
||||
items: DropdownItem[];
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function DropdownMenu({
|
||||
items,
|
||||
className,
|
||||
onClose,
|
||||
}: DropdownMenuProps) {
|
||||
const fadeIn = useSpring({
|
||||
from: { opacity: 0 },
|
||||
to: { opacity: 1 },
|
||||
config: { duration: 100 },
|
||||
});
|
||||
|
||||
return (
|
||||
<a.div style={fadeIn} className={`fy-dropdown-menu ${className}`.trim()}>
|
||||
{items.map(item => (
|
||||
<>
|
||||
{item.onClick ? (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={e => {
|
||||
if (item.onClick) {
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
className="fy-dropdown-item"
|
||||
>
|
||||
<span>
|
||||
{item.icon && (
|
||||
<FontAwesomeIcon icon={item.icon} className="me-2 fs-7" />
|
||||
)}
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{item.link && (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={item.link}
|
||||
className="fy-dropdown-item"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<span>
|
||||
{item.icon && (
|
||||
<FontAwesomeIcon icon={item.icon} className="me-2 fs-7" />
|
||||
)}
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</a.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DropdownMenu";
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-nav-item {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: rgba($white, 0.8);
|
||||
transition: 0.15s all;
|
||||
|
||||
a {
|
||||
color: rgba($white, 0.8);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: 0.15s all;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-nav-item:hover .fy-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState } from "react";
|
||||
import DropdownMenu from "../DropdownMenu/DropdownMenu";
|
||||
import "./NavItem.scss";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCaretDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface NavItemProps {
|
||||
icon?: IconDefinition;
|
||||
label: string;
|
||||
isHoverable?: boolean;
|
||||
link?: string;
|
||||
className?: string;
|
||||
dropdownItems?: {
|
||||
id: number;
|
||||
name: string;
|
||||
link: string;
|
||||
icon?: IconDefinition;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function NavItem({
|
||||
label,
|
||||
dropdownItems,
|
||||
icon,
|
||||
isHoverable,
|
||||
link,
|
||||
className,
|
||||
}: NavItemProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const textHover = `${isHovered && "text-white"}`.trim();
|
||||
|
||||
const renderContent = () => {
|
||||
if (isHoverable && dropdownItems) {
|
||||
return (
|
||||
<>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2 fs-7" />}
|
||||
<span className={textHover}>
|
||||
{label} <FontAwesomeIcon icon={faCaretDown} className="ms-1 fs-7" />
|
||||
</span>
|
||||
|
||||
{isHovered && (
|
||||
<DropdownMenu items={dropdownItems} onClose={() => {}} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{link && (
|
||||
<Link to={link} className={textHover}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2 fs-7" />}
|
||||
{label}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fy-nav-item ${className}`.trim()}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./NavItem";
|
||||
@@ -20,8 +20,10 @@ export default function Panel({
|
||||
return (
|
||||
<div
|
||||
className={`${clear ? "panel-clear" : "panel"}${
|
||||
padding ? " p-3 py-4 p-md-4" : ""
|
||||
}${mb ? " mb-4" : ""}${mt ? " mt-4" : ""}${className ? " " + className : ""}`}
|
||||
padding ? " p-3 p-md-4" : ""
|
||||
}${mb ? " mb-4" : ""}${mt ? " mt-4" : ""}${
|
||||
className ? " " + className : ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.fy-profile-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
transition: 0.15s all;
|
||||
}
|
||||
|
||||
.fy-profile-pic {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fy-profile-dropdown-menu {
|
||||
display: block;
|
||||
right: 0;
|
||||
left: auto;
|
||||
margin-top: 6px;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import DropdownMenu from "../DropdownMenu";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import "./ProfileDropdown.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCaretDown,
|
||||
faSignOutAlt,
|
||||
faUser,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ProfileDropdownProps {
|
||||
username: string;
|
||||
displayName: string;
|
||||
avatarIndex: number;
|
||||
backgroundColorIndex: number;
|
||||
emailHash: string;
|
||||
logoutHandler: () => void;
|
||||
}
|
||||
|
||||
export default function ProfileDropdown({
|
||||
username,
|
||||
displayName,
|
||||
avatarIndex,
|
||||
backgroundColorIndex,
|
||||
emailHash,
|
||||
logoutHandler,
|
||||
}: ProfileDropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const profileOptions = [
|
||||
{
|
||||
id: 1,
|
||||
name: "My Profile",
|
||||
link: `/profile/${displayName}`,
|
||||
icon: faUser,
|
||||
},
|
||||
{ id: 2, name: "Logout", onClick: logoutHandler, icon: faSignOutAlt },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fy-profile-dropdown" ref={dropdownRef}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="fy-profile-pic d-flex gap-2 align-items-center"
|
||||
>
|
||||
<Gravatar
|
||||
size={38}
|
||||
username={username}
|
||||
email_hash={emailHash}
|
||||
avatarIndex={avatarIndex}
|
||||
backgroundIndex={backgroundColorIndex}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCaretDown} />
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<DropdownMenu
|
||||
items={profileOptions}
|
||||
className="fy-profile-dropdown-menu"
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ProfileDropdown";
|
||||
@@ -85,6 +85,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.search-bar-container {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-bar-container {
|
||||
width: 100%;
|
||||
|
||||
@@ -56,6 +56,13 @@ export default function SideNav({
|
||||
const isDevelopmentEnv = fakeYouFrontendEnv.isDevelopment();
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const isMenuOpen = wrapper?.classList.contains("toggled");
|
||||
const isLoggedIn = sessionWrapper.isLoggedIn();
|
||||
const isOnLandingPage = window.location.pathname === "/";
|
||||
const isOnLoginOrSignUpPage =
|
||||
window.location.pathname === "/login" ||
|
||||
window.location.pathname === "/login/" ||
|
||||
window.location.pathname === "/signup" ||
|
||||
window.location.pathname === "/signup/";
|
||||
|
||||
let history = useHistory();
|
||||
const handleNavLinkClick = () => {
|
||||
@@ -105,17 +112,25 @@ export default function SideNav({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const shouldShowSidebar = windowWidth >= 992;
|
||||
const shouldNotShowSidebar =
|
||||
!isLoggedIn && (isOnLandingPage || isOnLoginOrSignUpPage);
|
||||
const shouldShowSidebar = windowWidth >= 992 && !shouldNotShowSidebar;
|
||||
const sidebarClassName = `sidebar ${
|
||||
shouldShowSidebar ? "visible" : ""
|
||||
}`.trim();
|
||||
|
||||
useEffect(() => {
|
||||
const contentWrapper = document.getElementById("page-content-wrapper");
|
||||
|
||||
if (windowWidth >= 992) {
|
||||
if (
|
||||
(shouldShowSidebar && isLoggedIn) ||
|
||||
(shouldShowSidebar && !isOnLandingPage)
|
||||
) {
|
||||
contentWrapper?.classList.remove("no-padding");
|
||||
} else {
|
||||
contentWrapper?.classList.add("no-padding");
|
||||
}
|
||||
}, [windowWidth]);
|
||||
}, [isLoggedIn, isOnLandingPage, shouldShowSidebar]);
|
||||
|
||||
const [queueStats, setQueueStats] = useState<GetQueueStatsSuccessResponse>({
|
||||
success: true,
|
||||
@@ -349,10 +364,7 @@ export default function SideNav({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id="sidebar-wrapper"
|
||||
className={`sidebar ${shouldShowSidebar ? "visible" : ""}`}
|
||||
>
|
||||
<div id="sidebar-wrapper" className={sidebarClassName}>
|
||||
<div>
|
||||
<ul className="sidebar-nav">
|
||||
<li>
|
||||
@@ -558,7 +570,7 @@ export default function SideNav({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 d-flex d-lg-none gap-2 mb-5">
|
||||
<div className="px-4 d-flex d-lg-none gap-2 mb-2">
|
||||
{userOrLoginButton}
|
||||
{signupOrLogOutButton}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
faBars,
|
||||
faFaceViewfinder,
|
||||
faMessageDots,
|
||||
faSearch,
|
||||
faSignOutAlt,
|
||||
faUser,
|
||||
faStar,
|
||||
faWandMagicSparkles,
|
||||
faWaveformLines,
|
||||
faXmark,
|
||||
faClipboardList
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -10,11 +13,13 @@ import { Button } from "components/common";
|
||||
import SearchBar from "components/common/SearchBar";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { WebUrl } from "common/WebUrl";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Logout } from "@storyteller/components/src/api/session/Logout";
|
||||
import { useModal } from "hooks";
|
||||
import { InferenceJobsModal } from "components/modals";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
import NavItem from "../../common/NavItem/NavItem";
|
||||
import ProfileDropdown from "components/common/ProfileDropdown";
|
||||
|
||||
interface TopNavProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -28,12 +33,20 @@ export default function TopNav({
|
||||
querySessionCallback,
|
||||
querySessionSubscriptionsCallback,
|
||||
}: TopNavProps) {
|
||||
const domain = useDomainConfig();
|
||||
let history = useHistory();
|
||||
const [isMobileSearchBarVisible, setIsMobileSearchBarVisible] =
|
||||
useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const [menuButtonIcon, setMenuButtonIcon] = useState(faBars);
|
||||
// const { t } = useLocalize("TopNav");
|
||||
const isOnLandingPage = window.location.pathname === "/";
|
||||
const isOnLoginOrSignUpPage =
|
||||
window.location.pathname === "/login" ||
|
||||
window.location.pathname === "/login/" ||
|
||||
window.location.pathname === "/signup" ||
|
||||
window.location.pathname === "/signup/";
|
||||
|
||||
const { open } = useModal();
|
||||
const openModal = () => open({ component: InferenceJobsModal });
|
||||
@@ -92,113 +105,85 @@ export default function TopNav({
|
||||
await Logout();
|
||||
querySessionCallback();
|
||||
querySessionSubscriptionsCallback();
|
||||
// PosthogClient.reset();
|
||||
// Analytics.accountLogout();
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
const loggedIn = sessionWrapper.isLoggedIn();
|
||||
|
||||
let userOrLoginButton = (
|
||||
<>
|
||||
<Button
|
||||
label="Login"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
let signupOrLogOutButton = (
|
||||
<>
|
||||
<Button
|
||||
label="Sign Up"
|
||||
small
|
||||
onClick={() => {
|
||||
history.push("/signup");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (loggedIn) {
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
// let gravatarHash = props.sessionWrapper.getEmailGravatarHash();
|
||||
// let gravatar = <span />;
|
||||
|
||||
if (displayName === undefined) {
|
||||
displayName = "My Account";
|
||||
}
|
||||
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
userOrLoginButton = (
|
||||
<>
|
||||
<Button
|
||||
icon={faUser}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push(url);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
signupOrLogOutButton = (
|
||||
<>
|
||||
<Button
|
||||
icon={faSignOutAlt}
|
||||
label="Logout"
|
||||
small
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
let profileDropdown = <></>;
|
||||
|
||||
if (sessionWrapper.isLoggedIn()) {
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
if (displayName === undefined) {
|
||||
displayName = "My Account";
|
||||
}
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
userOrLoginButton = (
|
||||
<Button
|
||||
icon={faUser}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => history.push(url)}
|
||||
className="d-none d-lg-block"
|
||||
let username = sessionWrapper.getUsername();
|
||||
let emailHash = sessionWrapper.getEmailGravatarHash();
|
||||
let avatarIndex = 0; //temporary
|
||||
let backgroundColorIndex = 0; //temporary
|
||||
|
||||
profileDropdown = (
|
||||
<ProfileDropdown
|
||||
username={username || ""}
|
||||
displayName={displayName || ""}
|
||||
avatarIndex={avatarIndex}
|
||||
backgroundColorIndex={backgroundColorIndex}
|
||||
emailHash={emailHash || ""}
|
||||
logoutHandler={logoutHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const aiToolsDropdown = [
|
||||
{ id: 1, name: "Text to Speech", link: "/tts", icon: faMessageDots },
|
||||
{
|
||||
id: 2,
|
||||
name: "Voice to Voice",
|
||||
link: "/voice-conversion",
|
||||
icon: faWaveformLines,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Face Animator",
|
||||
link: "/face-animator",
|
||||
icon: faFaceViewfinder,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Voice Designer",
|
||||
link: "/voice-designer",
|
||||
icon: faWandMagicSparkles,
|
||||
},
|
||||
// { id: 4, name: "Text to Image", link: "/text-to-image" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div id="topbar-wrapper" className="position-fixed">
|
||||
<div className="topbar-nav">
|
||||
<div className="topbar-nav-left">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
height="34"
|
||||
className="mb-2 d-none d-lg-block"
|
||||
/>
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo-Mobile.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
height="36"
|
||||
className="mb-0 d-block d-lg-none"
|
||||
/>
|
||||
</Link>
|
||||
<div className="d-flex gap-3 align-items-center">
|
||||
<Link to="/">
|
||||
<img
|
||||
src={domain.logo}
|
||||
alt={`${domain.title}: Cartoon and Celebrity Text to Speech`}
|
||||
height="34"
|
||||
className="mb-1 d-none d-lg-block"
|
||||
/>
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo-Mobile.png"
|
||||
alt={`${domain.title}: Cartoon and Celebrity Text to Speech`}
|
||||
height="36"
|
||||
className="mb-0 d-block d-lg-none"
|
||||
/>
|
||||
</Link>
|
||||
{((!loggedIn && isOnLandingPage) ||
|
||||
(!loggedIn && isOnLoginOrSignUpPage)) && (
|
||||
<div className="d-none d-lg-block">
|
||||
<NavItem
|
||||
isHoverable={true}
|
||||
label="AI Tools"
|
||||
dropdownItems={aiToolsDropdown}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="topbar-nav-center">
|
||||
@@ -213,11 +198,40 @@ export default function TopNav({
|
||||
</div>
|
||||
|
||||
<div className="topbar-nav-right">
|
||||
{((!loggedIn && isOnLandingPage) ||
|
||||
(!loggedIn && isOnLoginOrSignUpPage)) && (
|
||||
<NavItem
|
||||
icon={faStar}
|
||||
label="Pricing"
|
||||
link="/pricing"
|
||||
className="me-3 d-none d-lg-block"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<div className="d-none d-lg-flex gap-2">
|
||||
<Button {...{ icon: faClipboardList, label: "My Jobs", onClick: openModal, variant: "secondary" }}/>
|
||||
{userOrLoginButton}
|
||||
{signupOrLogOutButton}
|
||||
{loggedIn ? (
|
||||
profileDropdown
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
label="Login"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Sign Up"
|
||||
small
|
||||
onClick={() => {
|
||||
history.push("/signup");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
icon={faSearch}
|
||||
@@ -239,6 +253,8 @@ export default function TopNav({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="topbar-nav bg-panel">test</div> */}
|
||||
|
||||
{/* Mobile Searchbar */}
|
||||
{isMobileSearchBarVisible && (
|
||||
<div className="topbar-mobile-search-bar-container">
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { createContext, useContext, ReactNode } from "react";
|
||||
import { getCurrentDomainConfig } from "../utils/domainConfig";
|
||||
|
||||
const DomainConfigContext = createContext(getCurrentDomainConfig());
|
||||
|
||||
export const useDomainConfig = () => useContext(DomainConfigContext);
|
||||
|
||||
interface DomainConfigProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function DomainConfigProvider({
|
||||
children,
|
||||
}: DomainConfigProviderProps) {
|
||||
const domainConfig = getCurrentDomainConfig();
|
||||
|
||||
return (
|
||||
<DomainConfigContext.Provider value={domainConfig}>
|
||||
{children}
|
||||
</DomainConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -2,5 +2,12 @@ import AccountModalContext from "./AccountModalContext";
|
||||
import InferenceJobsContext from "./InferenceJobsContext";
|
||||
import ModalContext from "./ModalContext";
|
||||
import SessionContext from "./SessionContext";
|
||||
import DomainConfigContext from "./DomainConfigContext";
|
||||
|
||||
export { AccountModalContext, InferenceJobsContext, ModalContext, SessionContext };
|
||||
export {
|
||||
AccountModalContext,
|
||||
InferenceJobsContext,
|
||||
ModalContext,
|
||||
SessionContext,
|
||||
DomainConfigContext,
|
||||
};
|
||||
@@ -12,7 +12,7 @@ $warning: darken(#f7941c, 5%);
|
||||
$accent: #80cfd3;
|
||||
$workflow: $primary;
|
||||
$panel: #242433;
|
||||
$panel-inner: lighten($panel, 4%);
|
||||
$panel-inner: #2b2b3b;
|
||||
$panel-inner-border: lighten($panel, 8%);
|
||||
|
||||
$primary-lm: #000;
|
||||
|
||||
@@ -60,6 +60,7 @@ $topbar-height: 66px;
|
||||
position: relative;
|
||||
padding-left: 260px;
|
||||
padding-top: $topbar-height;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
|
||||
@@ -11,21 +11,21 @@ styles with important tags:
|
||||
//
|
||||
// Once the rollout is complete, this outer wrapper class can be removed.
|
||||
|
||||
@mixin panel {
|
||||
background-color: $panel;
|
||||
border-color: #ffffff12 !important;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0 0 0 / 16%),
|
||||
0 70px 65px rgb(0 0 0 / 8%),
|
||||
0 30px 30px rgb(0 0 0 / 7%),
|
||||
0 15px 15px rgb(0 0 0 / 5%),
|
||||
0 10px 8px rgb(0 0 0 / 3%),
|
||||
0 4px 4px rgb(0 0 0 / 2%),
|
||||
0 2px 2px rgb(0 0 0 / 1%) !important;
|
||||
@mixin panel {
|
||||
background-color: $panel;
|
||||
border-color: #ffffff12 !important;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-radius: 0px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0 0 0 / 16%),
|
||||
0 70px 65px rgb(0 0 0 / 8%),
|
||||
0 30px 30px rgb(0 0 0 / 7%),
|
||||
0 15px 15px rgb(0 0 0 / 5%),
|
||||
0 10px 8px rgb(0 0 0 / 3%),
|
||||
0 4px 4px rgb(0 0 0 / 2%),
|
||||
0 2px 2px rgb(0 0 0 / 1%) !important;
|
||||
}
|
||||
|
||||
.fakeyou-refresh {
|
||||
@@ -45,11 +45,11 @@ styles with important tags:
|
||||
margin: 0;
|
||||
}
|
||||
/* Firefox */
|
||||
.numberInputNoArrows input[type=number],
|
||||
input.numberInputNoArrows[type=number] {
|
||||
.numberInputNoArrows input[type="number"],
|
||||
input.numberInputNoArrows[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
|
||||
//-end--//
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@@ -360,7 +360,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.panel-inner {
|
||||
background-color: lighten($panel, 4%);
|
||||
background-color: $panel-inner;
|
||||
}
|
||||
|
||||
.panel-tts-results {
|
||||
@@ -1373,15 +1373,15 @@ styles with important tags:
|
||||
|
||||
.hanashi-before {
|
||||
position: absolute;
|
||||
top: -35%;
|
||||
left: -140%;
|
||||
top: -80%;
|
||||
left: -150%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hanashi-after {
|
||||
position: absolute;
|
||||
top: -35%;
|
||||
right: -140%;
|
||||
top: -80%;
|
||||
right: -150%;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -1696,8 +1696,8 @@ textarea {
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-top: 7rem;
|
||||
padding-bottom: 7rem;
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
type DomainConfig = Record<string, any>;
|
||||
|
||||
const domainConfigs: Record<string, DomainConfig> = {
|
||||
"storyteller.ai": {
|
||||
logo: "/fakeyou/Storyteller-Logo-5.png",
|
||||
title: "Storyteller AI",
|
||||
link: "https://storyteller.ai",
|
||||
},
|
||||
"fakeyou.com": {
|
||||
logo: "/fakeyou/FakeYou-Logo-2.png",
|
||||
title: "FakeYou",
|
||||
link: "https://fakeyou.com",
|
||||
},
|
||||
// Default to FakeYou.com
|
||||
default: {
|
||||
logo: "/fakeyou/FakeYou-Logo-2.png",
|
||||
title: "FakeYou",
|
||||
link: "https://fakeyou.com",
|
||||
},
|
||||
};
|
||||
|
||||
export const getCurrentDomainConfig = (): DomainConfig => {
|
||||
const currentHostname = window.location.hostname;
|
||||
|
||||
const matchingConfigKey = Object.keys(domainConfigs).find(
|
||||
key => currentHostname === key || currentHostname.endsWith(`.${key}`)
|
||||
);
|
||||
|
||||
const domainConfig =
|
||||
matchingConfigKey !== undefined
|
||||
? domainConfigs[matchingConfigKey]
|
||||
: domainConfigs["default"];
|
||||
|
||||
return domainConfig;
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -19,12 +19,14 @@ import {
|
||||
} from "@storyteller/components/src/api/server/GetServerInfo";
|
||||
import { useLocalize } from "hooks";
|
||||
import { Container } from "components/common";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function FooterNav(props: Props) {
|
||||
const domain = useDomainConfig();
|
||||
const { t } = useLocalize("Footer");
|
||||
const [serverInfo, setServerInfo] = useState<
|
||||
GetServerInfoSuccessResponse | undefined
|
||||
@@ -84,8 +86,8 @@ function FooterNav(props: Props) {
|
||||
<div className="col-12 col-lg-3 d-flex flex-column gap-4 align-items-center align-items-lg-start">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
src={domain.logo}
|
||||
alt={`${domain.title}: Cartoon and Celebrity Text to Speech`}
|
||||
height="34"
|
||||
/>
|
||||
</Link>
|
||||
@@ -204,12 +206,8 @@ function FooterNav(props: Props) {
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column flex-lg-row pt-2 align-items-center gap-2 gap-xl-4 flex-wrap">
|
||||
<span className="flex-grow-1">
|
||||
© 2023 FakeYou by{" "}
|
||||
<a href="https://storyteller.ai" target="_blank" rel="noreferrer">
|
||||
Storyteller.ai
|
||||
</a>
|
||||
</span>
|
||||
<span className="flex-grow-1 opacity-75">© {domain.title} 2024</span>
|
||||
|
||||
<div className="d-flex flex-column flex-lg-row align-items-center mt-4 mt-lg-0">
|
||||
{moderationLink}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
faArrowRight,
|
||||
faFileArrowUp,
|
||||
faFlask,
|
||||
faSparkles,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Panel } from "components/common";
|
||||
import { useLocalize } from "hooks";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface DashboardProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
export default function Dashboard(props: DashboardProps) {
|
||||
const { t } = useLocalize("LandingPage");
|
||||
const isLoggedIn = props.sessionWrapper.isLoggedIn();
|
||||
|
||||
let uploadModelSection = <></>;
|
||||
|
||||
if (isLoggedIn) {
|
||||
uploadModelSection = (
|
||||
<>
|
||||
<h2 className="fw-bold mb-4 mt-5 pt-4">Upload Models</h2>
|
||||
<div className="panel p-4 rounded">
|
||||
<div className="row gy-3 zi-2">
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/tts" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload TTS Model
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/voice_conversion" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload V2V Model
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/w2l_photo" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload W2L Model
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel clear={true} className="mt-5">
|
||||
<h1 className="fw-bold mb-4">{t("productsTitle")}</h1>
|
||||
<div className="row g-4 position-relative">
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/tts"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productTtsTitle")}
|
||||
</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productTtsText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/tts" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-tts.webp"
|
||||
alt="Text to speech"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/voice-conversion"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productVcTitle")}
|
||||
</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productVcText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/voice-conversion" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-vc.webp"
|
||||
alt="Voice Conversion"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/voice-designer"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<div className="mb-1">
|
||||
<span className="badge-beta d-inline-flex align-items-center mb-2 me-2">
|
||||
<FontAwesomeIcon icon={faFlask} className="me-1" />
|
||||
BETA
|
||||
</span>
|
||||
|
||||
<h4 className="fw-bold text-white d-inline-flex align-items-center mb-0">
|
||||
<span>Voice Designer</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
Create your own AI voice
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/voice-designer" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-vd.webp"
|
||||
alt="Voice Designer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/face-animator"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<div className="mb-1">
|
||||
<span className="badge-new d-inline-flex align-items-center mb-2 me-2">
|
||||
<FontAwesomeIcon icon={faSparkles} className="me-1" />
|
||||
{t("productNewTag")}
|
||||
</span>
|
||||
|
||||
<h4 className="fw-bold text-white d-inline-flex align-items-center mb-0">
|
||||
<span>{t("productFaceAnimatorTitle")}</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productFaceAnimatorText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/face-animator" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-w2l.webp"
|
||||
alt="Face Animator"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-top"
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{uploadModelSection}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,15 @@
|
||||
import React, { useMemo } from "react";
|
||||
import React from "react";
|
||||
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { useLocalize } from "hooks";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faStar,
|
||||
faArrowRight,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import { WebUrl } from "../../../../common/WebUrl";
|
||||
import {
|
||||
faFileArrowUp,
|
||||
faFlask,
|
||||
faSparkles,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import {} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import Alert from "components/common/Alert/Alert";
|
||||
// import { Analytics } from "../../../../../common/Analytics";
|
||||
import { Container } from "components/common";
|
||||
import FakeYouLandingHeader from "./fakeyou/FakeYouLandingHeader";
|
||||
import Dashboard from "./Dashboard";
|
||||
import FakeYouLandingBody from "./fakeyou/FakeYouLandingBody";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
import StorytellerLanding from "./storyteller/StorytellerLanding";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -28,537 +18,40 @@ interface Props {
|
||||
|
||||
function LandingPage(props: Props) {
|
||||
usePrefixedDocumentTitle("FakeYou Celebrity Voice Generator");
|
||||
|
||||
PosthogClient.recordPageview();
|
||||
const domain = useDomainConfig();
|
||||
|
||||
const { t } = useLocalize("LandingPage");
|
||||
|
||||
let signUpButton = <></>;
|
||||
let viewPricingButton = <></>;
|
||||
let upgradeButton = <></>;
|
||||
let myProfileButton = <></>;
|
||||
let uploadModelSection = <></>;
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
signUpButton = (
|
||||
<>
|
||||
<Link
|
||||
to="/signup"
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroSignup();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-primary w-100">
|
||||
{t("heroButtonSignUp")}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
viewPricingButton = (
|
||||
<>
|
||||
<Link
|
||||
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroViewPricing();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-secondary w-100">
|
||||
<FontAwesomeIcon icon={faStar} className="me-2" />
|
||||
{t("heroButtonPricing")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (props.sessionWrapper.isLoggedIn()) {
|
||||
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
myProfileButton = (
|
||||
<>
|
||||
<Link
|
||||
to={url}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroViewProfile();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-secondary w-100">
|
||||
<FontAwesomeIcon icon={faUser} className="me-2" />
|
||||
{t("heroButtonProfile")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
uploadModelSection = (
|
||||
<>
|
||||
<h2 className="fw-bold mb-4 mt-5 pt-4">Upload Models</h2>
|
||||
<div className="panel p-4 rounded">
|
||||
<div className="row gy-3 zi-2">
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/tts" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload TTS Model
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/voice_conversion" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload V2V Model
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<Link to="/upload/w2l_photo" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload W2L Model
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
|
||||
upgradeButton = (
|
||||
<>
|
||||
<Link
|
||||
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroUpgradePlan();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faStar} className="me-2" />
|
||||
{t("heroButtonUpgradePlan")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const randomHeroImage = useMemo(() => {
|
||||
const images = [
|
||||
// Main Images:
|
||||
"mascot/kitsune_pose2.webp",
|
||||
// "mascot/may4th.webp",
|
||||
// "mascot/halloween_1.webp",
|
||||
// "mascot/halloween_2.webp",
|
||||
// "mascot/halloween_3.webp",
|
||||
// "mascot/xmas_1.webp",
|
||||
// "mascot/xmas_2.webp",
|
||||
// "mascot/xmas_3.webp",
|
||||
// "mascot/xmas_4.webp",
|
||||
];
|
||||
|
||||
return images[Math.floor(Math.random() * images.length)];
|
||||
}, []);
|
||||
const isLoggedIn = props.sessionWrapper.isLoggedIn();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container pb-5 pb-lg-0 pt-lg-4 px-md-5 px-lg-5 px-xl-3">
|
||||
{/* Face Animation Alert Banner */}
|
||||
<Alert
|
||||
id="face-animation-alert"
|
||||
icon={faSparkles}
|
||||
message="Have you tried our new Face Animator? Turn photos of faces into animated lip-synced videos with just a picture and some audio!"
|
||||
alertVariant="new"
|
||||
link="/face-animator"
|
||||
linkText="Try it now"
|
||||
/>
|
||||
|
||||
<div className="row flex-md-row-reverse">
|
||||
<div className="col-12 col-lg-5 p-md-0 d-flex justify-content-center">
|
||||
<img
|
||||
src={randomHeroImage}
|
||||
alt="FakeYou Mascot"
|
||||
height={440}
|
||||
width={447}
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-lg-7 d-flex flex-column justify-content-center flex-reverse px-md-5 px-lg-3">
|
||||
<h1 className="fw-bold display-5 text-center text-lg-start px-md-5 px-lg-0">
|
||||
{t("heroTitle")}
|
||||
</h1>
|
||||
<p className="lead opacity-75 pb-4 text-center text-lg-start px-md-5 px-lg-0 pe-lg-5">
|
||||
{t("heroText")}
|
||||
</p>
|
||||
<div className="d-flex flex-column flex-md-row gap-3 mt-3 mb-4 w-100 justify-content-center justify-content-lg-start">
|
||||
{upgradeButton}
|
||||
{signUpButton}
|
||||
{viewPricingButton}
|
||||
{myProfileButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="container-panel pb-5 mb-4">
|
||||
<div className="panel p-3 py-4 p-md-4">
|
||||
<form
|
||||
className="w-100 d-flex flex-column"
|
||||
// onSubmit={handleFormSubmit}
|
||||
>
|
||||
<div className="row gx-5 gy-4">
|
||||
<div className="col-12 col-md-6 d-flex flex-column gap-4">
|
||||
<div>
|
||||
<label className="sub-title">
|
||||
Pick a Voice (xxx to choose from)
|
||||
</label>
|
||||
<div className="input-icon-search pb-4">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faMicrophone} />
|
||||
</span>
|
||||
|
||||
<VcModelListSearch
|
||||
voiceConversionModels={props.voiceConversionModels}
|
||||
setVoiceConversionModels={props.setVoiceConversionModels}
|
||||
maybeSelectedVoiceConversionModel={
|
||||
props.maybeSelectedVoiceConversionModel
|
||||
}
|
||||
setMaybeSelectedVoiceConversionModel={interceptModelChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
<span className="opacity-75">
|
||||
Pick one to get started, or choose from
|
||||
</span>{" "}
|
||||
<Link to="/tts">thousands more!</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 d-flex flex-column">
|
||||
<label className="sub-title">Select Your Input</label>
|
||||
<div className="d-flex flex-column flex-lg-row gap-3">
|
||||
<button className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faRightLeft} className="me-2" />
|
||||
Text
|
||||
</button>
|
||||
<button className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faFileArrowUp} className="me-2" />
|
||||
Upload
|
||||
</button>
|
||||
<button className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faMicrophone} className="me-2" />
|
||||
Microphone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="container pb-4 px-md-5 px-xl-3">
|
||||
<h2 className="fw-bold mb-4">{t("productsTitle")}</h2>
|
||||
<div className="row g-4 position-relative">
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/tts"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productTtsTitle")}
|
||||
</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productTtsText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/tts" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-tts.webp"
|
||||
alt="Text to speech"
|
||||
<>
|
||||
<Container type="panel">
|
||||
{domain.title === "FakeYou" ? (
|
||||
<>
|
||||
{/* FAKEYOU.COM */}
|
||||
{!isLoggedIn && (
|
||||
<FakeYouLandingHeader
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/voice-conversion"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<h3 className="fw-bold text-white mb-1">
|
||||
{t("productVcTitle")}
|
||||
</h3>
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productVcText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/voice-conversion" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-vc.webp"
|
||||
alt="Voice Conversion"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/voice-designer"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<div className="mb-1">
|
||||
<span className="badge-beta d-inline-flex align-items-center mb-2 me-2">
|
||||
<FontAwesomeIcon icon={faFlask} className="me-1" />
|
||||
BETA
|
||||
</span>
|
||||
<Dashboard sessionWrapper={props.sessionWrapper} />
|
||||
|
||||
<h4 className="fw-bold text-white d-inline-flex align-items-center mb-0">
|
||||
<span>Voice Designer</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
Create your own AI voice
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/voice-designer" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-vd.webp"
|
||||
alt="Voice Designer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<Link
|
||||
to="/face-animator"
|
||||
className="panel panel-select d-flex flex-column align-items-center"
|
||||
>
|
||||
<div className="d-flex px-4 pt-4 align-items-start w-100">
|
||||
<div className="flex-grow-1">
|
||||
<div className="mb-1">
|
||||
<span className="badge-new d-inline-flex align-items-center mb-2 me-2">
|
||||
<FontAwesomeIcon icon={faSparkles} className="me-1" />
|
||||
{t("productNewTag")}
|
||||
</span>
|
||||
|
||||
<h4 className="fw-bold text-white d-inline-flex align-items-center mb-0">
|
||||
<span>{t("productFaceAnimatorTitle")}</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h6 className="fw-normal opacity-75 text-white">
|
||||
{t("productFaceAnimatorText")}
|
||||
</h6>
|
||||
</div>
|
||||
<Link to="/face-animator" className="btn btn-square mt-1">
|
||||
<FontAwesomeIcon icon={faArrowRight} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<img
|
||||
className="img-fluid"
|
||||
src="/images/landing/select-w2l.webp"
|
||||
alt="Face Animator"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-top"
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{uploadModelSection}
|
||||
</div>
|
||||
|
||||
<div className="container section px-md-5 px-xl-3">
|
||||
<div className="row g-4 g-lg-5 flex-row-reverse">
|
||||
<div className="col-12 col-md-6 col-lg-7">
|
||||
<div className="position-relative">
|
||||
<div className="ratio ratio-16x9 video-container">
|
||||
<video
|
||||
autoPlay={true}
|
||||
playsInline={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
>
|
||||
<source src="/videos/tts-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-5 d-flex flex-column justify-content-center gap-3">
|
||||
<h2 className="fw-bold">{t("contextTitle")}</h2>
|
||||
<p className="opacity-75">{t("contextText")}</p>
|
||||
<div className="d-flex mt-3 gap-3">
|
||||
<Link to="/tts" className="btn btn-primary">
|
||||
{t("contextButtonTts")}
|
||||
</Link>
|
||||
<Link to="/voice-conversion" className="btn btn-primary">
|
||||
{t("contextButtonVc")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="container section px-md-5 px-xl-3">
|
||||
<div className="row g-4 g-lg-5">
|
||||
<div className="col-12 col-md-6 col-lg-7">
|
||||
<div className="position-relative">
|
||||
<div className="ratio ratio-16x9 video-container position-relative">
|
||||
<video
|
||||
autoPlay={true}
|
||||
playsInline={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
>
|
||||
<source src="/videos/tts-video.webm" type="video/webm" />
|
||||
</video>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-5 d-flex flex-column justify-content-center gap-3">
|
||||
<h2 className="fw-bold">Speak as your favorite characters</h2>
|
||||
<p className="opacity-75">
|
||||
Transform your speaking voice into someone else's voice with our
|
||||
cutting-edge voice conversion service. Just upload or record your
|
||||
audio and let our AI-powered technology do the rest. Perfect for
|
||||
entertainment, voiceovers, and language learning materials.
|
||||
</p>
|
||||
<div className="d-flex mt-3">
|
||||
<Link to="/voice-conversion" className="btn btn-primary">
|
||||
Convert your voice
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="d-flex flex-column section align-items-center panel rounded-0 my-5">
|
||||
<div className="d-flex flex-column align-items-center text-center cta-container">
|
||||
<div className="d-flex d-lg-none align-items-center mb-4 cta-mobile">
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
width={95}
|
||||
height={80}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="fw-bold">{t("ctaTitle")}</h2>
|
||||
<p className="mw-300 opacity-75">{t("ctaText")}</p>
|
||||
<div className="d-flex gap-3 justify-content-center mt-4">
|
||||
<Link to="/tts" className="btn btn-primary">
|
||||
{t("ctaButtonTts")}
|
||||
</Link>
|
||||
<Link to="/voice-conversion" className="btn btn-primary">
|
||||
{t("ctaButtonVc")}
|
||||
</Link>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
className="hanashi-before d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-grey.webp"
|
||||
alt="grey chevrons"
|
||||
className="chevrons-grey d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
className="chevrons-red d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
className="hanashi-after d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="container text-center community-container">
|
||||
<div className="panel px-4 py-5 d-flex flex-column align-items-center community-container rounded">
|
||||
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
|
||||
<p className="opacity-75">{t("communityText")}</p>
|
||||
<div className="d-flex mt-4 gap-3">
|
||||
<a
|
||||
href="https://discord.gg/fakeyou"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-discord"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-2" />
|
||||
{t("communityButtonDiscord")}
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/intent/follow?screen_name=FakeYouApp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-twitter"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-2" />
|
||||
{t("communityButtonTwitter")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom me-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoggedIn && <FakeYouLandingBody />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* STORYTELLER,AI */}
|
||||
{!isLoggedIn ? (
|
||||
<StorytellerLanding />
|
||||
) : (
|
||||
<Dashboard sessionWrapper={props.sessionWrapper} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Panel } from "components/common";
|
||||
import { useLocalize } from "hooks";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface FakeYouLandingBodyProps {}
|
||||
|
||||
export default function FakeYouLandingBody(props: FakeYouLandingBodyProps) {
|
||||
const { t } = useLocalize("LandingPage");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel clear={true}>
|
||||
<div className="section px-md-5 px-xl-3">
|
||||
<div className="row g-4 g-lg-5 flex-row-reverse">
|
||||
<div className="col-12 col-md-6 col-lg-7">
|
||||
<div className="position-relative">
|
||||
<div className="ratio ratio-16x9 video-container">
|
||||
<video
|
||||
autoPlay={true}
|
||||
playsInline={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
>
|
||||
<source src="/videos/tts-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-5 d-flex flex-column justify-content-center gap-3">
|
||||
<h2 className="fw-bold">{t("contextTitle")}</h2>
|
||||
<p className="opacity-75">{t("contextText")}</p>
|
||||
<div className="d-flex mt-3 gap-3">
|
||||
<Link to="/tts" className="btn btn-primary">
|
||||
{t("contextButtonTts")}
|
||||
</Link>
|
||||
<Link to="/voice-conversion" className="btn btn-primary">
|
||||
{t("contextButtonVc")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div className="py-4 mb-4">
|
||||
<div className="container text-center community-container">
|
||||
<div className="panel px-4 py-5 d-flex flex-column align-items-center community-container rounded">
|
||||
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
|
||||
<p className="opacity-75">{t("communityText")}</p>
|
||||
<div className="d-flex mt-4 gap-3">
|
||||
<a
|
||||
href="https://discord.gg/fakeyou"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-discord"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-2" />
|
||||
{t("communityButtonDiscord")}
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/intent/follow?screen_name=FakeYouApp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-twitter"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-2" />
|
||||
{t("communityButtonTwitter")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom me-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { faArrowRight, faStar, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { WebUrl } from "common/WebUrl";
|
||||
import { Panel } from "components/common";
|
||||
import { useLocalize } from "hooks";
|
||||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface FakeYouLandingHeaderProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
|
||||
}
|
||||
|
||||
export default function FakeYouLandingHeader(props: FakeYouLandingHeaderProps) {
|
||||
const { t } = useLocalize("LandingPage");
|
||||
const isLoggedIn = props.sessionWrapper.isLoggedIn();
|
||||
const isSubscribed = props.sessionSubscriptionsWrapper.hasPaidFeatures();
|
||||
|
||||
let signUpButton = <></>;
|
||||
let viewPricingButton = <></>;
|
||||
let upgradeButton = <></>;
|
||||
let myProfileButton = <></>;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
signUpButton = (
|
||||
<>
|
||||
<Link
|
||||
to="/signup"
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroSignup();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-primary w-100">
|
||||
{t("heroButtonSignUp")}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-2" />
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
viewPricingButton = (
|
||||
<>
|
||||
<Link
|
||||
to={WebUrl.pricingPageWithReferer("tts_hero_new")}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroViewPricing();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-secondary w-100">
|
||||
<FontAwesomeIcon icon={faStar} className="me-2" />
|
||||
{t("heroButtonPricing")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
let displayName = props.sessionWrapper.getDisplayName() as string; // NB: If logged in, should be string
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
myProfileButton = (
|
||||
<>
|
||||
<Link
|
||||
to={url}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroViewProfile();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-secondary w-100">
|
||||
<FontAwesomeIcon icon={faUser} className="me-2" />
|
||||
{t("heroButtonProfile")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
if (!isSubscribed) {
|
||||
upgradeButton = (
|
||||
<>
|
||||
<Link
|
||||
to={WebUrl.pricingPageWithReferer("tts_hero_user")}
|
||||
// onClick={() => {
|
||||
// Analytics.ttsClickHeroUpgradePlan();
|
||||
// }}
|
||||
>
|
||||
<button type="button" className="btn btn-primary w-100">
|
||||
<FontAwesomeIcon icon={faStar} className="me-2" />
|
||||
{t("heroButtonUpgradePlan")}
|
||||
</button>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const randomHeroImage = useMemo(() => {
|
||||
const images = [
|
||||
// Main Images:
|
||||
"mascot/kitsune_pose2.webp",
|
||||
// "mascot/may4th.webp",
|
||||
// "mascot/halloween_1.webp",
|
||||
// "mascot/halloween_2.webp",
|
||||
// "mascot/halloween_3.webp",
|
||||
// "mascot/xmas_1.webp",
|
||||
// "mascot/xmas_2.webp",
|
||||
// "mascot/xmas_3.webp",
|
||||
// "mascot/xmas_4.webp",
|
||||
];
|
||||
|
||||
return images[Math.floor(Math.random() * images.length)];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Panel clear={true} className="mt-5">
|
||||
{/* <Alert
|
||||
id="face-animation-alert"
|
||||
icon={faSparkles}
|
||||
message="Have you tried our new Face Animator? Turn photos of faces into animated lip-synced videos with just a picture and some audio!"
|
||||
alertVariant="new"
|
||||
link="/face-animator"
|
||||
linkText="Try it now"
|
||||
/> */}
|
||||
|
||||
<div className="row flex-md-row-reverse">
|
||||
<div className="col-12 col-lg-5 p-md-0 d-flex justify-content-center">
|
||||
<img
|
||||
src={randomHeroImage}
|
||||
alt="FakeYou Mascot"
|
||||
height={440}
|
||||
width={447}
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-lg-7 d-flex flex-column justify-content-center flex-reverse px-md-5 px-lg-3">
|
||||
<h1 className="fw-bold display-5 text-center text-lg-start px-md-5 px-lg-0">
|
||||
{t("heroTitle")}
|
||||
</h1>
|
||||
<p className="lead opacity-75 pb-4 text-center text-lg-start px-md-5 px-lg-0 pe-lg-5">
|
||||
{t("heroText")}
|
||||
</p>
|
||||
<div className="d-flex flex-column flex-md-row gap-3 mt-3 mb-4 w-100 justify-content-center justify-content-lg-start">
|
||||
{upgradeButton}
|
||||
{signUpButton}
|
||||
{viewPricingButton}
|
||||
{myProfileButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCompass,
|
||||
faMessageDots,
|
||||
faWaveformLines,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Container, Input, Panel, Select } from "components/common";
|
||||
import { useLocalize } from "hooks";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface StorytellerLandingProps {}
|
||||
|
||||
export default function StorytellerLanding(props: StorytellerLandingProps) {
|
||||
const { t } = useLocalize("LandingPage");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container type="panel" className="py-5">
|
||||
<Panel clear={true} className="py-lg-5">
|
||||
<div className="row g-5 g-lg-5 flex-row-reverse">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="position-relative">
|
||||
<div className="ratio ratio-16x9 video-container">
|
||||
<video
|
||||
autoPlay={true}
|
||||
playsInline={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
>
|
||||
<source src="/videos/tts-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-right-bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 d-flex flex-column justify-content-center text-center text-lg-start gap-2">
|
||||
<h1 className="fw-bold display-5">
|
||||
Enabling Anyone to Create Quality Movies with AI
|
||||
</h1>
|
||||
<p className="opacity-75">
|
||||
We are combining generative AI and User Generated Content to
|
||||
radically democratize both audio and video production.
|
||||
</p>
|
||||
<div className="d-flex mt-3 mt-lg-4 gap-3 justify-content-center justify-content-lg-start">
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
label="Sign Up"
|
||||
to="/signup"
|
||||
/>
|
||||
<Button
|
||||
icon={faCompass}
|
||||
label="Explore"
|
||||
variant="secondary"
|
||||
to="/explore"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
|
||||
<div className="panel rounded-0 py-5 my-5">
|
||||
<Container type="panel" className="py-lg-5">
|
||||
<Panel clear={true}>
|
||||
<div className="d-flex flex-column align-items-center rounded-0 mt-lg-4 pt-lg-4">
|
||||
<div className="d-flex flex-column align-items-center text-center cta-container">
|
||||
<h2 className="fw-bold">AI Audio Generation</h2>
|
||||
<p className="mw-300 opacity-75">{t("ctaText")}</p>
|
||||
<div className="d-flex d-lg-none align-items-center mt-2 cta-mobile">
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
width={95}
|
||||
height={80}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/hanashi-before.webp"
|
||||
alt="hanashi before"
|
||||
className="hanashi-before d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-grey.webp"
|
||||
alt="grey chevrons"
|
||||
className="chevrons-grey d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/chevrons-red.webp"
|
||||
alt="red chevrons"
|
||||
className="chevrons-red d-none d-lg-block"
|
||||
width={127}
|
||||
height={108}
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/hanashi-after.webp"
|
||||
alt="hanashi after"
|
||||
className="hanashi-after d-none d-lg-block"
|
||||
width={311}
|
||||
height={311}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row gy-4 mt-lg-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<Panel padding={true} className="panel-inner rounded h-100">
|
||||
<div className="d-flex gap-2">
|
||||
<h4 className="fw-semibold mb-4 flex-grow-1">
|
||||
<FontAwesomeIcon icon={faMessageDots} className="me-3" />
|
||||
Text to Speech
|
||||
</h4>
|
||||
<Link to="/tts">
|
||||
Try more voices{" "}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Select label="Select a Voice" />
|
||||
<Input
|
||||
label="Your Text"
|
||||
placeholder="Type what you want your character to say"
|
||||
/>
|
||||
<div className="d-flex gap-2 justify-content-end">
|
||||
<Button
|
||||
label="Generate"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<Panel padding={true} className="panel-inner rounded h-100">
|
||||
<div className="d-flex gap-2">
|
||||
<h4 className="fw-semibold mb-4 flex-grow-1">
|
||||
<FontAwesomeIcon
|
||||
icon={faWaveformLines}
|
||||
className="me-3"
|
||||
/>
|
||||
Voice to Voice
|
||||
</h4>
|
||||
<Link to="/voice-conversion">
|
||||
Try more voices{" "}
|
||||
<FontAwesomeIcon icon={faArrowRight} className="ms-1" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Select label="Select a Voice" />
|
||||
<div className="d-flex gap-2 justify-content-end">
|
||||
<Button
|
||||
label="Generate"
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<Container type="panel" className="pt-5 my-5">
|
||||
<Panel padding={true}>
|
||||
<div className="d-flex flex-column align-items-center py-5">
|
||||
<h2 className="fw-bold mb-2">{t("communityTitle")}</h2>
|
||||
<p className="opacity-75">{t("communityText")}</p>
|
||||
<div className="d-flex mt-4 gap-3">
|
||||
<a
|
||||
href="https://discord.gg/fakeyou"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-discord"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} className="me-2" />
|
||||
{t("communityButtonDiscord")}
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/intent/follow?screen_name=FakeYouApp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-twitter"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTwitter} className="me-2" />
|
||||
{t("communityButtonTwitter")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/images/landing/bg-dots.webp"
|
||||
alt="background dots"
|
||||
className="dots-left-bottom me-3"
|
||||
/>
|
||||
</Panel>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocument
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import Container from "components/common/Container";
|
||||
import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -25,7 +25,7 @@ interface Props {
|
||||
function LoginPage(props: Props) {
|
||||
let history = useHistory();
|
||||
PosthogClient.recordPageview();
|
||||
|
||||
const domain = useDomainConfig();
|
||||
const [password, setPassword] = useState("");
|
||||
const [usernameOrEmail, setUsernameOrEmail] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -90,12 +90,12 @@ function LoginPage(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container type="panel" className="login-panel">
|
||||
<PageHeader
|
||||
title="Login"
|
||||
subText="Log into your account."
|
||||
panel={false}
|
||||
/>
|
||||
<Container
|
||||
type="panel"
|
||||
className="login-panel d-flex flex-column align-items-center"
|
||||
>
|
||||
<h2 className="fw-bold mb-0 mt-5 mb-4">Login to {domain.title}</h2>
|
||||
|
||||
<Panel padding={true}>
|
||||
{errorWarning}
|
||||
|
||||
@@ -132,10 +132,16 @@ function LoginPage(props: Props) {
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</div>
|
||||
<p className="d-flex flex-column flex-lg-row gap-2">
|
||||
<Link to="/password-reset" className="text-link form-text">
|
||||
<p className="d-flex flex-lg-row gap-2">
|
||||
<Link
|
||||
to="/password-reset"
|
||||
className="text-link form-text flex-grow-1"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
<span className="form-text text-link">
|
||||
Don't have an account? <Link to="/signup">Sign up</Link>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { WebUrl } from "../../../../common/WebUrl";
|
||||
import { BeginStripeCheckoutFlow } from "../../../../common/BeginStripeCheckoutFlow";
|
||||
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import { Container, Panel } from "components/common";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -147,8 +148,8 @@ function PricingPage(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="container pt-5 pb-4 text-center">
|
||||
<Container type="panel">
|
||||
<Panel clear={true} className="text-center my-5">
|
||||
<h1 className=" fw-bold">Pricing</h1>
|
||||
{/* <p className="fs-5">
|
||||
By purchasing FakeYou premium, you help us build more!
|
||||
@@ -157,8 +158,9 @@ function PricingPage(props: Props) {
|
||||
<FontAwesomeIcon icon={faHeart} className="text-red me-3" />
|
||||
By purchasing FakeYou premium, you help us build more!
|
||||
</div>
|
||||
</div>
|
||||
<div className="container mb-5">
|
||||
</Panel>
|
||||
|
||||
<Panel clear={true}>
|
||||
<div className="row gx-3 gy-4">
|
||||
{/* Starter Tier */}
|
||||
{/*<div className="col-12 col-sm-6 col-lg-3" >
|
||||
@@ -712,8 +714,8 @@ function PricingPage(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import { BeginStripeCheckoutFlow } from "../../../../common/BeginStripeCheckoutF
|
||||
import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle";
|
||||
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
|
||||
import Container from "components/common/Container";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Panel from "components/common/Panel";
|
||||
import { useDomainConfig } from "context/DomainConfigContext";
|
||||
|
||||
enum FieldTriState {
|
||||
EMPTY_FALSE,
|
||||
@@ -34,6 +34,7 @@ interface Props {
|
||||
|
||||
function SignupPage(props: Props) {
|
||||
let history = useHistory();
|
||||
const domain = useDomainConfig();
|
||||
|
||||
const parsedQueryString = queryString.parse(window.location.search);
|
||||
|
||||
@@ -349,12 +350,11 @@ function SignupPage(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container type="panel" className="login-panel">
|
||||
<PageHeader
|
||||
title="Sign Up"
|
||||
subText="Create a new account."
|
||||
panel={false}
|
||||
/>
|
||||
<Container
|
||||
type="panel"
|
||||
className="login-panel d-flex flex-column align-items-center"
|
||||
>
|
||||
<h2 className="fw-bold mb-0 mt-5 mb-4">Sign Up for {domain.title}</h2>
|
||||
|
||||
<Panel padding={true}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
Reference in new issue
Block a user