diff --git a/packages/fakeyou.com/src/components/common/Tabs/Tabs.tsx b/packages/fakeyou.com/src/components/common/Tabs/Tabs.tsx
new file mode 100644
index 0000000000..c205a96d76
--- /dev/null
+++ b/packages/fakeyou.com/src/components/common/Tabs/Tabs.tsx
@@ -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 (
+
+
+ {label}
+
+
+ );
+}
+
+function TabContent({ children }: TabContentProps) {
+ return {children}
;
+}
+
+function Tabs({ tabs }: TabsProps) {
+ const [activeTab, setActiveTab] = useState(tabs[0].to);
+
+ return (
+
+ );
+}
+
+export default Tabs;
diff --git a/packages/fakeyou.com/src/components/common/Tabs/index.ts b/packages/fakeyou.com/src/components/common/Tabs/index.ts
new file mode 100644
index 0000000000..9a31da7f16
--- /dev/null
+++ b/packages/fakeyou.com/src/components/common/Tabs/index.ts
@@ -0,0 +1 @@
+export { default } from "./Tabs";
diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx
index edd1671dba..0ec563b011 100644
--- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx
+++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx
@@ -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 for initial voice creation */}
-
-
+
+
{/* Route for editing the dataset details */}
-
-
+
+
{/* Route for handling dataset token for uploading samples */}
-
-
+
+
@@ -695,14 +692,16 @@ class PageContainer extends React.Component<
-
+
diff --git a/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/NewProfilePage.tsx b/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/NewProfilePage.tsx
new file mode 100644
index 0000000000..99f87cd8f3
--- /dev/null
+++ b/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/NewProfilePage.tsx
@@ -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(undefined);
+ const [notFoundState, setNotFoundState] = useState(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 ;
+ }
+
+ if (notFoundState) {
+ return (
+
+
+
User not found
+
+
+ Back to main
+
+
+
+
+ );
+ }
+
+ if (!userData) {
+ return ;
+ }
+
+ 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 = (
+ <>
+
+
+ Upgrade
+
+ >
+ );
+ }
+ }
+ }
+
+ 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 = (
+ <>
+
+
+ {buttonLabel}
+
+ >
+ );
+ }
+
+ let profileButtonsMobile = ;
+
+ 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 = (
+ <>
+
+
+ {buttonLabel}
+
+ >
+ );
+
+ profileButtonsMobile = (
+
+
+ {upgradeButton}
+ {editProfileButton}
+ {banUserButton}
+
+
+ );
+ }
+
+ let profileDesc = (
+
+ No profile description.
+
+ );
+
+ if (!!userData.profile_rendered_html) {
+ profileDesc = (
+
+ );
+ }
+
+ let profileRows: Array = [];
+ let profileJoinDate: Array = [];
+
+ const createdAt = new Date(userData.created_at);
+ const joinDate = format(createdAt, "LLLL y");
+ profileJoinDate.push(
+
+ );
+
+ if (userData.website_url !== undefined && userData.website_url !== null) {
+ let websiteUrl = {userData.website_url};
+ if (
+ userData.website_url?.startsWith("http://") ||
+ userData.website_url?.startsWith("https://")
+ ) {
+ websiteUrl = (
+
+
+
+
+
+ );
+ }
+
+ profileRows.push({websiteUrl}
);
+ }
+
+ if (userData.twitch_username) {
+ let twitchUrl = `https://twitch.com/${userData.twitch_username}`;
+ let twitchLink = (
+
+
+
+
+
+ );
+
+ profileRows.push({twitchLink}
);
+ }
+
+ if (userData.twitter_username) {
+ let twitterUrl = `https://twitter.com/${userData.twitter_username}`;
+ let twitterLink = (
+
+
+
+
+
+ );
+ profileRows.push({twitterLink}
);
+ }
+
+ if (userData.discord_username) {
+ profileRows.push(
+
+ Discord
+
+ {userData.discord_username}
+
+ }
+ interactive
+ hideOnClick
+ placement="bottom"
+ theme="fakeyou"
+ arrow={false}
+ >
+
+
+
+
+ );
+ }
+
+ if (userData.github_username) {
+ let githubUrl = `https://github.com/${userData.github_username}`;
+ let githubLink = (
+
+
+
+
+
+ );
+ profileRows.push({githubLink}
);
+ }
+
+ if (userData.cashapp_username) {
+ // NB: URL includes a dollar sign
+ let cashAppUrl = `https://cash.me/$${userData.cashapp_username}`;
+ let cashAppLink = (
+
+
+
+
+
+ );
+ profileRows.push({cashAppLink}
);
+ }
+
+ const tabs = [
+ {
+ to: `/profile/${username}/media`,
+ label: "Media",
+ content: Media
,
+ },
+ {
+ to: `/profile/${username}/weights`,
+ label: "Weights",
+ content: Weights
,
+ },
+ {
+ to: `/profile/${username}/favorites`,
+ label: "Favorites",
+ content: Favorites
,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+ {userData.display_name}
+
+
+ {banUserButton}
+ {upgradeButton}
+ {editProfileButton}
+
+
+
{profileJoinDate}
+
{profileDesc}
+
+ {profileRows}
+
+
+
+
+ {profileButtonsMobile}
+
+
+
+
+
+ );
+}
+
+export { NewProfilePage };