mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-15 06:12:18 +00:00
profile tabs wip
This commit is contained in:
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 };
|
||||
Reference in new issue
Block a user