Merge pull request #400 from storytold/revert-397-revert-386-bflat-fy-new-profile

Revert "Revert "new profile page tabs""
This commit is contained in:
BFlat authored and GitHub committed 2023-12-02 17:44:56 +07:00
commit 1e9a22bdaa
47 files changed
+2568 -68

No files matched your search

+9
View File
@@ -15,6 +15,7 @@
"@fortawesome/pro-regular-svg-icons": "^6.2.1",
"@fortawesome/pro-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@react-spring/types": "^9.7.3",
"@react-spring/web": "^9.7.3",
"@storyteller/components": "0.1.0",
"@testing-library/jest-dom": "^4.2.4",
@@ -26,8 +27,11 @@
"@types/google.analytics": "^0.0.42",
"@types/gtag.js": "^0.0.12",
"@types/howler": "^2.1.2",
"@types/imagesloaded": "^4.1.6",
"@types/infinite-scroll": "^4.0.3",
"@types/jest": "^24.9.1",
"@types/js-cookie": "^3.0.1",
"@types/masonry-layout": "^4.2.7",
"@types/node": "^12.12.47",
"@types/react": "^16.9.36",
"@types/react-dom": "^16.9.8",
@@ -42,7 +46,10 @@
"i18next": "^21.6.11",
"i18next-browser-languagedetector": "^7.1.0",
"i18next-http-backend": "^2.2.1",
"imagesloaded": "^5.0.0",
"infinite-scroll": "^4.0.1",
"js-cookie": "^3.0.1",
"masonry-layout": "^4.2.2",
"posthog-js": "^1.73.1",
"pretty-num": "^0.5.2",
"query-string": "^8.1.0",
@@ -52,6 +59,7 @@
"react-dom": "^16.13.1",
"react-drag-drop-files": "^2.3.9",
"react-i18next": "^11.15.4",
"react-infinite-scroll-component": "^6.1.0",
"react-range": "^1.8.14",
"react-reveal": "^1.2.2",
"react-router-dom": "^5.2.0",
@@ -62,6 +70,7 @@
"react-wavesurfer.js": "^0.0.8",
"sass": "^1.57.1",
"typescript": "^3.7.5",
"universal-cookie": "^6.1.1",
"uuid": "^8.3.2",
"wavesurfer.js": "^6.4.0",
"zustand": "^4.3.9"
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

@@ -0,0 +1,3 @@
wave {
z-index: 2;
}
@@ -0,0 +1,98 @@
import React, { useEffect, useState, useRef } from "react";
import WaveSurfer from "wavesurfer.js";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { faPlay, faPause } from "@fortawesome/pro-solid-svg-icons";
import Button from "components/common/Button";
import "./AudioPlayer.scss";
import { useAudioPlayerContext } from "./AudioPlayerContext";
import useOnScreen from "hooks/useOnScreen";
interface AudioPlayerProps {
src: string;
id: string;
}
export default function AudioPlayer({ src, id }: AudioPlayerProps) {
const { currentPlayingId, setCurrentPlayingId } = useAudioPlayerContext();
const [isPlaying, setIsPlaying] = useState(false);
const waveformRef = useRef<HTMLDivElement | null>(null);
const waveSurferRef = useRef<WaveSurfer | null>(null);
const isPlayerOnScreen = useOnScreen(waveformRef); //This stops the audio from playing when the player is not on screen
useEffect(() => {
const audioLink = new BucketConfig().getGcsUrl(src);
if (!waveSurferRef.current && waveformRef.current) {
const wavesurferInstance = WaveSurfer.create({
container: waveformRef.current,
height: 38,
responsive: true,
waveColor: "#cbcbcb",
progressColor: "#fc8481",
cursorColor: "#fc6b68",
cursorWidth: 2,
normalize: true,
});
waveSurferRef.current = wavesurferInstance;
waveSurferRef.current.load(audioLink);
waveSurferRef.current.on("pause", () => {
setIsPlaying(false);
});
waveSurferRef.current.on("play", () => {
setIsPlaying(true);
setCurrentPlayingId(id);
});
waveSurferRef.current.on("finish", () => {
waveSurferRef.current?.pause();
});
}
return () => {
// Cleanup when component unmounts
if (waveSurferRef.current) {
waveSurferRef.current.destroy();
}
};
}, [src, setCurrentPlayingId, id]);
useEffect(() => {
if (currentPlayingId !== id && isPlaying) {
setIsPlaying(false);
waveSurferRef.current?.pause();
}
}, [currentPlayingId, id, isPlaying]);
useEffect(() => {
if (!isPlayerOnScreen && isPlaying) {
waveSurferRef.current?.pause();
}
}, [isPlayerOnScreen, isPlaying]);
const togglePlayPause = () => {
if (waveSurferRef.current) {
if (!isPlaying) {
setCurrentPlayingId(id);
}
waveSurferRef.current.playPause();
}
};
const playButtonIcon = isPlaying ? faPause : faPlay;
return (
<div className="d-flex gap-3 align-items-center">
<Button
square={true}
icon={playButtonIcon}
onClick={togglePlayPause}
small={true}
/>
<div ref={waveformRef} className="w-100" />
</div>
);
}
@@ -0,0 +1,26 @@
import React, { createContext, useContext, useState } from "react";
interface AudioPlayerContextProps {
children: React.ReactNode;
}
const AudioPlayerContext = createContext({
currentPlayingId: null,
setCurrentPlayingId: (id: any) => {},
});
export const useAudioPlayerContext = () => useContext(AudioPlayerContext);
export default function AudioPlayerProvider({
children,
}: AudioPlayerContextProps) {
const [currentPlayingId, setCurrentPlayingId] = useState(null);
return (
<AudioPlayerContext.Provider
value={{ currentPlayingId, setCurrentPlayingId }}
>
{children}
</AudioPlayerContext.Provider>
);
}
@@ -0,0 +1 @@
export { default } from "./AudioPlayer";
@@ -0,0 +1,27 @@
@import "scss/colours.scss";
.fy-badge {
font-weight: 600;
border-radius: 0.35rem;
font-size: 13px;
padding: 4px 8px;
transition: 0.2s;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
height: 100%;
margin-bottom: 10px;
&.shadow {
box-shadow: 0 0 12px rgba($bg, 0.4);
}
}
@each $color, $value in $badge-colors {
.badge-#{$color} {
background-color: $value;
color: contrast($value) !important;
}
}
@@ -0,0 +1,22 @@
import React from "react";
import "./Badge.scss";
interface BadgeProps {
label: string;
color: string;
overlay?: boolean;
className?: string;
}
export default function Badge({
label,
color,
overlay = false,
className = "",
}: BadgeProps) {
const badgeClass = `fy-badge badge-${color} ${
overlay ? "shadow" : ""
} mb-0 ${className}`.trim();
return <span className={badgeClass}>{label}</span>;
}
@@ -0,0 +1 @@
export { default } from "./Badge";
@@ -0,0 +1,101 @@
import React from "react";
import { useHistory } from "react-router-dom";
import Card from "../Card";
import AudioPlayer from "components/common/AudioPlayer";
import useTimeAgo from "hooks/useTimeAgo";
import Badge from "components/common/Badge";
import LikeButton from "components/common/LikeButton";
import FavoriteButton from "components/common/FavoriteButton";
import Button from "components/common/Button";
interface AudioCardProps {
data: any;
type: "media" | "weights";
}
export default function AudioCard({ data, type }: AudioCardProps) {
const history = useHistory();
const handleCardClick = () => {
if (type === "media") {
history.push(`/media/${data.token}`);
} else if (type === "weights") {
history.push(`/weights/${data.token}`);
}
};
const handleInnerClick = (event: any) => {
event.stopPropagation();
};
const handleUseModel = () => {
if (type === "weights") {
history.push(`/weights/${data.token}`);
}
};
const timeAgo = useTimeAgo(data.created_at);
const handleLike = async (data: any) => {
console.log(`The item is now ${data.isLiked ? "liked" : "not liked"}.`);
};
return (
<Card padding={true} onClick={handleCardClick}>
{type === "media" && (
<>
<div className="mb-3">
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="Audio" color="teal" />
</div>
</div>
<h6 className="fw-semibold text-white mb-1 mt-3">
{data.weight_name}
</h6>
<p className="fs-7 opacity-75">{timeAgo}</p>
</div>
<div onClick={handleInnerClick}>
<AudioPlayer src={data.public_bucket_path} id={data.token} />
</div>
<div className="mt-3" onClick={handleInnerClick}>
<LikeButton onToggle={handleLike} likeCount={data.likes} />
</div>
</>
)}
{type === "weights" && (
<>
<div>
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="RVC" color="orange" />
</div>
<div onClick={handleInnerClick}>
<FavoriteButton
onToggle={handleLike}
favoriteCount={data.likes}
/>
</div>
</div>
<div className="d-flex align-items-center mt-3">
<div className="flex-grow-1">
<h6 className="fw-semibold text-white mb-1">
{data.weight_name}
</h6>
<p className="fs-7 opacity-75">{timeAgo}</p>
</div>
<div onClick={handleInnerClick}>
<Button label="Use" small={true} onClick={handleUseModel} />
</div>
</div>
</div>
</>
)}
</Card>
);
}
@@ -0,0 +1 @@
export { default } from "./AudioCard";
@@ -0,0 +1,85 @@
@import "scss/colours.scss";
.card {
background-color: $panel-inner;
border: 1px solid $panel-inner-border;
border-radius: 0.5rem;
overflow: hidden;
transition: 0.2s;
position: relative;
.card-img {
width: 100%;
transition: 0.4s ease;
object-fit: cover;
object-position: center;
min-height: 300px;
max-height: 500px;
}
.card-video {
width: 100%;
transition: 0.4s ease;
object-fit: cover;
object-position: center;
min-height: 240px;
max-height: 240px;
}
.card-img-overlay {
position: absolute;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
.card-video-play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 50px;
color: rgba($white, 0.5);
transition: 0.2s;
}
.card-img-gradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40%;
background: linear-gradient(180deg, transparent, darken($bg, 5%));
}
.card-img-overlay-text {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 1rem;
color: $white;
text-shadow: 0 0 10px black;
}
&.card-clickable {
&:hover {
background-color: lighten($panel-inner, 2%);
cursor: pointer;
border: 1px solid lighten($panel-inner-border, 15%);
}
&:hover > .card-img {
transform: scale(1.05);
}
&:hover > .card-video {
transform: scale(1.05);
}
&:hover .card-video-play {
color: rgba($white, 0.8);
}
}
}
@@ -0,0 +1,21 @@
import React from "react";
import "./Card.scss";
interface CardProps {
padding?: boolean;
children?: React.ReactNode;
onClick?: () => void;
}
export default function Card({ padding, children, onClick }: CardProps) {
return (
<div
className={`card ${padding ? "p-3" : ""} ${
onClick ? "card-clickable" : ""
}`}
onClick={onClick}
>
{children}
</div>
);
}
@@ -0,0 +1,111 @@
import React from "react";
import { useHistory } from "react-router-dom";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import Badge from "components/common/Badge";
import LikeButton from "components/common/LikeButton";
import Button from "components/common/Button";
import FavoriteButton from "components/common/FavoriteButton";
interface ImageCardProps {
data: any;
type: "media" | "weights";
}
export default function ImageCard({ data, type }: ImageCardProps) {
const history = useHistory();
const handleCardClick = () => {
if (type === "media") {
history.push(`/media/${data.token}`);
} else if (type === "weights") {
history.push(`/weights/${data.token}`);
}
};
const handleInnerClick = (event: any) => {
event.stopPropagation();
};
const timeAgo = useTimeAgo(data.created_at);
const handleLike = async (data: any) => {
console.log(`The item is now ${data.isLiked ? "liked" : "not liked"}.`);
};
return (
<Card padding={false} onClick={handleCardClick}>
{type === "media" && (
<>
<img
src={data.public_bucket_path}
alt={data.weight_name}
className="card-img"
/>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="Image" color="ultramarine" overlay={true} />
</div>
</div>
<div className="card-img-overlay-text">
<div>
<p className="fs-7 opacity-75 mb-0">{timeAgo}</p>
</div>
<div onClick={handleInnerClick} className="mt-2">
<LikeButton
onToggle={handleLike}
likeCount={data.likes}
overlay={true}
/>
</div>
</div>
</div>
</>
)}
{type === "weights" && (
<>
<img
src={data.public_bucket_path}
alt={data.weight_name}
className="card-img"
/>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="LORA" color="pink" overlay={true} />
</div>
<div onClick={handleInnerClick}>
<FavoriteButton
onToggle={handleLike}
favoriteCount={data.likes}
overlay={true}
/>
</div>
</div>
<div className="card-img-overlay-text">
<div className="d-flex align-items-center mt-3">
<div className="flex-grow-1">
<h6 className="fw-semibold text-white mb-1">
{data.weight_name}
</h6>
<p className="fs-7 opacity-75">{timeAgo}</p>
</div>
<div onClick={handleInnerClick}>
<Button label="Use" small={true} />
</div>
</div>
</div>
</div>
</>
)}
</Card>
);
}
@@ -0,0 +1 @@
export { default } from "./ImageCard";
@@ -0,0 +1,37 @@
import React from "react";
import Card from "../Card";
import Skeleton from "components/common/Skeleton";
interface SkeletonCardProps {}
export default function SkeletonCard(props: SkeletonCardProps) {
return (
<div className="col-12 col-sm-6 col-xl-4">
<Card padding={true}>
<div className="mb-3">
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Skeleton type="short" />
</div>
<div>
<Skeleton type="short" />
</div>
</div>
<h6 className="fw-semibold text-white mb-1 mt-3">
<Skeleton type="medium" />
</h6>
<p className="fs-7 opacity-75">
<Skeleton type="short" />
</p>
</div>
<div>
<h2 className="mb-0">
<Skeleton type="full" />
</h2>
</div>
</Card>
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./SkeletonCard";
@@ -0,0 +1,105 @@
import React from "react";
import { useHistory } from "react-router-dom";
import Card from "../Card";
import useTimeAgo from "hooks/useTimeAgo";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlayCircle } from "@fortawesome/pro-solid-svg-icons";
import Badge from "components/common/Badge";
import LikeButton from "components/common/LikeButton";
interface VideoCardProps {
data: any;
type: "media" | "weights";
}
export default function VideoCard({ data, type }: VideoCardProps) {
const history = useHistory();
const handleCardClick = () => {
history.push(`/media/${data.token}`);
};
const handleInnerClick = (event: any) => {
event.stopPropagation();
};
const timeAgo = useTimeAgo(data.created_at);
const handleLike = async (data: any) => {
console.log(`The item is now ${data.isLiked ? "liked" : "not liked"}.`);
};
return (
<Card padding={false} onClick={handleCardClick}>
{type === "media" && (
<>
<img
src={data.public_bucket_path}
alt={data.weight_name}
className="card-video"
/>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="Video" color="purple" overlay={true} />
</div>
</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>
<div className="mt-2" onClick={handleInnerClick}>
<LikeButton
onToggle={handleLike}
likeCount={data.likes}
overlay={true}
/>
</div>
</div>
</div>
</div>
</>
)}
{type === "weights" && (
<>
<img
src={data.public_bucket_path}
alt={data.weight_name}
className="card-video"
/>
<div className="card-img-overlay">
<div className="card-img-gradient" />
<div className="d-flex align-items-center">
<div className="flex-grow-1">
<Badge label="Video" color="purple" overlay={true} />
</div>
</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>
<div className="mt-2" onClick={handleInnerClick}>
<LikeButton
onToggle={handleLike}
likeCount={data.likes}
overlay={true}
/>
</div>
</div>
</div>
</div>
</>
)}
</Card>
);
}
@@ -0,0 +1 @@
export { default } from "./VideoCard";
@@ -0,0 +1 @@
export { default } from "./Card";
@@ -0,0 +1,83 @@
@import "scss/colours.scss";
.favorite-button {
display: flex;
align-items: center;
background-color: rgba(lighten($secondary, 5%), 0.9);
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
font-size: 16px;
transition: 0.2s;
padding: 3px 8px;
border-radius: 0.5rem;
&.shadow {
box-shadow: 0 0 12px rgba($bg, 0.4) !important;
&:hover {
box-shadow: 0 0 12px rgba($bg, 0.4) !important;
}
}
&:hover {
background: rgba(lighten($secondary, 10%), 0.9);
}
&:hover .favorite-number {
opacity: 1;
}
&:hover .icon-default {
color: map-get($badge-colors, "yellow");
opacity: 1;
}
&.toggled {
background-color: darken(rgba(map-get($badge-colors, "yellow"), 0.9), 32%);
&:hover {
background-color: darken(
rgba(map-get($badge-colors, "yellow"), 0.9),
25%
);
}
}
}
.favorite-button .icon-default {
color: $white;
opacity: 0.7;
transition: 0.2s;
}
.favorite-button .icon-toggled {
color: map-get($badge-colors, "yellow");
animation: pulse 0.3s ease;
}
.favorite-button .icon-toggled:hover {
color: map-get($badge-colors, "yellow");
}
.favorite-number {
font-size: 14px;
font-weight: 400 !important;
opacity: 0.8;
transition: 0.2s;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBookmark } from "@fortawesome/pro-solid-svg-icons";
import { faBookmark as faBookmarkOutline } from "@fortawesome/pro-regular-svg-icons";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
import "./FavoriteButton.scss";
interface FavoriteButtonProps {
initialToggled?: boolean;
onToggle: (toggled: boolean) => Promise<void>;
favoriteCount?: number;
overlay?: boolean;
}
export default function FavoriteButton({
initialToggled = false,
onToggle,
favoriteCount,
overlay,
}: FavoriteButtonProps) {
const [isToggled, setIsToggled] = useState(initialToggled);
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
try {
await onToggle(!isToggled);
setIsToggled(!isToggled);
} catch (error) {
console.error("Error calling API", error);
} finally {
setIsLoading(false);
}
};
const buttonClass = isToggled ? "favorite-button toggled" : "favorite-button";
const buttonShadow = overlay ? "shadow" : "";
const iconClass = isToggled ? "icon-toggled" : "icon-default";
const toolTip = isToggled ? "Unfavorite" : "Favorite";
return (
<div className="d-flex gap-2" onClick={handleClick}>
<Tippy
theme="fakeyou"
content={toolTip}
hideOnClick={false}
trigger="mouseenter"
delay={[500, 0]}
offset={[0, 12]}
>
<button
onClick={handleClick}
disabled={isLoading}
className={`${buttonClass} ${buttonShadow}`}
>
<FontAwesomeIcon
icon={isToggled ? faBookmark : faBookmarkOutline}
className={`${iconClass} me-2`}
/>
{favoriteCount && <p className="favorite-number">{favoriteCount}</p>}
</button>
</Tippy>
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./FavoriteButton";
@@ -0,0 +1,80 @@
@import "scss/colours.scss";
.like-button {
display: flex;
align-items: center;
background-color: rgba(lighten($secondary, 5%), 0.9);
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
font-size: 16px;
transition: 0.2s;
padding: 3px 8px;
border-radius: 0.5rem;
&.shadow {
box-shadow: 0 0 12px rgba($bg, 0.4) !important;
&:hover {
box-shadow: 0 0 12px rgba($bg, 0.4) !important;
}
}
&:hover {
background: rgba(lighten($secondary, 10%), 0.9);
}
&:hover .like-number {
opacity: 1;
}
&:hover .icon-default {
color: $primary;
opacity: 1;
}
&.toggled {
background-color: rgba(#593a48, 0.9);
&:hover {
background-color: darken(rgba(#593a48, 0.9), 2%);
}
}
}
.like-button .icon-default {
color: $white;
opacity: 0.7;
transition: 0.2s;
}
.like-button .icon-toggled {
color: $primary;
animation: pulse 0.3s ease;
}
.like-button .icon-toggled:hover {
color: $primary;
}
.like-number {
font-size: 14px;
font-weight: 400 !important;
opacity: 0.8;
transition: 0.2s;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
@@ -0,0 +1,67 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHeart } from "@fortawesome/pro-solid-svg-icons";
import { faHeart as faHeartOutline } from "@fortawesome/pro-regular-svg-icons";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
import "./LikeButton.scss";
interface LikeButtonProps {
initialToggled?: boolean;
onToggle: (toggled: boolean) => Promise<void>;
likeCount?: number;
overlay?: boolean;
}
export default function LikeButton({
initialToggled = false,
onToggle,
likeCount,
overlay,
}: LikeButtonProps) {
const [isToggled, setIsToggled] = useState(initialToggled);
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
try {
await onToggle(!isToggled);
setIsToggled(!isToggled);
} catch (error) {
console.error("Error calling API", error);
} finally {
setIsLoading(false);
}
};
const buttonClass = isToggled ? "like-button toggled" : "like-button";
const buttonShadow = overlay ? "shadow" : "";
const iconClass = isToggled ? "icon-toggled" : "icon-default";
const toolTip = isToggled ? "Unlike" : "Like";
return (
<div className="d-flex gap-2" onClick={handleClick}>
<Tippy
theme="fakeyou"
content={toolTip}
hideOnClick={false}
trigger="mouseenter"
delay={[500, 0]}
offset={[0, 12]}
placement="bottom"
>
<button
onClick={handleClick}
disabled={isLoading}
className={`${buttonClass} ${buttonShadow}`}
>
<FontAwesomeIcon
icon={isToggled ? faHeart : faHeartOutline}
className={`${iconClass} me-2`}
/>
{likeCount && <p className="like-number">{likeCount}</p>}
</button>
</Tippy>
</div>
);
}
@@ -0,0 +1 @@
export { default } from "./LikeButton";
@@ -0,0 +1,56 @@
import Masonry from "masonry-layout"; //Refer to: https://github.com/desandro/masonry
import imagesLoaded from "imagesloaded"; //Refer to: https://github.com/desandro/imagesloaded
import React, { useEffect } from "react";
interface MasonryGridProps {
children: React.ReactNode;
onLayoutComplete?: () => void;
gridRef: React.RefObject<HTMLDivElement>;
}
export default function MasonryGrid({
children,
onLayoutComplete,
gridRef,
}: MasonryGridProps) {
useEffect(() => {
let masonryInstance: Masonry | null = null;
const updateLayout = () => {
//Needs this type guard to check that it is not undefined.
if (masonryInstance && typeof masonryInstance.layout === "function") {
masonryInstance.layout();
}
};
if (gridRef.current) {
masonryInstance = new Masonry(gridRef.current, {
itemSelector: ".grid-item",
percentPosition: true,
});
//Needs this or images will overflow masonry card. It checks if image is loaded then after it is loaded, it resizes the card.
imagesLoaded(gridRef.current, function () {
updateLayout();
onLayoutComplete?.();
});
}
//Clean up
return () => {
if (masonryInstance && typeof masonryInstance.destroy === "function") {
masonryInstance.destroy();
}
};
}, [gridRef, onLayoutComplete]);
return (
<div
ref={gridRef}
className="row gy-3 gx-3"
data-masonry='{"percentPosition": true}'
>
{children}
</div>
);
}
@@ -19,7 +19,7 @@ export default function Panel({
}: PanelProps) {
return (
<div
className={`panel ${clear && "panel-clear"} ${
className={`${clear ? "panel-clear" : "panel"} ${
padding ? "p-3 py-4 p-md-4" : ""
}
${mb ? "mb-4" : ""} ${mt ? "mt-4" : ""} ${className || ""}`}
@@ -0,0 +1,35 @@
@import "scss/colours.scss";
.nav-tabs {
border-bottom: 2px solid #ffffff15;
.nav-link {
border: none;
padding: 1rem 1.4rem;
border-bottom: 4px solid transparent;
color: rgba($white, 0.5);
font-weight: 500;
white-space: nowrap;
transition: 0.2s;
&:focus {
border-bottom: 4px solid transparent;
}
&:hover {
color: rgba($white, 0.75);
border-bottom: 4px solid rgba($white, 0.75);
}
&:focus:hover {
color: rgba($white, 0.75);
border-bottom: 4px solid rgba($white, 0.75);
}
&.active {
color: $white !important;
background-color: transparent;
border-bottom: 4px solid $primary !important;
}
}
}
@@ -0,0 +1,101 @@
import React, { useEffect, useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useSpring, a } from "@react-spring/web";
import "./Tabs.scss";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface TabProps {
to: string;
label: string;
content?: React.ReactNode;
icon?: IconDefinition;
}
interface TabsProps {
tabs: TabProps[];
}
interface TabContentProps {
children: React.ReactNode;
}
function Tab({ to, label, icon, onClick }: TabProps & { onClick: () => void }) {
return (
<li className="nav-item">
<NavLink
to={to}
className="nav-link fs-6 px-3 px-lg-4"
activeClassName="active"
onClick={onClick}
>
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
{label}
</NavLink>
</li>
);
}
function TabContent({ children }: TabContentProps) {
return <div className="tab-content">{children}</div>;
}
function Tabs({ tabs }: TabsProps) {
const location = useLocation();
const currentPath = location.pathname;
const initialTab = tabs.find((tab) => tab.to === currentPath) || tabs[0];
const [activeTab, setActiveTab] = useState(initialTab.to);
const [fade, setFade] = useSpring(() => ({
opacity: 1,
from: { opacity: 0 },
config: { duration: 50 },
}));
useEffect(() => {
setFade({ opacity: 1 });
}, [activeTab, setFade]);
useEffect(() => {
setActiveTab(currentPath);
}, [currentPath]);
const handleTabClick = (tabTo: string) => {
if (activeTab === tabTo) {
return;
}
setFade({ opacity: 0 });
setTimeout(() => setActiveTab(tabTo), 50);
};
return (
<nav>
<ul className="nav nav-tabs">
{tabs.map((tab) => (
<Tab
key={tab.to}
to={tab.to}
label={tab.label}
onClick={() => handleTabClick(tab.to)}
icon={tab.icon}
/>
))}
</ul>
<TabContent>
{tabs.map((tab) => (
<a.div
key={tab.to}
style={fade}
className={`tab-pane fade ${
activeTab === tab.to ? "show active" : ""
}`}
>
{tab.content}
</a.div>
))}
</TabContent>
</nav>
);
}
export default Tabs;
@@ -0,0 +1 @@
export { default } from "./Tabs";
@@ -0,0 +1 @@
export { default } from "./useOnScreen";
@@ -0,0 +1,35 @@
import { useEffect, useState, RefObject } from "react";
const useOnScreen = (
ref: RefObject<HTMLElement>,
rootMargin: string = "0px"
) => {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const currentRef = ref.current;
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
},
{
rootMargin,
}
);
if (currentRef) {
observer.observe(currentRef);
}
return () => {
if (currentRef) {
observer.unobserve(currentRef);
}
};
}, [ref, rootMargin]);
return isIntersecting;
};
export default useOnScreen;
+19 -3
View File
@@ -2,7 +2,6 @@
$primary: #e66462;
$secondary: #39394c;
$bg: #1a1a27;
$panel: #242433;
$input: #39394c;
$input-placeholder: #aeaeb3;
$danger: #8f4951;
@@ -12,8 +11,9 @@ $success: #40ad48;
$warning: darken(#f7941c, 5%);
$accent: #80cfd3;
$workflow: $primary;
// Light mode colours
$panel: #242433;
$panel-inner: lighten($panel, 4%);
$panel-inner-border: lighten($panel, 8%);
$primary-lm: #000;
@@ -59,3 +59,19 @@ $danger-dark: darken($danger, 10%);
$accent-light: desaturate(lighten($accent, 20%), 15%);
$accent-dark: saturate(darken($accent, 15%), 10%);
$badge-colors: (
"gray": #5b7282,
"red": #d91f11,
"orange": #e86427,
"yellow": #f5c518,
"lime": #3c7d0e,
"green": #077d55,
"teal": #167b7d,
"turquoise": #067a91,
"aqua": #0073ba,
"blue": #186ade,
"ultramarine": #535fe8,
"purple": #8f49de,
"pink": #cc1d92,
);
+36 -24
View File
@@ -148,7 +148,7 @@ styles with important tags:
}
.container {
max-width: 1200px !important;
max-width: 1280px !important;
}
.container,
@@ -296,20 +296,15 @@ styles with important tags:
border-left: none;
border-right: none;
border-radius: 0px;
box-shadow: 0 0 0 1px rgba(0 0 0 / 16%), 0 70px 65px rgb(0 0 0 / 8%),
0 30px 30px rgb(0 0 0 / 7%), 0 15px 15px rgb(0 0 0 / 5%),
0 10px 8px rgb(0 0 0 / 3%), 0 4px 4px rgb(0 0 0 / 2%),
box-shadow:
0 0 0 1px rgba(0 0 0 / 16%),
0 70px 65px rgb(0 0 0 / 8%),
0 30px 30px rgb(0 0 0 / 7%),
0 15px 15px rgb(0 0 0 / 5%),
0 10px 8px rgb(0 0 0 / 3%),
0 4px 4px rgb(0 0 0 / 2%),
0 2px 2px rgb(0 0 0 / 1%) !important;
&.panel-clear {
padding-left: 1rem;
padding-right: 1rem;
background-color: transparent;
border: none;
border-color: transparent !important;
box-shadow: none !important;
}
.video-bg {
background-color: $black;
}
@@ -356,6 +351,15 @@ styles with important tags:
}
}
.panel-clear {
padding-left: 1rem;
padding-right: 1rem;
background-color: transparent;
border: none;
border-color: transparent !important;
box-shadow: none !important;
}
.menu {
border-radius: 0.5rem !important;
color: $white;
@@ -1020,9 +1024,13 @@ styles with important tags:
background-color: $panel !important;
min-height: 220px;
height: 100%;
box-shadow: 0 0 0 1px rgba(0 0 0 / 30%), 0 70px 65px rgb(0 0 0 / 10%),
0 30px 30px rgb(0 0 0 / 8%), 0 15px 15px rgb(0 0 0 / 6%),
0 10px 8px rgb(0 0 0 / 4%), 0 4px 4px rgb(0 0 0 / 2%),
box-shadow:
0 0 0 1px rgba(0 0 0 / 30%),
0 70px 65px rgb(0 0 0 / 10%),
0 30px 30px rgb(0 0 0 / 8%),
0 15px 15px rgb(0 0 0 / 6%),
0 10px 8px rgb(0 0 0 / 4%),
0 4px 4px rgb(0 0 0 / 2%),
0 2px 2px rgb(0 0 0 / 1%) !important;
}
@@ -1031,9 +1039,13 @@ styles with important tags:
border-radius: 0.5rem;
background-color: $panel !important;
min-height: 650px;
box-shadow: 0 0 0 1px rgba(0 0 0 / 30%), 0 70px 65px rgb(0 0 0 / 10%),
0 30px 30px rgb(0 0 0 / 8%), 0 15px 15px rgb(0 0 0 / 6%),
0 10px 8px rgb(0 0 0 / 4%), 0 4px 4px rgb(0 0 0 / 2%),
box-shadow:
0 0 0 1px rgba(0 0 0 / 30%),
0 70px 65px rgb(0 0 0 / 10%),
0 30px 30px rgb(0 0 0 / 8%),
0 15px 15px rgb(0 0 0 / 6%),
0 10px 8px rgb(0 0 0 / 4%),
0 4px 4px rgb(0 0 0 / 2%),
0 2px 2px rgb(0 0 0 / 1%) !important;
}
@@ -1633,18 +1645,18 @@ textarea {
padding-left: var(--bs-gutter-x, 1.75rem);
margin-right: auto;
margin-left: auto;
max-width: 1200px;
max-width: 1280px;
border-radius: 0.5rem;
}
.panel {
border-radius: 0.5rem;
border: 1px solid;
}
&.panel-clear {
padding-left: 0rem;
padding-right: 0rem;
}
.panel-clear {
padding-left: 0rem;
padding-right: 0rem;
}
ul.patrons-list {
@@ -8,7 +8,6 @@ import { ModerationIpBanListFc } from "./pages/moderation/moderation_ip_ban_list
import { ModerationViewIpBanFc } from "./pages/moderation/moderation_view_ip_ban/ModerationViewIpBanFc";
import { LipsyncEditor } from "./pages/lipsync";
import { ProfileEditFc } from "./pages/profile/profile_edit/ProfileEditFc";
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { SignupPage } from "./pages/signup/SignupPage";
@@ -96,6 +95,8 @@ import { VoiceDesignerVoiceEditPage } from "./pages/voice_designer/VoiceDesigner
import VoiceDesignerUseVoicePage from "./pages/voice_designer/VoiceDesignerUseVoicePage";
import { PasswordResetEmailPage } from "./pages/password_reset/PasswordResetEmailPage";
import { PasswordResetVerificationPage } from "./pages/password_reset/PasswordResetVerificationPage";
import { NewProfilePage } from "./pages/profile/profile_view/NewProfilePage";
import { ProfilePage } from "./pages/profile/profile_view/ProfilePage";
interface Props {
sessionWrapper: SessionWrapper;
@@ -305,6 +306,16 @@ class PageContainer extends React.Component<
/>
</Route>
{/* New Profile Page */}
<Route path="/dev-profile/:username">
<NewProfilePage
sessionWrapper={this.props.sessionWrapper}
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
/>
</Route>
<Route path="/signup">
<SignupPage
querySessionCallback={() => {}}
@@ -639,42 +650,39 @@ class PageContainer extends React.Component<
</Route>
{/* Route for initial voice creation */}
<Route
exact
path="/voice-designer/create"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/create">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for editing the dataset details */}
<Route
exact
path="/voice-designer/dataset/:dataset_token/edit"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/dataset/:dataset_token/edit">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
{/* Route for handling dataset token for uploading samples */}
<Route
exact
path="/voice-designer/dataset/:dataset_token/upload"
>
<VoiceDesignerFormPage {...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper
}}/>
<Route exact path="/voice-designer/dataset/:dataset_token/upload">
<VoiceDesignerFormPage
{...{
enqueueInferenceJob: this.props.enqueueInferenceJob,
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
}}
/>
</Route>
<Route path="/voice-designer/voice/:voice_token/edit">
@@ -695,14 +703,16 @@ class PageContainer extends React.Component<
</Route>
<Route path="/voice-designer">
<VoiceDesignerMainPage {...{
sessionWrapper:this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
enqueueInferenceJob: this.props.enqueueInferenceJob,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory: this.props.inferenceJobsByCategory
}} />
<VoiceDesignerMainPage
{...{
sessionWrapper: this.props.sessionWrapper,
sessionSubscriptionsWrapper:
this.props.sessionSubscriptionsWrapper,
enqueueInferenceJob: this.props.enqueueInferenceJob,
inferenceJobs: this.props.inferenceJobs,
inferenceJobsByCategory: this.props.inferenceJobsByCategory,
}}
/>
</Route>
<Route path="/generate-speech">
@@ -0,0 +1,430 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useCallback, useEffect, useState } from "react";
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faDiscord,
faGithub,
faTwitch,
faTwitter,
} from "@fortawesome/free-brands-svg-icons";
import {
faBan,
faGear,
faGlobe,
faDollarSign,
faStar,
} from "@fortawesome/free-solid-svg-icons";
import {
GetUserByUsername,
GetUserByUsernameIsErr,
GetUserByUsernameIsOk,
User,
UserLookupError,
} from "@storyteller/components/src/api/user/GetUserByUsername";
import { format } from "date-fns";
import { WebUrl } from "../../../../../common/WebUrl";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import {
faBookmark,
faCalendarAlt,
faLayerGroup,
faPhotoFilmMusic,
} 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";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
}
function NewProfilePage(this: any, props: Props) {
const { username }: { username: string } = useParams();
const { pathname } = useLocation();
PosthogClient.recordPageview();
const [userData, setUserData] = useState<User | undefined>(undefined);
const [notFoundState, setNotFoundState] = useState<boolean>(false);
const getUser = useCallback(async (username) => {
const response = await GetUserByUsername(username);
if (GetUserByUsernameIsOk(response)) {
setUserData(response);
} else if (GetUserByUsernameIsErr(response)) {
switch (response) {
case UserLookupError.NotFound:
setNotFoundState(true);
break;
}
}
}, []);
useEffect(() => {
getUser(username);
}, [username, getUser]);
const documentTitle =
userData?.display_name === undefined
? undefined
: `${userData.display_name}`;
usePrefixedDocumentTitle(documentTitle);
if (
pathname === `/dev-profile/${username}` ||
pathname === `/dev-profile/${username}/`
) {
return <Redirect to={`/dev-profile/${username}/media`} />;
}
if (notFoundState) {
return (
<div className="container py-5">
<div className="py-5">
<h1 className="fw-semibold text-center mb-4">User not found</h1>
<div className="text-center">
<Link className="btn btn-primary" to="/">
Back to main
</Link>
</div>
</div>
</div>
);
}
if (!userData) {
return <div />;
}
let userEmailHash = userData.email_gravatar_hash;
let editProfileButton = undefined;
let banUserButton = undefined;
let upgradeButton = undefined;
if (props.sessionWrapper.isLoggedIn()) {
if (props.sessionWrapper.userTokenMatches(userData.user_token)) {
if (!props.sessionSubscriptionsWrapper.hasPaidFeatures()) {
const upgradeLinkUrl = WebUrl.pricingPage();
upgradeButton = (
<>
<Link className="btn btn-primary" to={upgradeLinkUrl}>
<FontAwesomeIcon icon={faStar} className="me-2" />
<span>Upgrade</span>
</Link>
</>
);
}
}
}
if (props.sessionWrapper.canBanUsers()) {
const currentlyBanned = userData.maybe_moderator_fields?.is_banned;
const banLinkUrl = WebUrl.userProfileBanPage(userData.username);
const buttonLabel = currentlyBanned ? "Unban" : "Ban";
const banButtonCss = currentlyBanned
? "btn btn-secondary"
: "btn btn-destructive";
banUserButton = (
<>
<Link className={banButtonCss} to={banLinkUrl}>
<FontAwesomeIcon icon={faBan} className="me-2" />
{buttonLabel}
</Link>
</>
);
}
let profileButtonsMobile = <span />;
if (props.sessionWrapper.canEditUserProfile(userData.username)) {
const editLinkUrl = WebUrl.userProfileEditPage(userData.username);
// Mods shouldn't edit preferences.
const buttonLabel = props.sessionWrapper.userTokenMatches(
userData.user_token
)
? "Edit Profile & Preferences"
: "Edit Profile";
editProfileButton = (
<>
<Link className={"btn btn-secondary"} to={editLinkUrl}>
<FontAwesomeIcon icon={faGear} className="me-2" />
{buttonLabel}
</Link>
</>
);
profileButtonsMobile = (
<div className="container d-flex d-lg-none mb-4">
<div className="d-flex w-100 gap-3 justify-content-center flex-column flex-md-row">
{upgradeButton}
{editProfileButton}
{banUserButton}
</div>
</div>
);
}
let profileDesc = (
<div className="mt-3 text-center text-lg-start opacity-50">
No profile description.
</div>
);
if (!!userData.profile_rendered_html) {
profileDesc = (
<div
className="mt-3 text-center text-lg-start"
dangerouslySetInnerHTML={{
__html: userData.profile_rendered_html || "",
}}
/>
);
}
let profileRows: Array<JSX.Element> = [];
let profileJoinDate: Array<JSX.Element> = [];
const createdAt = new Date(userData.created_at);
const joinDate = format(createdAt, "LLLL y");
profileJoinDate.push(
<div
key="created"
className="d-flex align-items-center justify-content-center justify-content-lg-start"
>
<FontAwesomeIcon icon={faCalendarAlt} className="me-2" />
<p className="fw-normal">Joined {joinDate}</p>
</div>
);
if (userData.website_url !== undefined && userData.website_url !== null) {
let websiteUrl = <span>{userData.website_url}</span>;
if (
userData.website_url?.startsWith("http://") ||
userData.website_url?.startsWith("https://")
) {
websiteUrl = (
<Tippy
content="Website"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
href={userData.website_url}
target="_blank"
rel="noopener noreferrer nofollow"
>
<FontAwesomeIcon icon={faGlobe} />
</a>
</Tippy>
);
}
profileRows.push(<div key="website">{websiteUrl}</div>);
}
if (userData.twitch_username) {
let twitchUrl = `https://twitch.com/${userData.twitch_username}`;
let twitchLink = (
<Tippy
content="Twitch"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={twitchUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faTwitch} />
</a>
</Tippy>
);
profileRows.push(<div key="twitch">{twitchLink}</div>);
}
if (userData.twitter_username) {
let twitterUrl = `https://twitter.com/${userData.twitter_username}`;
let twitterLink = (
<Tippy
content="Twitter"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={twitterUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faTwitter} />
</a>
</Tippy>
);
profileRows.push(<div key="twitter">{twitterLink}</div>);
}
if (userData.discord_username) {
profileRows.push(
<Tippy
allowHTML
content={
<div className="text-center">
Discord
<br />
<p className="fw-semibold fs-6">{userData.discord_username}</p>
</div>
}
interactive
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
// eslint-disable-next-line no-script-url
href="javascript:;"
// onClick={copyToClipboard(userData.discord_username)}
>
<FontAwesomeIcon icon={faDiscord} />
</a>
</Tippy>
);
}
if (userData.github_username) {
let githubUrl = `https://github.com/${userData.github_username}`;
let githubLink = (
<Tippy
content="GitHub"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a
href={githubUrl}
target="_blank"
rel="noopener noreferrer nofollow"
className="fw-normal"
>
<FontAwesomeIcon icon={faGithub} />
</a>
</Tippy>
);
profileRows.push(<div key="github">{githubLink}</div>);
}
if (userData.cashapp_username) {
// NB: URL includes a dollar sign
let cashAppUrl = `https://cash.me/$${userData.cashapp_username}`;
let cashAppLink = (
<Tippy
content="CashApp"
hideOnClick
placement="bottom"
theme="fakeyou"
arrow={false}
>
<a href={cashAppUrl} target="_blank" rel="noopener noreferrer nofollow">
<FontAwesomeIcon icon={faDollarSign} />
</a>
</Tippy>
);
profileRows.push(<div key="cashapp">{cashAppLink}</div>);
}
const tabs = [
{
to: `/dev-profile/${username}/media`,
label: "Media",
content: <MediaTab />,
icon: faPhotoFilmMusic,
},
{
to: `/dev-profile/${username}/weights`,
label: "Weights",
content: <WeightsTab />,
icon: faLayerGroup,
},
{
to: `/dev-profile/${username}/bookmarks`,
label: "Bookmarks",
content: <BookmarksTab />,
icon: faBookmark,
},
];
return (
<Container type="panel">
<Panel clear={true}>
<div className="d-flex flex-column flex-lg-row py-4">
<div className="mb-3 me-lg-4 mb-lg-0">
<div className="border-3 d-none d-lg-block">
<Gravatar
size={150}
username={userData.display_name}
email_hash={userEmailHash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
</div>
<div className="border-3 text-center d-lg-none">
<Gravatar
size={100}
username={userData.display_name}
email_hash={userEmailHash}
avatarIndex={userData.default_avatar_index}
backgroundIndex={userData.default_avatar_color_index}
/>
</div>
</div>
<div className="d-flex flex-column w-100">
<div className="d-flex">
<h1 className="fw-bold mb-0 flex-grow-1 text-center text-lg-start">
{userData.display_name}
</h1>
<div className="gap-3 d-none d-lg-flex">
{banUserButton}
{upgradeButton}
{editProfileButton}
</div>
</div>
<div className="opacity-75 mt-1">{profileJoinDate}</div>
<div>{profileDesc}</div>
<div className="d-flex mt-3 gap-4 gap-lg-3 profile-social-icons justify-content-center justify-content-lg-start">
{profileRows}
</div>
</div>
</div>
</Panel>
{profileButtonsMobile}
<Panel mb={true}>
<Tabs tabs={tabs} />
</Panel>
</Container>
);
}
export { NewProfilePage };
@@ -0,0 +1,58 @@
import React, { useRef, useState } from "react";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import AudioCard from "components/common/Card/AudioCard";
import ImageCard from "components/common/Card/ImageCard";
import VideoCard from "components/common/Card/VideoCard";
import Panel from "components/common/Panel";
import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContext";
import SkeletonCard from "components/common/Card/SkeletonCard";
import mockWeightsData from "./mockWeightsData";
export default function BookmarksTab() {
const [data, setData] = useState(mockWeightsData);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const gridContainerRef = useRef<HTMLDivElement | null>(null);
return (
<Panel clear={true} padding={true}>
<AudioPlayerProvider>
{isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{data.map((data, index) => {
let card;
switch (data.media_type) {
case "audio":
card = <AudioCard key={index} data={data} type="weights" />;
break;
case "image":
card = <ImageCard key={index} data={data} type="weights" />;
break;
case "video":
card = <VideoCard key={index} data={data} type="weights" />;
break;
default:
card = <div key={index}>Unsupported media type</div>;
}
return (
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
{card}
</div>
);
})}
</MasonryGrid>
)}
</AudioPlayerProvider>
</Panel>
);
}
@@ -0,0 +1,90 @@
import React, { useRef, useState } from "react";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import mockMediaData from "./mockMediaData";
import AudioCard from "components/common/Card/AudioCard";
import ImageCard from "components/common/Card/ImageCard";
import VideoCard from "components/common/Card/VideoCard";
import Panel from "components/common/Panel";
import { Select } from "components/common/Inputs/Inputs";
import {
faArrowDownWideShort,
faFilter,
} from "@fortawesome/pro-solid-svg-icons";
import AudioPlayerProvider from "components/common/AudioPlayer/AudioPlayerContext";
import SkeletonCard from "components/common/Card/SkeletonCard";
export default function MediaTab() {
const [data, setData] = useState(mockMediaData);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const filterOptions = [
{ value: "all", label: "All Media" },
{ value: "images", label: "Images" },
{ value: "audio", label: "Audio" },
{ value: "video", label: "Video" },
];
const sortOptions = [
{ value: "newest", label: "Newest" },
{ value: "oldest", label: "Oldest" },
{ value: "mostliked", label: "Most Liked" },
];
return (
<Panel clear={true} padding={true}>
<div className="d-flex gap-2 mb-3">
<Select
icon={faArrowDownWideShort}
options={sortOptions}
defaultValue={sortOptions[0]}
isSearchable={false}
/>
<Select
icon={faFilter}
options={filterOptions}
defaultValue={filterOptions[0]}
isSearchable={false}
/>
</div>
<AudioPlayerProvider>
{isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{data.map((data, index) => {
let card;
switch (data.media_type) {
case "audio":
card = <AudioCard key={index} data={data} type="media" />;
break;
case "image":
card = <ImageCard key={index} data={data} type="media" />;
break;
case "video":
card = <VideoCard key={index} data={data} type="media" />;
break;
default:
card = <div key={index}>Unsupported media type</div>;
}
return (
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
{card}
</div>
);
})}
</MasonryGrid>
)}
</AudioPlayerProvider>
</Panel>
);
}
@@ -0,0 +1,86 @@
import React, { useRef, useState } from "react";
import MasonryGrid from "components/common/MasonryGrid/MasonryGrid";
import mockWeightsData from "./mockWeightsData";
import AudioCard from "components/common/Card/AudioCard";
import ImageCard from "components/common/Card/ImageCard";
import VideoCard from "components/common/Card/VideoCard";
import Panel from "components/common/Panel";
import { Select } from "components/common/Inputs/Inputs";
import {
faArrowDownWideShort,
faFilter,
} from "@fortawesome/pro-solid-svg-icons";
import SkeletonCard from "components/common/Card/SkeletonCard";
export default function WeightsTab() {
const [data, setData] = useState(mockWeightsData);
const [isLoading, setIsLoading] = useState(false);
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const filterOptions = [
{ value: "all", label: "All Weights" },
{ value: "tts", label: "Text to Speech" },
{ value: "vc", label: "Voice to Voice" },
{ value: "vd", label: "Voice Designer" },
];
const sortOptions = [
{ value: "newest", label: "Newest" },
{ value: "oldest", label: "Oldest" },
{ value: "mostliked", label: "Most Favorited" },
];
return (
<Panel clear={true} padding={true}>
<div className="d-flex gap-2 mb-3">
<Select
icon={faArrowDownWideShort}
options={sortOptions}
defaultValue={sortOptions[0]}
isSearchable={false}
/>
<Select
icon={faFilter}
options={filterOptions}
defaultValue={filterOptions[0]}
isSearchable={false}
/>
</div>
{isLoading ? (
<div className="row gx-3 gy-3">
{Array.from({ length: 6 }).map((_, index) => (
<SkeletonCard key={index} />
))}
</div>
) : (
<MasonryGrid
gridRef={gridContainerRef}
onLayoutComplete={() => console.log("Layout complete!")}
>
{data.map((data, index) => {
let card;
switch (data.media_type) {
case "audio":
card = <AudioCard key={index} data={data} type="weights" />;
break;
case "image":
card = <ImageCard key={index} data={data} type="weights" />;
break;
case "video":
card = <VideoCard key={index} data={data} type="weights" />;
break;
default:
card = <div key={index}>Unsupported media type</div>;
}
return (
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
{card}
</div>
);
})}
</MasonryGrid>
)}
</Panel>
);
}
@@ -0,0 +1,384 @@
let mockMediaData = [
{
token: "token1",
weight_name: "Queen Elizabeth",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token2",
weight_name: "Monster House",
media_type: "image",
public_bucket_path: "/images/dummy-image.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token3",
weight_name: "Ronaldo",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token4",
weight_name: "Desert",
media_type: "video",
public_bucket_path: "/images/dummy-video.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token5",
weight_name: "3D Cat",
media_type: "image",
public_bucket_path: "/images/dummy-image-2.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token6",
weight_name: "Superman",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token7",
weight_name: "Son Goku",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token8",
weight_name: "Astronaut in Space",
media_type: "image",
public_bucket_path: "/images/dummy-image-3.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token9",
weight_name: "Monokuma",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token10",
weight_name: "Watery Landscape",
media_type: "image",
public_bucket_path: "/images/dummy-image-4.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token11",
weight_name: "Queen Elizabeth",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token12",
weight_name: "Monster House",
media_type: "image",
public_bucket_path: "/images/dummy-image.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token13",
weight_name: "Ronaldo",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token14",
weight_name: "Desert",
media_type: "video",
public_bucket_path: "/images/dummy-video.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token15",
weight_name: "3D Cat",
media_type: "image",
public_bucket_path: "/images/dummy-image-2.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token16",
weight_name: "Superman",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token17",
weight_name: "Son Goku",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token18",
weight_name: "Astronaut in Space",
media_type: "image",
public_bucket_path: "/images/dummy-image-3.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token19",
weight_name: "Monokuma",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token20",
weight_name: "Watery Landscape",
media_type: "image",
public_bucket_path: "/images/dummy-image-4.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token1",
weight_name: "Queen Elizabeth",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token2",
weight_name: "Monster House",
media_type: "image",
public_bucket_path: "/images/dummy-image.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token3",
weight_name: "Ronaldo",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token4",
weight_name: "Desert",
media_type: "video",
public_bucket_path: "/images/dummy-video.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token5",
weight_name: "3D Cat",
media_type: "image",
public_bucket_path: "/images/dummy-image-2.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token6",
weight_name: "Superman",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token7",
weight_name: "Son Goku",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token8",
weight_name: "Astronaut in Space",
media_type: "image",
public_bucket_path: "/images/dummy-image-3.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token9",
weight_name: "Monokuma",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token10",
weight_name: "Watery Landscape",
media_type: "image",
public_bucket_path: "/images/dummy-image-4.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token11",
weight_name: "Queen Elizabeth",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token12",
weight_name: "Monster House",
media_type: "image",
public_bucket_path: "/images/dummy-image.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token13",
weight_name: "Ronaldo",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token14",
weight_name: "Desert",
media_type: "video",
public_bucket_path: "/images/dummy-video.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token15",
weight_name: "3D Cat",
media_type: "image",
public_bucket_path: "/images/dummy-image-2.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token16",
weight_name: "Superman",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token17",
weight_name: "Son Goku",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token18",
weight_name: "Astronaut in Space",
media_type: "image",
public_bucket_path: "/images/dummy-image-3.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token19",
weight_name: "Monokuma",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token20",
weight_name: "Watery Landscape",
media_type: "image",
public_bucket_path: "/images/dummy-image-4.jpg",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
];
export default mockMediaData;
@@ -0,0 +1,66 @@
let mockWeightsData = [
{
token: "token1",
weight_name: "Queen Elizabeth",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 150,
isLiked: true,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token2",
weight_name: "Monster House",
media_type: "image",
public_bucket_path: "/images/dummy-image.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token3",
weight_name: "Ronaldo",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token5",
weight_name: "3D Cat",
media_type: "image",
public_bucket_path: "/images/dummy-image-2.jpg",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
{
token: "token6",
weight_name: "Superman",
media_type: "audio",
public_bucket_path:
"/media/s/p/x/d/2/spxd2xr7nt7zdezyhp61h00kxatr18pf/fakeyou_rvc_spxd2xr7nt7zdezyhp61h00kxatr18pf.wav",
likes: 200,
isLiked: false,
created_at: "2023-08-10T09:00:00.000Z",
},
];
// Randomize the order of the array
for (let i = mockWeightsData.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[mockWeightsData[i], mockWeightsData[j]] = [
mockWeightsData[j],
mockWeightsData[i],
];
}
// Update the 'token' property to start at 1
mockWeightsData.forEach((item, index) => {
item.token = `token${index + 1}`;
});
export default mockWeightsData;
+170 -1
View File
@@ -4420,6 +4420,7 @@ __metadata:
"@fortawesome/pro-regular-svg-icons": ^6.2.1
"@fortawesome/pro-solid-svg-icons": ^6.2.1
"@fortawesome/react-fontawesome": ^0.2.0
"@react-spring/types": ^9.7.3
"@react-spring/web": ^9.7.3
"@storyteller/components": 0.1.0
"@testing-library/jest-dom": ^4.2.4
@@ -4431,8 +4432,11 @@ __metadata:
"@types/google.analytics": ^0.0.42
"@types/gtag.js": ^0.0.12
"@types/howler": ^2.1.2
"@types/imagesloaded": ^4.1.6
"@types/infinite-scroll": ^4.0.3
"@types/jest": ^24.9.1
"@types/js-cookie": ^3.0.1
"@types/masonry-layout": ^4.2.7
"@types/node": ^12.12.47
"@types/react": ^16.9.36
"@types/react-autocomplete": ^1.8.6
@@ -4451,7 +4455,10 @@ __metadata:
i18next: ^21.6.11
i18next-browser-languagedetector: ^7.1.0
i18next-http-backend: ^2.2.1
imagesloaded: ^5.0.0
infinite-scroll: ^4.0.1
js-cookie: ^3.0.1
masonry-layout: ^4.2.2
posthog-js: ^1.73.1
pretty-num: ^0.5.2
query-string: ^8.1.0
@@ -4461,6 +4468,7 @@ __metadata:
react-dom: ^16.13.1
react-drag-drop-files: ^2.3.9
react-i18next: ^11.15.4
react-infinite-scroll-component: ^6.1.0
react-range: ^1.8.14
react-reveal: ^1.2.2
react-router-dom: ^5.2.0
@@ -4471,6 +4479,7 @@ __metadata:
react-wavesurfer.js: ^0.0.8
sass: ^1.57.1
typescript: ^3.7.5
universal-cookie: ^6.1.1
uuid: ^8.3.2
wavesurfer.js: ^6.4.0
zustand: ^4.3.9
@@ -4572,7 +4581,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storyteller/root@workspace:."
dependencies:
"@react-spring/types": ^9.7.3
craco: ^0.0.3
cross-env: ^7.0.3
husky: ^8.0.3
@@ -5251,6 +5259,34 @@ __metadata:
languageName: node
linkType: hard
"@types/imagesloaded@npm:^4.1.6":
version: 4.1.6
resolution: "@types/imagesloaded@npm:4.1.6"
dependencies:
"@types/jquery": "*"
checksum: adfea961f0295e173ac4f81a58d13b62247576aef057f010da6b651e4191e86d52c97d4386d154c73ca8fd71d05b3bb29da8f9f3e20fda2c677af94a122feb6d
languageName: node
linkType: hard
"@types/infinite-scroll@npm:^4.0.3":
version: 4.0.3
resolution: "@types/infinite-scroll@npm:4.0.3"
dependencies:
"@types/isotope-layout": "*"
"@types/jquery": "*"
"@types/masonry-layout": "*"
"@types/packery": "*"
checksum: c7c9a2db7eb6a26c646a63c36fb1b04b381b9bcbae1b5fbc97d36f05eea36d6b17749b52cb906be578087aa203bfce72541b175fd0529df7c71961a7e7608668
languageName: node
linkType: hard
"@types/isotope-layout@npm:*":
version: 3.0.12
resolution: "@types/isotope-layout@npm:3.0.12"
checksum: 7ae477fad4017a2e7ebbd0b5202e26d6febc58bc0604688d99d2ff822157d48eab77c8a89d942afc21ad1b0d51967491d07c9619d2e5722b626964c2c03e71b9
languageName: node
linkType: hard
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1":
version: 2.0.4
resolution: "@types/istanbul-lib-coverage@npm:2.0.4"
@@ -5305,6 +5341,15 @@ __metadata:
languageName: node
linkType: hard
"@types/jquery@npm:*":
version: 3.5.29
resolution: "@types/jquery@npm:3.5.29"
dependencies:
"@types/sizzle": "*"
checksum: 5e959762d6f7050b07b4387b6507a308113384566a77cfc4f8d0f54c2fb0a79f6bc8c057706c6aa4840cde56f32ad0e5814fb53c5f078c5db9e01670a1ecd535
languageName: node
linkType: hard
"@types/js-cookie@npm:^3.0.1":
version: 3.0.2
resolution: "@types/js-cookie@npm:3.0.2"
@@ -5326,6 +5371,15 @@ __metadata:
languageName: node
linkType: hard
"@types/masonry-layout@npm:*, @types/masonry-layout@npm:^4.2.7":
version: 4.2.7
resolution: "@types/masonry-layout@npm:4.2.7"
dependencies:
"@types/jquery": "*"
checksum: ef12ab8dc278ce0c96239f8542715d87b856db2414dd57c4256bb232f389247522d7f73ddd2bfe717a6c7a5e27ac5ec4dbc44ab95786ee49f97c38d97087cd71
languageName: node
linkType: hard
"@types/mime@npm:^1":
version: 1.3.2
resolution: "@types/mime@npm:1.3.2"
@@ -5375,6 +5429,15 @@ __metadata:
languageName: node
linkType: hard
"@types/packery@npm:*":
version: 1.4.37
resolution: "@types/packery@npm:1.4.37"
dependencies:
"@types/jquery": "*"
checksum: 8350af4392e89988f4d8ec57827b47503be54365f5486c34ef36af3c0f48f1dc18332805eec7d04b06a6c22c2ef7db6cae03bc840058a9972d08fca21b0bc957
languageName: node
linkType: hard
"@types/parse-json@npm:^4.0.0":
version: 4.0.0
resolution: "@types/parse-json@npm:4.0.0"
@@ -5538,6 +5601,13 @@ __metadata:
languageName: node
linkType: hard
"@types/sizzle@npm:*":
version: 2.3.8
resolution: "@types/sizzle@npm:2.3.8"
checksum: 2ac62443dc917f5f903cbd9afc51c7d6cc1c6569b4e1a15faf04aea5b13b486e7f208650014c3dc4fed34653eded3e00fe5abffe0e6300cbf0e8a01beebf11a6
languageName: node
linkType: hard
"@types/sockjs@npm:^0.3.33":
version: 0.3.33
resolution: "@types/sockjs@npm:0.3.33"
@@ -9360,6 +9430,13 @@ __metadata:
languageName: node
linkType: hard
"desandro-matches-selector@npm:^2.0.0":
version: 2.0.2
resolution: "desandro-matches-selector@npm:2.0.2"
checksum: 30979e6b45d7720d259d4db11ec026fcbee242deee5ec0944c265d7e386a7358ab29f7417cbc3631830d0dbe6e827940dee31f789ed865dba22880d209fc9f8e
languageName: node
linkType: hard
"destroy@npm:~1.0.4":
version: 1.0.4
resolution: "destroy@npm:1.0.4"
@@ -10389,6 +10466,20 @@ __metadata:
languageName: node
linkType: hard
"ev-emitter@npm:^1.0.0":
version: 1.1.1
resolution: "ev-emitter@npm:1.1.1"
checksum: 3dd78a7620701ef8095794f611dd251bdd9023badf322b3d6fa5e10a660c50e83eec5ee5bbc7654682635cf7b8858c94e2c77e1d10bdc188a2b0593436403476
languageName: node
linkType: hard
"ev-emitter@npm:^2.1.0, ev-emitter@npm:^2.1.2":
version: 2.1.2
resolution: "ev-emitter@npm:2.1.2"
checksum: 1102823ef040684869256100e19a6c6aeeecd422d09a267d7c74e77023136eede2ee86ee475ad1019d30d1f7917557d74542d5665e9f91c398bd5814260af6b3
languageName: node
linkType: hard
"eventemitter3@npm:^3.1.0":
version: 3.1.2
resolution: "eventemitter3@npm:3.1.2"
@@ -10869,6 +10960,22 @@ __metadata:
languageName: node
linkType: hard
"fizzy-ui-utils@npm:^2.0.0":
version: 2.0.7
resolution: "fizzy-ui-utils@npm:2.0.7"
dependencies:
desandro-matches-selector: ^2.0.0
checksum: 001e54effa22c4f62728b90a25e8eb5477057ab53d0184e1c3231bbdfc7c34e278418fbcaf7d63c75ede823d82e61245e668ce8cba90e1c5699de960188c654f
languageName: node
linkType: hard
"fizzy-ui-utils@npm:^3.0.0":
version: 3.0.0
resolution: "fizzy-ui-utils@npm:3.0.0"
checksum: 73b53e5dc89e033bad3eb9cf111d220502afae083c84fbc842d9453202673284de270f7bbe6cc77a806678e62cf8068da1b4280f0d3a1bf1310110248dfa96cd
languageName: node
linkType: hard
"flat-cache@npm:^3.0.4":
version: 3.0.4
resolution: "flat-cache@npm:3.0.4"
@@ -11298,6 +11405,10 @@ __metadata:
languageName: node
linkType: hard
"get-size@npm:^2.0.2":
version: 2.0.3
resolution: "get-size@npm:2.0.3"
checksum: 18d5a5fdb3f541db8b1e6ad46a5411d4bb7da3061d2ef7e544db3bbce7a13543f05c91fd40c7f68b4bc4bc035c160fed23c45514cde7d5fb18de1cce10208b19
"get-stream@npm:^5.0.0":
version: 5.2.0
resolution: "get-stream@npm:5.2.0"
@@ -12186,6 +12297,15 @@ __metadata:
languageName: node
linkType: hard
"imagesloaded@npm:^5.0.0":
version: 5.0.0
resolution: "imagesloaded@npm:5.0.0"
dependencies:
ev-emitter: ^2.1.2
checksum: aec1826e866bcf2f92fc533b78e431597096c32dda3cf6de060f9dc2bdf1e39bb600eda244a7280ed09a7d12462fb63cf01e723799d35a86a326592e2f23751b
languageName: node
linkType: hard
"immer@npm:^9.0.7":
version: 9.0.12
resolution: "immer@npm:9.0.12"
@@ -12243,6 +12363,16 @@ __metadata:
languageName: node
linkType: hard
"infinite-scroll@npm:^4.0.1":
version: 4.0.1
resolution: "infinite-scroll@npm:4.0.1"
dependencies:
ev-emitter: ^2.1.0
fizzy-ui-utils: ^3.0.0
checksum: e6000c0b82eace9df15ffb007447ecd12b806df60a41c6a06cdc0f931ae419e360be1e713f02e1047e088c75e0060198b25d19de6c3f88bd5fe0f987e326050a
languageName: node
linkType: hard
"inflight@npm:^1.0.4":
version: 1.0.6
resolution: "inflight@npm:1.0.6"
@@ -14374,6 +14504,16 @@ __metadata:
languageName: node
linkType: hard
"masonry-layout@npm:^4.2.2":
version: 4.2.2
resolution: "masonry-layout@npm:4.2.2"
dependencies:
get-size: ^2.0.2
outlayer: ^2.1.0
checksum: b947029b8fdfb384d17d7749d80a4f00f88d885acea04d77e1fefd36827cc2a345fd40d9ccddf720ed9323e133d8ea498125eece91d2c8517f69fd4e9e1bbdf4
languageName: node
linkType: hard
"md5.js@npm:^1.3.4":
version: 1.3.5
resolution: "md5.js@npm:1.3.5"
@@ -15701,6 +15841,17 @@ __metadata:
languageName: node
linkType: hard
"outlayer@npm:^2.1.0":
version: 2.1.1
resolution: "outlayer@npm:2.1.1"
dependencies:
ev-emitter: ^1.0.0
fizzy-ui-utils: ^2.0.0
get-size: ^2.0.2
checksum: a8b69d07bad6b498ea1956f89a4bae6d38414d81a5feeae6af06cd8206a1bf47db82f664dc3ccd13b8d2c5886a6cded4a4642d0fafcbbd80f68873b7eb9f4153
languageName: node
linkType: hard
"p-finally@npm:^1.0.0":
version: 1.0.0
resolution: "p-finally@npm:1.0.0"
@@ -17653,6 +17804,17 @@ __metadata:
languageName: node
linkType: hard
"react-infinite-scroll-component@npm:^6.1.0":
version: 6.1.0
resolution: "react-infinite-scroll-component@npm:6.1.0"
dependencies:
throttle-debounce: ^2.1.0
peerDependencies:
react: ">=16.0.0"
checksum: 3708398934366df907dbad215247ebc1033221957ce7e32289ea31750cce70aa16513e2d03743e06c8b868ac7c542d12d5dbb6c830fd408433a4762f3cb5ecfb
languageName: node
linkType: hard
"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0, react-is@npm:^16.8.4":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -20217,6 +20379,13 @@ __metadata:
languageName: node
linkType: hard
"throttle-debounce@npm:^2.1.0":
version: 2.3.0
resolution: "throttle-debounce@npm:2.3.0"
checksum: 6d90aa2ddb294f8dad13d854a1cfcd88fdb757469669a096a7da10f515ee466857ac1e750649cb9da931165c6f36feb448318e7cb92570f0a3679d20e860a925
languageName: node
linkType: hard
"through2@npm:^2.0.0":
version: 2.0.5
resolution: "through2@npm:2.0.5"