mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
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:
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;
|
||||
|
||||
@@ -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} /> Open in Studio Editor</a>
|
||||
>
|
||||
<FontAwesomeIcon icon={faCube} /> 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user