new profile page and sidebar removal (#566)

* top and side nav changes

* profile page v3 and sidebar removal

* updates

* padding fixes

* side panel size increase
This commit is contained in:
BFlat authored and GitHub committed 2024-05-20 14:57:29 -04:00
1 parent da75f9cb30
commit 254c9db9a6
20 files changed
+1545 -101

No files matched your search

@@ -65,6 +65,23 @@
border-color: lighten($secondary, 5%);
}
.button-action,
.button-action:focus,
.button-action:active {
background-color: $action;
color: $white !important;
border: 2.5px solid;
border-color: $action;
transition: 0.2s;
}
.button-action:hover {
background-color: lighten($action, 5%);
color: $white !important;
border: 2.5px solid;
border-color: lighten($action, 5%);
}
.button-danger,
.button-danger:focus,
.button-danger:active {
@@ -28,8 +28,15 @@ export default function Button({
}: ButtonProps) {
let variant = variantProps;
if (isActive !== undefined) {
if (isActive) variant = "primary";
else variant = "secondary";
if (isActive) {
variant = "primary";
} else variant = "secondary";
}
if (variant === "action") {
variant = "action";
}
if (variant === "danger") {
variant = "danger";
}
let iconMarginClass = !square && label ? (iconFlip ? "ms-2" : "me-2") : "";
let IconComponent = icon ? (
@@ -48,6 +48,7 @@
transition: 0.4s ease;
object-fit: cover;
object-position: center;
aspect-ratio: 16/9;
}
.card-img-overlay {
@@ -1,11 +1,9 @@
import React, { useEffect, useState } from "react";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faPlayCircle } from "@fortawesome/pro-solid-svg-icons";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import Badge from "components/common/Badge";
import Button from "components/common/Button";
import { CardFooter } from "components/entities";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { Link } from "react-router-dom";
import getCardUrl from "../getCardUrl";
@@ -164,14 +162,13 @@ export default function VideoCard({
/>
)}
</div>
<FontAwesomeIcon icon={faPlayCircle} className="card-video-play" />
<div className="card-img-overlay-text">
<div>
<h6 className="fw-semibold text-white mb-1">
{data.weight_name}
</h6>
<p className="fs-7 opacity-75">{timeAgo}</p>
<CardFooter
{/* <CardFooter
{...{
creator: data?.maybe_creator,
entityToken: data.token,
@@ -180,7 +177,7 @@ export default function VideoCard({
makeRatingsProps: ratings?.makeProps,
showCreator,
}}
/>
/> */}
</div>
</div>
</div>
@@ -4,12 +4,14 @@ interface ContainerProps {
children: React.ReactNode;
type?: "full" | "padded" | "panel" | "panel-full";
className?: string;
style?: React.CSSProperties;
}
export default function PageContainer({
children,
type = "full",
className = "",
style,
}: ContainerProps) {
let containerClass = "";
@@ -34,5 +36,9 @@ export default function PageContainer({
// Merge the classNames
const mergedClassNames = `${containerClass} ${className}`.trim();
return <div className={mergedClassNames}>{children}</div>;
return (
<div className={mergedClassNames} style={style}>
{children}
</div>
);
}
@@ -11,6 +11,7 @@ interface NavItemProps {
label: string;
isHoverable?: boolean;
link?: string;
externalLink?: string;
className?: string;
dropdownItems?: {
id: number;
@@ -27,6 +28,7 @@ export default function NavItem({
isHoverable,
link,
className,
externalLink,
}: NavItemProps) {
const [isHovered, setIsHovered] = useState(false);
@@ -55,6 +57,13 @@ export default function NavItem({
{label}
</Link>
)}
{externalLink && (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a href={externalLink} className={textHover}>
{icon && <FontAwesomeIcon icon={icon} className="me-2 fs-7" />}
{label}
</a>
)}
</>
);
}
@@ -3,10 +3,14 @@
.nav-tabs {
border-bottom: 2px solid #ffffff15;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
overflow-x: auto !important;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch;
@media screen and (max-width: 768px) {
width: 100vw;
}
.nav-link {
border: none;
padding: 1rem 1.4rem;
@@ -0,0 +1,307 @@
@import "scss/colours.scss";
/* Toggle Styles */
#wrapper {
padding-left: 0;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
$topbar-height: 66px;
#profile-sidebar-wrapper {
z-index: 999;
position: fixed;
left: 340px;
width: 0;
height: calc(100% - $topbar-height);
margin-left: -340px;
overflow-x: hidden;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
margin-top: $topbar-height;
display: grid;
grid-template-rows: 1fr auto;
&::-webkit-scrollbar {
width: 0;
}
&::-webkit-scrollbar-thumb {
background-color: rgba($secondary, 1);
border-radius: 0.5rem;
}
&::-webkit-scrollbar-track {
background-color: rgba($panel, 1) !important;
}
&:hover::-webkit-scrollbar {
width: 10px;
}
}
.profile-sidebar-panel {
background-color: #39394d;
height: 100%;
border-radius: 0.8rem;
}
#wrapper.toggled #profile-sidebar-wrapper {
width: 340px;
margin-left: 0;
}
#page-content-wrapper {
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
width: 100%;
position: relative;
padding-left: 340px;
padding-top: $topbar-height;
transition: all 0.2s ease;
&.no-page-padding-top {
padding-top: 0;
}
}
/* Sidebar Styles */
.profile-sidebar-nav {
width: 340px;
margin: 0;
padding: 0;
list-style: none;
height: 100%;
background: $panel;
border-color: rgba(255, 255, 255, 0.04) !important;
border-right: 1px solid;
padding-top: 1rem;
}
.profile-sidebar-nav li a {
display: block;
text-decoration: none;
color: rgba($white, 0.7);
font-weight: 500;
font-size: 14px;
margin: 0rem 1rem;
padding: 0.4rem 1rem;
border-radius: 0.3rem;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
.profile-sidebar-nav li a:hover {
text-decoration: none;
color: $white;
}
.profile-sidebar-nav li a:active,
.profile-sidebar-nav li a:focus {
text-decoration: none;
}
.profile-sidebar-brand {
font-size: 18px;
padding-top: 1.5rem;
line-height: 0;
margin-left: 1.5rem;
}
.profile-sidebar-buttons {
margin-left: 1.5rem;
}
.profile-sidebar-heading {
margin-left: 1.5rem;
margin-bottom: 0.25rem;
margin-top: 0.75rem;
opacity: 0.5;
font-size: 13px;
}
.profile-sidebar-heading-icon {
margin-right: 0.75rem;
}
.active-link {
color: $white !important;
background-color: $secondary;
}
#overlay {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 900;
}
.toggled #page-content-wrapper {
position: relative;
z-index: 2;
}
.toggled #overlay {
display: block;
}
.no-padding {
padding-left: 0 !important;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease-out;
}
/* Visible state for the sidebar */
.sidebar.visible {
transform: translateX(0);
}
#topbar-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: $topbar-height;
background: $panel;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
z-index: 1000;
transition: 0.15s;
&.topbar-wrapper-transparent {
background: none;
}
}
.topbar-nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 100%;
padding: 0 1rem;
justify-content: space-between;
align-items: center;
.topbar-nav-right {
display: flex;
justify-content: flex-end;
}
.topbar-nav-right {
display: flex;
justify-content: flex-end;
}
.topbar-nav-center {
grid-column: 2;
}
.button {
white-space: nowrap;
}
}
.topbar-mobile-search-bar-container {
position: absolute;
width: 100%;
z-index: 1000;
top: 0;
height: $topbar-height;
background-color: $panel;
.topbar-mobile-search-bar {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
padding: 0 1rem;
.close-search-button {
position: absolute;
top: 0;
right: 0;
margin: 0 1.8rem;
color: $white;
height: 100%;
font-size: 1.5rem;
cursor: pointer;
z-index: 4;
background: none;
border: none;
padding: 0;
}
}
}
.mobile-fixed-bottom {
position: sticky;
bottom: 0;
padding: 0.75rem 0rem;
width: 100%;
background-color: $panel-inner;
border-top: 1px solid lighten($panel-inner, 5%) !important;
font-size: 11px;
.profile-sidebar-heading {
margin-top: 0;
font-size: 13px;
}
}
@media (min-width: 992px) {
#wrapper {
padding-left: 340px;
}
#wrapper.toggled {
padding-left: 0;
}
#profile-sidebar-wrapper {
width: 340px;
}
#wrapper.toggled #profile-sidebar-wrapper {
width: 0;
}
#page-content-wrapper {
position: relative;
}
#wrapper.toggled #page-content-wrapper {
position: relative;
margin-right: 0;
}
}
@media (max-width: 992px) {
#wrapper.toggled #profile-sidebar-wrapper {
width: 100%;
}
#profile-sidebar-wrapper {
left: 100%;
margin-left: 0;
}
.profile-sidebar-nav {
width: 100%;
}
}
@@ -0,0 +1,88 @@
import React, { useEffect, useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { FakeYouFrontendEnvironment } from "@storyteller/components/src/env/FakeYouFrontendEnvironment";
import "./ProfileSidePanel.scss";
import UserProfileInfo from "./UserProfileInfo";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
interface ProfileSidePanelProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export default function ProfileSidePanel({
sessionWrapper,
sessionSubscriptionsWrapper,
}: ProfileSidePanelProps) {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const fakeYouFrontendEnv = FakeYouFrontendEnvironment.getInstance();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isDevelopmentEnv = fakeYouFrontendEnv.isDevelopment();
const isOnProfilePage = window.location.pathname.includes("/profile/");
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
const wrapper = document.getElementById("wrapper");
const overlay = document.getElementById("overlay");
if (
(!wrapper?.contains(event.target as Node) ||
overlay?.contains(event.target as Node)) &&
window.innerWidth < 992
) {
wrapper?.classList.remove("toggled");
}
};
document.addEventListener("click", handleOutsideClick);
return () => {
document.removeEventListener("click", handleOutsideClick);
};
}, []);
useEffect(() => {
// Update window width on resize
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
// Cleanup listener on unmount
window.removeEventListener("resize", handleResize);
};
}, []);
const shouldNotShowSidebar = !isOnProfilePage;
const shouldShowSidebar = windowWidth >= 992 && !shouldNotShowSidebar;
const sidebarClassName = `sidebar d-none d-lg-block ${
shouldShowSidebar ? "visible" : ""
}`.trim();
useEffect(() => {
const contentWrapper = document.getElementById("page-content-wrapper");
if (shouldShowSidebar && isOnProfilePage) {
contentWrapper?.classList.remove("no-padding");
} else {
contentWrapper?.classList.add("no-padding");
}
}, [shouldShowSidebar, isOnProfilePage]);
return (
<>
<div id="profile-sidebar-wrapper" className={sidebarClassName}>
<div className="py-3 ps-3 h-100">
<div className="profile-sidebar-panel">
<UserProfileInfo
sessionWrapper={sessionWrapper}
sessionSubscriptionsWrapper={sessionSubscriptionsWrapper}
/>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,375 @@
import React, { useCallback, useEffect, useState } from "react";
import {
GetUserByUsername,
GetUserByUsernameIsErr,
GetUserByUsernameIsOk,
User,
UserLookupError,
} from "@storyteller/components/src/api/user/GetUserByUsername";
import { useLocation } from "react-router-dom";
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBan,
faCalendarAlt,
faDollarSign,
faGear,
faGlobe,
faStar,
} from "@fortawesome/pro-solid-svg-icons";
import { format } from "date-fns";
import { Button } from "components/common";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { WebUrl } from "common/WebUrl";
import {
faDiscord,
faGithub,
faTwitch,
faTwitter,
} from "@fortawesome/free-brands-svg-icons";
import Tippy from "@tippyjs/react";
interface UserProfileInfoProps {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
export default function UserProfileInfo(props: UserProfileInfoProps) {
const location = useLocation();
const pathSegments = location.pathname.split("/");
const username = pathSegments[2];
const [userData, setUserData] = useState<User | undefined>(undefined);
const [notFoundState, setNotFoundState] = useState<boolean>(false);
const getUser = useCallback(async username => {
const response = await GetUserByUsername(username);
console.log("Response received:", response);
if (GetUserByUsernameIsOk(response)) {
setUserData(response);
} else if (GetUserByUsernameIsErr(response)) {
switch (response) {
case UserLookupError.NotFound:
setNotFoundState(true);
break;
}
}
}, []);
useEffect(() => {
getUser(username);
}, [username, getUser]);
if (notFoundState) {
return <h3>User not found</h3>;
}
if (!userData) {
return null;
}
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>
);
let profileDesc = (
<div className="mt-3 text-center opacity-50">No profile description.</div>
);
if (!!userData.profile_rendered_html) {
profileDesc = (
<div
className="mt-3 text-center"
dangerouslySetInnerHTML={{
__html: userData.profile_rendered_html || "",
}}
/>
);
}
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 = (
<>
<Button
to={upgradeLinkUrl}
small={true}
label="Upgrade"
icon={faStar}
/>
</>
);
}
}
}
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 ? "secondary" : "danger";
banUserButton = (
<>
<Button
variant={banButtonCss}
to={banLinkUrl}
label={buttonLabel}
icon={faBan}
small={true}
/>
</>
);
}
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 = (
<>
<Button
variant="action"
to={editLinkUrl}
label={buttonLabel}
icon={faGear}
small={true}
/>
</>
);
}
let profileRows: Array<JSX.Element> = [];
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}
>
<button
onClick={() =>
navigator.clipboard.writeText(userData.discord_username || "")
}
style={{
cursor: "pointer",
border: "none",
background: "transparent",
}}
className="m-0 p-0 text-link"
>
<FontAwesomeIcon icon={faDiscord} />
</button>
</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>);
}
return (
<>
{/* DESKTOP */}
<div className="py-4 px-4 d-none d-lg-flex flex-column align-items-center">
<Gravatar
size={150}
username={userData.display_name}
email_hash={userData.email_gravatar_hash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
<h3 className="fw-bold mt-3 text-center mb-1">
{userData.display_name || "Profile Name"}
</h3>
<div className="opacity-75 mb-2">{profileJoinDate}</div>
<div style={{ fontSize: "15px" }}>{profileDesc}</div>
<>
<hr className="w-100" />
<div className="mb-3 d-flex gap-4 gap-lg-3 profile-social-icons justify-content-center justify-content-lg-start">
{profileRows}
</div>
{props.sessionWrapper.isLoggedIn() && (
<div className="gap-2 d-flex flex-column w-100">
{editProfileButton}
{banUserButton}
{upgradeButton}
</div>
)}
</>
</div>
{/* MOBILE */}
<div className="px-3 d-flex d-lg-none flex-column">
<div className="d-flex gap-3 align-items-center">
<Gravatar
size={50}
username={userData.display_name}
email_hash={userData.email_gravatar_hash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
<div>
<h3 className="fw-bold mb-0">
{userData.display_name || "Profile Name"}
</h3>
<div className="opacity-75 mb- fs-7">{profileJoinDate}</div>
</div>
</div>
{/* <div style={{ fontSize: "15px" }}>{profileDesc}</div> */}
<div className="my-3 d-flex gap-3 gap-lg-3 profile-social-icons">
{profileRows}
</div>
{props.sessionWrapper.isLoggedIn() && (
<div className="gap-2 d-flex w-100">
{editProfileButton}
{banUserButton}
{upgradeButton}
</div>
)}
</div>
</>
);
}
@@ -62,6 +62,7 @@ export default function SideNav({
const isOnLoginPage = window.location.pathname.includes("/login");
const isOnSignUpPage = window.location.pathname.includes("/signup");
const isOnStudioPage = window.location.pathname.includes("/studio");
const isOnProfilePage = window.location.pathname.includes("/profile/");
const isOnBetaKeyRedeemPage =
window.location.pathname.includes("/beta-key/redeem");
const domain: WebsiteConfig = useDomainConfig();
@@ -115,9 +116,13 @@ export default function SideNav({
}, []);
const shouldNotShowSidebar =
(!isLoggedIn && (isOnLandingPage || isOnLoginPage || isOnSignUpPage)) ||
(!isLoggedIn &&
(isOnLandingPage ||
isOnLoginPage ||
isOnSignUpPage ||
isOnProfilePage)) ||
isOnStudioPage ||
(domain.website === Website.StorytellerAi && isOnLandingPage) ||
isOnProfilePage ||
isOnBetaKeyRedeemPage;
const shouldShowSidebar = windowWidth >= 992 && !shouldNotShowSidebar;
const sidebarClassName = `sidebar ${
@@ -337,6 +342,10 @@ export default function SideNav({
</>
);
if (domain.website === Website.StorytellerAi) {
return null;
}
return (
<>
<div id="sidebar-wrapper" className={sidebarClassName}>
@@ -10,19 +10,30 @@ import {
faClipboardList,
faChevronLeft,
faMessageImage,
faPortalEnter,
faHome,
faCompass,
faCloudUpload,
faTrophy,
faBookOpen,
faFilms,
} from "@fortawesome/pro-solid-svg-icons";
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 { Link, NavLink, useHistory } from "react-router-dom";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { Logout } from "@storyteller/components/src/api/session/Logout";
import { useModal, useSession } from "hooks";
import { useLocalize, useModal, useSession } from "hooks";
import { InferenceJobsModal } from "components/modals";
import { useDomainConfig } from "context/DomainConfigContext";
import NavItem from "../../common/NavItem/NavItem";
import ProfileDropdown from "components/common/ProfileDropdown";
import "./TopNav.scss";
import { Website } from "@storyteller/components/src/env/GetWebsite";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { GetDiscordLink } from "@storyteller/components/src/env/GetDiscordLink";
interface TopNavProps {
sessionWrapper: SessionWrapper;
@@ -43,7 +54,7 @@ export default function TopNav({
const [isFocused, setIsFocused] = useState(false);
const wrapper = document.getElementById("wrapper");
const [menuButtonIcon, setMenuButtonIcon] = useState(faBars);
// const { t } = useLocalize("TopNav");
const { t } = useLocalize("SideNav");
const isOnLandingPage = window.location.pathname === "/";
const isOnLoginPage = window.location.pathname.includes("/login");
const isOnSignUpPage = window.location.pathname.includes("/signup");
@@ -56,20 +67,10 @@ export default function TopNav({
const [isScrolled, setIsScrolled] = useState(false);
const loggedIn = sessionWrapper.isLoggedIn();
const showNavItem =
!loggedIn && (isOnLandingPage || isOnLoginPage || isOnSignUpPage);
(!loggedIn && (isOnLandingPage || isOnLoginPage || isOnSignUpPage)) ||
domain.titlePart === "Storyteller AI";
const handleMenuButtonClick = () => {
if (window.innerWidth < 1200) {
if (wrapper) {
wrapper.classList.toggle("toggled");
if (menuButtonIcon === faBars) {
setMenuButtonIcon(faXmark);
} else {
setMenuButtonIcon(faBars);
}
}
}
};
const [mobileMenu, setMobileMenu] = useState("d-none");
const handleSearchButtonClick = () => {
setIsMobileSearchBarVisible(true);
@@ -212,6 +213,21 @@ export default function TopNav({
return null;
}
const handleNavLinkClick = () => {
setMobileMenu("d-none");
setMenuButtonIcon(faBars);
};
const handleMenuButtonClick = () => {
if (mobileMenu === "d-none") {
setMobileMenu("d-block");
setMenuButtonIcon(faXmark);
} else {
setMobileMenu("d-none");
setMenuButtonIcon(faBars);
}
};
return (
<div
id="topbar-wrapper"
@@ -225,8 +241,8 @@ export default function TopNav({
>
<div className="topbar-nav">
<div className="topbar-nav-left">
<div className="d-flex gap-3 align-items-center">
<Link to="/">
<div className="d-flex gap-1 align-items-center">
<Link to="/" className="me-3">
<img
src={domain.logo}
alt={`${domain.titlePart}: Cartoon and Celebrity Text to Speech`}
@@ -240,15 +256,20 @@ export default function TopNav({
className="mb-0 d-block d-lg-none"
/>
</Link>
{showNavItem && (
<div className="d-none d-lg-block">
<NavItem
isHoverable={true}
label="AI Tools"
dropdownItems={aiToolsDropdown}
/>
</div>
)}
<div className="d-none d-lg-block">
<NavItem
isHoverable={true}
label="AI Tools"
dropdownItems={aiToolsDropdown}
/>
</div>
<NavItem
icon={faStar}
label="Pricing"
link="/pricing"
className="me-3 d-none d-lg-block"
/>
{isOnStudioPage && loggedIn && (
<Button
icon={faChevronLeft}
@@ -265,36 +286,43 @@ export default function TopNav({
<div className="topbar-nav-center">
{/* Search Bar */}
<div className="d-none d-lg-block">
{(!isOnLandingPage &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(loggedIn &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(isOnLandingPage &&
isScrolled &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ? (
<SearchBar
onFocus={onFocusHandler}
onBlur={onBlurHandler}
isFocused={isFocused}
/>
) : null}
{domain.titlePart === "FakeYou" && (
<>
{(!isOnLandingPage &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(loggedIn &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ||
(isOnLandingPage &&
isScrolled &&
!isOnLoginPage &&
!isOnSignUpPage &&
!isOnStudioPage) ? (
<SearchBar
onFocus={onFocusHandler}
onBlur={onBlurHandler}
isFocused={isFocused}
/>
) : null}
</>
)}
</div>
</div>
<div className="topbar-nav-right">
{showNavItem && (
<NavItem
icon={faStar}
label="Pricing"
link="/pricing"
className="me-3 d-none d-lg-block"
/>
{domain.titlePart === "Storyteller AI" && (
<div className="d-none d-lg-block">
<Button
icon={faPortalEnter}
label="Enter Storyteller Studio"
href="https://studio.storyteller.ai/"
small={true}
className="me-3"
/>
</div>
)}
<div className="d-flex align-items-center gap-2">
@@ -366,7 +394,176 @@ export default function TopNav({
</div>
</div>
{/* <div className="topbar-nav bg-panel">test</div> */}
<div className={`${mobileMenu} d-lg-none`} style={{ height: "100vh" }}>
<ul className="sidebar-nav">
<li>
<NavLink
exact={true}
to={domain.website === Website.FakeYou ? "/" : "/dashboard"}
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon icon={faHome} className="sidebar-heading-icon" />
{domain.website === Website.FakeYou ? "Home" : "Dashboard"}
</NavLink>
</li>
<li>
<NavLink
to="/pricing"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon icon={faStar} className="sidebar-heading-icon" />
{t("infoPricing")}
</NavLink>
</li>
<li>
<NavLink
to="/explore"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faCompass}
className="sidebar-heading-icon"
/>
Explore
</NavLink>
</li>
<li>
<NavLink
to="/inference-jobs-list"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faClipboardList}
className="sidebar-heading-icon"
/>
My Jobs
</NavLink>
</li>
<li className="sidebar-heading">{t("speechTitle")}</li>
<li>
<NavLink
to="/tts"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faMessageDots}
className="sidebar-heading-icon"
/>
{t("speechTts")}
</NavLink>
</li>
<li>
<NavLink
to="/voice-conversion"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faWaveformLines}
className="sidebar-heading-icon"
/>
{t("speechVc")}
</NavLink>
</li>
<li>
<NavLink
to="/voice-designer"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faWandMagicSparkles}
className="sidebar-heading-icon"
/>
{"Voice Designer"}
</NavLink>
</li>
<li className="sidebar-heading">{t("videoTitle")}</li>
<li>
<NavLink
to="/video-styletransfer"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faFilms}
className="sidebar-heading-icon"
/>
{t("videoStyleTransfer")}
</NavLink>
</li>
<li>
<NavLink
to="/face-animator"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faFaceViewfinder}
className="sidebar-heading-icon"
/>
{t("videoFaceAnimator")}
</NavLink>
</li>
{/* {maybeImageGeneration}
{maybeBetaFeatures} */}
<li className="sidebar-heading">{t("communityTitle")}</li>
<li>
<NavLink
to="/contribute"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faCloudUpload}
className="sidebar-heading-icon"
/>
{t("communityUploadModels")}
</NavLink>
</li>
<li className="mb-3">
<a href={GetDiscordLink()} target="_blank" rel="noreferrer">
<FontAwesomeIcon
icon={faDiscord}
className="sidebar-heading-icon"
/>
{t("communityDiscord")}
</a>
<NavLink
to="/leaderboard"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faTrophy}
className="sidebar-heading-icon"
/>
{t("communityLeaderboard")}
</NavLink>
<NavLink
to="/guide"
activeClassName="active-link"
onClick={handleNavLinkClick}
>
<FontAwesomeIcon
icon={faBookOpen}
className="sidebar-heading-icon"
/>
{t("communityGuide")}
</NavLink>
</li>
</ul>
</div>
{/* Mobile Searchbar */}
{isMobileSearchBarVisible && (
@@ -1,6 +1,7 @@
// Default colours
$primary: #e66462;
$secondary: #39394c;
$action: #55556b;
$bg: #1a1a27;
$input: #39394c;
$input-placeholder: #aeaeb3;
+1 -1
View File
@@ -58,7 +58,7 @@ $topbar-height: 66px;
flex-direction: column;
width: 100%;
position: relative;
padding-left: 260px;
// padding-left: 260px;
padding-top: $topbar-height;
transition: all 0.2s ease;
@@ -1093,7 +1093,7 @@ styles with important tags:
}
.tippy-box[data-theme~="fakeyou"] {
background-color: lighten($secondary, 5%);
background-color: darken($secondary, 12%);
color: $white;
}
@@ -98,7 +98,6 @@ import VcModelEditPage from "./pages/vc/vc_model_edit/VcModelEditPage";
import VcModelDeletePage from "./pages/vc/vc_model_delete/VcModelDeletePage";
// import { StorytellerStudioListPage } from "./pages/storyteller_studio/StorytellerStudioPage";
import TopNav from "components/layout/TopNav/TopNav";
import SideNav from "components/layout/SideNav/SideNav";
import MediaPage from "./pages/media/MediaPage";
import EditCoverImage from "./pages/media/EditCoverImage";
import MediaRenamePage from "./pages/media/MediaRenamePage";
@@ -110,7 +109,7 @@ import { PasswordResetEmailPage } from "./pages/password_reset/PasswordResetEmai
import { PasswordResetVerificationPage } from "./pages/password_reset/PasswordResetVerificationPage";
// import EngineCompositor from "./pages/EngineCompositor/EngineCompositor";
import InferenceJobsPage from "./pages/inference_jobs_page/InferenceJobsPage";
import { NewProfilePage } from "./pages/profile/profile_view/NewProfilePage";
// import { NewProfilePage } from "./pages/profile/profile_view/NewProfilePage";
import { ModerationJobControlPage } from "./pages/moderation/job_control/ModerationJobControlPage";
import WeightPage from "./pages/weight/WeightPage";
import ExplorePage from "./pages/explore/ExplorePage";
@@ -140,6 +139,8 @@ import CreateBetaKeyPage from "./pages/beta_key/CreateBetaKeyPage";
import RedeemBetaKeyPage from "./pages/beta_key/RedeemBetaKeyPage";
import RedeemSuccessPage from "./pages/beta_key/RedeemSuccessPage";
import BetaKeysListPage from "./pages/beta_key/BetaKeysListPage";
import { ProfilePageV3 } from "./pages/profile/profile_view/ProfilePageV3";
import ProfileSidePanel from "components/layout/ProfileSidePanel/ProfileSidePanel";
interface Props {
sessionWrapper: SessionWrapper;
@@ -262,12 +263,11 @@ class PageContainer extends React.Component<
this.props.querySessionSubscriptionsAction
}
/>
<SideNav
<ProfileSidePanel
sessionWrapper={this.props.sessionWrapper}
logoutHandler={this.logout}
querySessionCallback={this.props.querySessionAction}
querySessionSubscriptionsCallback={
this.props.querySessionSubscriptionsAction
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
@@ -343,14 +343,23 @@ class PageContainer extends React.Component<
<ProfileBanFc sessionWrapper={this.props.sessionWrapper} />
</Route>
{/* New Profile Page */}
<Route path="/profile/:username">
{/* Old Profile Page */}
{/* <Route path="/profile/:username">
<NewProfilePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route> */}
<Route path="/profile/:username">
<ProfilePageV3
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/signup">
@@ -152,13 +152,13 @@ export default function MediaPage() {
let pageTitle;
switch (mediaFile?.media_type) {
case MediaFileType.Audio:
case MediaFileType.Audio:
pageTitle = mediaFile?.maybe_title || "Audio File";
break;
case MediaFileType.Video:
case MediaFileType.Video:
pageTitle = mediaFile?.maybe_title || "Video File";
break;
case MediaFileType.Image:
case MediaFileType.Image:
pageTitle = mediaFile?.maybe_title || "Image File";
break;
default:
@@ -231,7 +231,9 @@ export default function MediaPage() {
{prompt?.used_face_detailer && (
<>
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
<h6 className="fw-semibold mb-0 flex-grow-1">Used Face Detailer</h6>
<h6 className="fw-semibold mb-0 flex-grow-1">
Used Face Detailer
</h6>
</div>
<div className="panel-inner p-2 rounded">
<p className="fs-7">
@@ -246,26 +248,28 @@ export default function MediaPage() {
<h6 className="fw-semibold mb-0 flex-grow-1">Used Upscaler</h6>
</div>
<div className="panel-inner p-2 rounded">
<p className="fs-7">
{prompt?.used_upscaler ? "Yes" : "No"}
</p>
</div>
</>
)}
{isModerator && mediaFile?.maybe_moderator_fields?.maybe_style_transfer_source_media_file_token && (
<>
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
<h6 className="fw-semibold mb-0 flex-grow-1">Source Video</h6>
</div>
<div className="panel-inner p-2 rounded">
<p className="fs-7">
<Link
to={`/media/${mediaFile?.maybe_moderator_fields?.maybe_style_transfer_source_media_file_token}`}
>Link to source (staff only)</Link>
</p>
<p className="fs-7">{prompt?.used_upscaler ? "Yes" : "No"}</p>
</div>
</>
)}
{isModerator &&
mediaFile?.maybe_moderator_fields
?.maybe_style_transfer_source_media_file_token && (
<>
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
<h6 className="fw-semibold mb-0 flex-grow-1">Source Video</h6>
</div>
<div className="panel-inner p-2 rounded">
<p className="fs-7">
<Link
to={`/media/${mediaFile?.maybe_moderator_fields?.maybe_style_transfer_source_media_file_token}`}
>
Link to source (staff only)
</Link>
</p>
</div>
</>
)}
</Panel>
)}
</>
@@ -977,10 +981,13 @@ export default function MediaPage() {
{mediaFile?.maybe_scene_source_media_file_token && (
<>
<div className="d-flex gap-2">
<a
<a
className="btn btn-success w-100"
href={`https://studio.storyteller.ai/${mediaFile?.maybe_scene_source_media_file_token}`}
><FontAwesomeIcon icon={faCube} /> &nbsp; Open in Studio Editor</a>
>
<FontAwesomeIcon icon={faCube} /> &nbsp; Open in Studio
Editor
</a>
</div>
</>
)}
@@ -0,0 +1,171 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useCallback, useEffect, useState } from "react";
import { Link, 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 {
GetUserByUsername,
GetUserByUsernameIsErr,
GetUserByUsernameIsOk,
User,
UserLookupError,
} from "@storyteller/components/src/api/user/GetUserByUsername";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import {
faBookmark,
faComment,
faFilm,
faLayerGroup,
faPhotoFilmMusic,
faVolume,
} 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";
import MediaTab from "./tabs/MediaTab";
import WeightsTab from "./tabs/WeightsTab";
import BookmarksTab from "./tabs/BookmarksTab";
import { CommentComponent } from "v2/view/_common/comments/CommentComponent";
import VideosTab from "./tabs/VideosTab";
import AudiosTab from "./tabs/AudiosTab";
import UserProfileInfo from "components/layout/ProfileSidePanel/UserProfileInfo";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
function ProfilePageV3(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}/videos`} />;
}
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 />;
}
const tabs = [
{
to: `/profile/${username}/videos`,
label: "Videos",
content: <VideosTab username={username} />,
icon: faFilm,
padding: true,
},
{
to: `/profile/${username}/audios`,
label: "Audios",
content: <AudiosTab username={username} />,
icon: faVolume,
padding: true,
},
{
to: `/profile/${username}/media`,
label: "All Media",
content: <MediaTab username={username} />,
icon: faPhotoFilmMusic,
padding: true,
},
{
to: `/profile/${username}/weights`,
label: "Weights",
content: <WeightsTab username={username} />,
icon: faLayerGroup,
padding: true,
},
{
to: `/profile/${username}/bookmarks`,
label: "Bookmarks",
content: <BookmarksTab username={username} />,
icon: faBookmark,
padding: true,
},
{
to: `/profile/${username}/comments`,
label: "Comments",
content: (
<CommentComponent entityType="user" entityToken={userData.user_token} />
),
icon: faComment,
padding: true,
},
];
return (
<Container type="panel-full">
<div className="d-flex gap-4 mt-3">
<div className="flex-grow-1">
<Panel clear={true} className="d-lg-none mb-4">
<div className="profile-sidebar-panel py-3">
<UserProfileInfo
sessionWrapper={props.sessionWrapper}
sessionSubscriptionsWrapper={props.sessionSubscriptionsWrapper}
/>
</div>
</Panel>
<Panel clear={true}>
<h1 className="fw-bold mb-3">Creations</h1>
</Panel>
<Panel>
<Tabs tabs={tabs} />
</Panel>
</div>
</div>
</Container>
);
}
export { ProfilePageV3 };
@@ -0,0 +1,117 @@
import React, { useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import MediaCards from "components/common/Card/MediaCards";
import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContext";
import SkeletonCard from "components/common/Card/SkeletonCard";
import Pagination from "components/common/Pagination";
import { GetMediaByUser } from "@storyteller/components/src/api/media_files/GetMediaByUser";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { useBookmarks, useListContent, useRatings } from "hooks";
import prepFilter from "resources/prepFilter";
export default function AudiosTab({ username }: { username: string }) {
const { pathname: source, search } = useLocation();
const urlQueries = new URLSearchParams(search);
const bookmarks = useBookmarks();
const ratings = useRatings();
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [mediaType, mediaTypeSet] = useState(
urlQueries.get("filter_media_type") || "audio"
);
const [list, listSet] = useState<MediaFile[]>([]);
const media = useListContent({
addQueries: {
page_size: urlQueries.get("page_size") || "24",
...prepFilter(
mediaType,
"filter_media_type",
mediaType === "3dFile" ? "bvh,glb,gltf" : ""
),
},
addSetters: { mediaTypeSet },
// debug: "profile media",
fetcher: GetMediaByUser,
list,
listSet,
onInputChange: () => setShowMasonryGrid(false),
onSuccess: res => {
bookmarks.gather({ res, key: "token" });
ratings.gather({ res, key: "token" });
setShowMasonryGrid(true);
},
requestList: true,
urlParam: username.toLowerCase(),
});
const handlePageClick = (selectedItem: { selected: number }) => {
media.pageChange(selectedItem.selected);
};
const paginationProps = {
onPageChange: handlePageClick,
pageCount: media.pageCount,
currentPage: media.page,
};
return (
<>
<div className="d-flex flex-wrap gap-3 mb-3">
<Pagination {...paginationProps} />
</div>
<AudioPlayerProvider>
{media.isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 12 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<>
{showMasonryGrid && (
<>
{media.list.length === 0 && media.status === 3 ? (
<div className="text-center mt-4 opacity-75">
No media created yet.
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{media.list.map((data: MediaFile, key: number) => {
let props = {
bookmarks,
data,
source,
ratings,
type: "media",
};
return (
<div
{...{
className:
"col-12 col-sm-6 col-lg-6 col-xl-4 col-xxl-3 grid-item",
key,
}}
>
<MediaCards {...{ type: data.media_type, props }} />
</div>
);
})}
</MasonryGrid>
)}
</>
)}
</>
)}
</AudioPlayerProvider>
<div className="d-flex justify-content-end mt-4">
<Pagination {...paginationProps} />
</div>
</>
);
}
@@ -0,0 +1,122 @@
import React, { useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import MediaCards from "components/common/Card/MediaCards";
import { TempSelect } from "components/common";
import {
faArrowDownWideShort,
faFilter,
} from "@fortawesome/pro-solid-svg-icons";
import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContext";
import SkeletonCard from "components/common/Card/SkeletonCard";
import Pagination from "components/common/Pagination";
import { GetMediaByUser } from "@storyteller/components/src/api/media_files/GetMediaByUser";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import { useBookmarks, useListContent, useRatings } from "hooks";
import prepFilter from "resources/prepFilter";
export default function VideosTab({ username }: { username: string }) {
const { pathname: source, search } = useLocation();
const urlQueries = new URLSearchParams(search);
const bookmarks = useBookmarks();
const ratings = useRatings();
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [showMasonryGrid, setShowMasonryGrid] = useState(true);
const [mediaType, mediaTypeSet] = useState(
urlQueries.get("filter_media_type") || "video"
);
const [list, listSet] = useState<MediaFile[]>([]);
const media = useListContent({
addQueries: {
page_size: urlQueries.get("page_size") || "24",
...prepFilter(
mediaType,
"filter_media_type",
mediaType === "3dFile" ? "bvh,glb,gltf" : ""
),
},
addSetters: { mediaTypeSet },
// debug: "profile media",
fetcher: GetMediaByUser,
list,
listSet,
onInputChange: () => setShowMasonryGrid(false),
onSuccess: res => {
bookmarks.gather({ res, key: "token" });
ratings.gather({ res, key: "token" });
setShowMasonryGrid(true);
},
requestList: true,
urlParam: username.toLowerCase(),
});
const handlePageClick = (selectedItem: { selected: number }) => {
media.pageChange(selectedItem.selected);
};
const paginationProps = {
onPageChange: handlePageClick,
pageCount: media.pageCount,
currentPage: media.page,
};
return (
<>
<div className="d-flex flex-wrap gap-3 mb-3">
<Pagination {...paginationProps} />
</div>
<AudioPlayerProvider>
{media.isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 12 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<>
{showMasonryGrid && (
<>
{media.list.length === 0 && media.status === 3 ? (
<div className="text-center mt-4 opacity-75">
No media created yet.
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{media.list.map((data: MediaFile, key: number) => {
let props = {
bookmarks,
data,
source,
ratings,
type: "media",
};
return (
<div
{...{
className:
"col-12 col-sm-6 col-lg-6 col-xl-4 grid-item",
key,
}}
>
<MediaCards {...{ type: data.media_type, props }} />
</div>
);
})}
</MasonryGrid>
)}
</>
)}
</>
)}
</AudioPlayerProvider>
<div className="d-flex justify-content-end mt-4">
<Pagination {...paginationProps} />
</div>
</>
);
}