profile tabs wip

This commit is contained in:
BFlat committed 2023-11-23 12:03:55 +07:00
1 parent 00d8381797
commit d71637598d
4 files changed
+519 -40

No files matched your search

@@ -0,0 +1,62 @@
import React, { useState } from "react";
import { NavLink } from "react-router-dom";
interface TabProps {
to: string;
label: string;
content?: React.ReactNode;
}
interface TabsProps {
tabs: TabProps[];
}
interface TabContentProps {
children: React.ReactNode;
}
function Tab({ to, label }: TabProps) {
return (
<li className="nav-item">
<NavLink
to={to}
className="nav-link fs-6 px-3 px-lg-4"
activeClassName="active"
>
{label}
</NavLink>
</li>
);
}
function TabContent({ children }: TabContentProps) {
return <div className="tab-content">{children}</div>;
}
function Tabs({ tabs }: TabsProps) {
const [activeTab, setActiveTab] = useState(tabs[0].to);
return (
<nav>
<ul className="nav nav-tabs">
{tabs.map((tab) => (
<Tab key={tab.to} to={tab.to} label={tab.label} />
))}
</ul>
<TabContent>
{tabs.map((tab) => (
<div
key={tab.to}
className={`tab-pane fade ${
activeTab === tab.to ? "show active" : ""
}`}
>
{tab.content}
</div>
))}
</TabContent>
</nav>
);
}
export default Tabs;
@@ -0,0 +1 @@
export { default } from "./Tabs";
@@ -8,7 +8,6 @@ import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
import { LipsyncEditor } from "./pages/lipsync";
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SignupPage } from "./pages/signup/SignupPage";
@@ -96,6 +95,7 @@ import { VoiceDesignerVoiceEditPage } from "./pages/voice_designer/VoiceDesigner
import VoiceDesignerUseVoicePage from "./pages/voice_designer/VoiceDesignerUseVoicePage";
import { PasswordResetEmailPage } from "./pages/password_reset/PasswordResetEmailPage";
import { PasswordResetVerificationPage } from "./pages/password_reset/PasswordResetVerificationPage";
import { NewProfilePage } from "./pages/profile/profile_view/NewProfilePage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -297,7 +297,7 @@ class PageContainer extends React.Component<
</Route>
<Route path="/profile/:username">
<ProfilePage
<NewProfilePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
@@ -639,42 +639,39 @@ class PageContainer extends React.Component<
</Route>
{/* Route for initial voice creation */}
<Route
exact
path="/voice-designer/create"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/create">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for editing the dataset details */}
<Route
exact
path="/voice-designer/dataset/:dataset_token/edit"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/dataset/:dataset_token/edit">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for handling dataset token for uploading samples */}
<Route
exact
path="/voice-designer/dataset/:dataset_token/upload"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/dataset/:dataset_token/upload">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
<Route path="/voice-designer/voice/:voice_token/edit">
@@ -695,14 +692,16 @@ class PageContainer extends React.Component<
</Route>
<Route path="/voice-designer">
<VoiceDesignerMainPage {...{
sessionWrapper:this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
enqueueInferenceJob: this.props.enqueueInferenceJob,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory: this.props.inferenceJobsByCategory
}} />
<VoiceDesignerMainPage
{...{
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
enqueueInferenceJob: this.props.enqueueInferenceJob,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory: this.props.inferenceJobsByCategory,
}}
/>
</Route>
<Route path="/generate-speech">
@@ -0,0 +1,417 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useCallback, useEffect, useState } from "react";
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
import { Link, NavLink, Redirect, useLocation } from "react-router-dom";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { useParams } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faDiscord,
faGithub,
faTwitch,
faTwitter,
} from "@fortawesome/free-brands-svg-icons";
import {
faBan,
faGear,
faGlobe,
faDollarSign,
faStar,
} from "@fortawesome/free-solid-svg-icons";
import {
GetUserByUsername,
GetUserByUsernameIsErr,
GetUserByUsernameIsOk,
User,
UserLookupError,
} from "@storyteller/components/src/api/user/GetUserByUsername";
import { format } from "date-fns";
import { WebUrl } from "../../../../../common/WebUrl";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import { faCalendarAlt } from "@fortawesome/pro-solid-svg-icons";
// import { CommentComponent } from "../../../_common/comments/CommentComponent";
import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient";
import { Panel } from "components/common";
import Container from "components/common/Container";
import Tabs from "components/common/Tabs";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
function NewProfilePage(this: any, props: Props) {
const { username }: { username: string } = useParams();
const { pathname } = useLocation();
PosthogClient.recordPageview();
const [userData, setUserData] = useState<User | undefined>(undefined);
const [notFoundState, setNotFoundState] = useState<boolean>(false);
const getUser = useCallback(async (username) => {
const response = await GetUserByUsername(username);
if (GetUserByUsernameIsOk(response)) {
setUserData(response);
} else if (GetUserByUsernameIsErr(response)) {
switch (response) {
case UserLookupError.NotFound:
setNotFoundState(true);
break;
}
}
}, []);
useEffect(() => {
getUser(username);
}, [username, getUser]);
const documentTitle =
userData?.display_name === undefined
? undefined
: `${userData.display_name}`;
usePrefixedDocumentTitle(documentTitle);
if (
pathname === `/profile/${username}` ||
pathname === `/profile/${username}/`
) {
return <Redirect to={`/profile/${username}/media`} />;
}
if (notFoundState) {
return (
<div className="container py-5">
<div className="py-5">
<h1 className="fw-semibold text-center mb-4">User not found</h1>
<div className="text-center">
<Link className="btn btn-primary" to="/">
Back to main
</Link>
</div>
</div>
</div>
);
}
if (!userData) {
return <div />;
}
let userEmailHash = userData.email_gravatar_hash;
let editProfileButton = undefined;
let banUserButton = undefined;
let upgradeButton = undefined;
if (props.sessionWrapper.isLoggedIn()) {
if (props.sessionWrapper.userTokenMatches(userData.user_token)) {
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
const upgradeLinkUrl = WebUrl.pricingPage();
upgradeButton = (
<>
<Link className="btn btn-primary" to={upgradeLinkUrl}>
<FontAwesomeIcon icon={faStar} className="me-2" />
<span>Upgrade</span>
</Link>
</>
);
}
}
}
if (props.sessionWrapper.canBanUsers()) {
const currentlyBanned = userData.maybe_moderator_fields?.is_banned;
const banLinkUrl = WebUrl.userProfileBanPage(userData.username);
const buttonLabel = currentlyBanned ? "Unban" : "Ban";
const banButtonCss = currentlyBanned
? "btn btn-secondary"
: "btn btn-destructive";
banUserButton = (
<>
<Link className={banButtonCss} to={banLinkUrl}>
<FontAwesomeIcon icon={faBan} className="me-2" />
{buttonLabel}
</Link>
</>
);
}
let profileButtonsMobile = <span />;
if (props.sessionWrapper.canEditUserProfile(userData.username)) {
const editLinkUrl = WebUrl.userProfileEditPage(userData.username);
// Mods shouldn't edit preferences.
const buttonLabel = props.sessionWrapper.userTokenMatches(
userData.user_token
)
? "Edit Profile & Preferences"
: "Edit Profile";
editProfileButton = (
<>
<Link className={"btn btn-secondary"} to={editLinkUrl}>
<FontAwesomeIcon icon={faGear} className="me-2" />
{buttonLabel}
</Link>
</>
);
profileButtonsMobile = (
<div className="container d-flex d-lg-none mb-4">
<div className="d-flex w-100 gap-3 justify-content-center flex-column flex-md-row">
{upgradeButton}
{editProfileButton}
{banUserButton}
</div>
</div>
);
}
let profileDesc = (
<div className="mt-3 text-center text-lg-start opacity-50">
No profile description.
</div>
);
if (!!userData.profile_rendered_html) {
profileDesc = (
<div
className="mt-3 text-center text-lg-start"
dangerouslySetInnerHTML={{
__html: userData.profile_rendered_html || "",
}}
/>
);
}
let profileRows: Array<JSX.Element> = [];
let profileJoinDate: Array<JSX.Element> = [];
const createdAt = new Date(userData.created_at);
const joinDate = format(createdAt, "LLLL y");
profileJoinDate.push(
<div
key="created"
className="d-flex align-items-center justify-content-center justify-content-lg-start"
>
<FontAwesomeIcon icon={faCalendarAlt} className="me-2" />
<p className="fw-normal">Joined {joinDate}</p>
</div>
);
if (userData.website_url !== undefined && userData.website_url !== null) {
let websiteUrl = <span>{userData.website_url}</span>;
if (
userData.website_url?.startsWith("http://") ||
userData.website_url?.startsWith("https://")
) {
websiteUrl = (
<Tippy
content="Website"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
href={userData.website_url}
target="_blank"
rel="noopener noreferrer nofollow"
>
<FontAwesomeIcon icon={faGlobe} />
</a>
</Tippy>
);
}
profileRows.push(<div key="website">{websiteUrl}</div>);
}
if (userData.twitch_username) {
let twitchUrl = `https://twitch.com/${userData.twitch_username}`;
let twitchLink = (
<Tippy
content="Twitch"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={twitchUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faTwitch} />
</a>
</Tippy>
);
profileRows.push(<div key="twitch">{twitchLink}</div>);
}
if (userData.twitter_username) {
let twitterUrl = `https://twitter.com/${userData.twitter_username}`;
let twitterLink = (
<Tippy
content="Twitter"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={twitterUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faTwitter} />
</a>
</Tippy>
);
profileRows.push(<div key="twitter">{twitterLink}</div>);
}
if (userData.discord_username) {
profileRows.push(
<Tippy
allowHTML
content={
<div className="text-center">
Discord
<br />
<p className="fw-semibold fs-6">{userData.discord_username}</p>
</div>
}
interactive
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
// eslint-disable-next-line no-script-url
href="javascript:;"
// onClick={copyToClipboard(userData.discord_username)}
>
<FontAwesomeIcon icon={faDiscord} />
</a>
</Tippy>
);
}
if (userData.github_username) {
let githubUrl = `https://github.com/${userData.github_username}`;
let githubLink = (
<Tippy
content="GitHub"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
href={githubUrl}
target="_blank"
rel="noopener noreferrer nofollow"
className="fw-normal"
>
<FontAwesomeIcon icon={faGithub} />
</a>
</Tippy>
);
profileRows.push(<div key="github">{githubLink}</div>);
}
if (userData.cashapp_username) {
// NB: URL includes a dollar sign
let cashAppUrl = `https://cash.me/$${userData.cashapp_username}`;
let cashAppLink = (
<Tippy
content="CashApp"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={cashAppUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faDollarSign} />
</a>
</Tippy>
);
profileRows.push(<div key="cashapp">{cashAppLink}</div>);
}
const tabs = [
{
to: `/profile/${username}/media`,
label: "Media",
content: <div>Media</div>,
},
{
to: `/profile/${username}/weights`,
label: "Weights",
content: <div>Weights</div>,
},
{
to: `/profile/${username}/favorites`,
label: "Favorites",
content: <div>Favorites</div>,
},
];
return (
<Container type="panel">
<div className="d-flex flex-column flex-lg-row py-4">
<div className="mb-3 me-lg-4 mb-lg-0">
<div className="border-3 d-none d-lg-block">
<Gravatar
size={150}
username={userData.display_name}
email_hash={userEmailHash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
</div>
<div className="border-3 text-center d-lg-none">
<Gravatar
size={100}
username={userData.display_name}
email_hash={userEmailHash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
</div>
</div>
<div className="d-flex flex-column w-100">
<div className="d-flex">
<h1 className="fw-bold mb-0 flex-grow-1 text-center text-lg-start">
{userData.display_name}
</h1>
<div className="gap-3 d-none d-lg-flex">
{banUserButton}
{upgradeButton}
{editProfileButton}
</div>
</div>
<div className="opacity-75 mt-1">{profileJoinDate}</div>
<div>{profileDesc}</div>
<div className="d-flex mt-3 gap-4 gap-lg-3 profile-social-icons justify-content-center justify-content-lg-start">
{profileRows}
</div>
</div>
</div>
{profileButtonsMobile}
<Panel mb={true}>
<Tabs tabs={tabs} />
</Panel>
</Container>
);
}
export { NewProfilePage };