mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
Merge pull request #432 from storytold/bflat-fy-explore-search-rebased
explore and search bar
This commit is contained in:
40 files changed
+3037
-901
No files matched your search
@@ -0,0 +1,19 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
import { Pagination } from "../_common/SharedFetchTypes";
|
||||
|
||||
export interface UserBookmarksListRequest {}
|
||||
|
||||
export interface UserBookmarksListResponse {
|
||||
// pagination: Pagination,
|
||||
success: boolean,
|
||||
// weights: any
|
||||
}
|
||||
|
||||
export interface UserBookmarksListQueries {
|
||||
page_index: number,
|
||||
}
|
||||
|
||||
export const GetBookmarksByUser = MakeRequest<string, UserBookmarksListRequest, UserBookmarksListResponse, UserBookmarksListQueries>({
|
||||
method: "GET",
|
||||
routingFunction: (userToken: string) => `/v1/user_bookmarks/list/user/${userToken}`,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
import { MediaFile } from "./GetMedia";
|
||||
// import { MediaFileType } from "../_common/enums/MediaFileType";
|
||||
import { Pagination } from "../_common/SharedFetchTypes";
|
||||
|
||||
export interface ListMediaFilesRequest {}
|
||||
|
||||
export interface ListMediaFilesResponse {
|
||||
pagination: Pagination;
|
||||
success: boolean;
|
||||
results: MediaFile[];
|
||||
}
|
||||
|
||||
export interface ListMediaFilesParams {
|
||||
page_index: number;
|
||||
}
|
||||
|
||||
export const ListMediaFiles = MakeRequest<
|
||||
string,
|
||||
ListMediaFilesRequest,
|
||||
ListMediaFilesResponse,
|
||||
ListMediaFilesParams
|
||||
>({
|
||||
method: "GET",
|
||||
routingFunction: () => `/v1/media_files/list`,
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
import { Pagination } from "../_common/SharedFetchTypes";
|
||||
|
||||
export interface UserWeightListRequest {}
|
||||
|
||||
export interface UserWeightListResponse {
|
||||
// pagination: Pagination,
|
||||
success: boolean,
|
||||
weights: any
|
||||
}
|
||||
|
||||
export interface UserWeightListQueries {
|
||||
page_index: number,
|
||||
}
|
||||
|
||||
export const GetWeightsByUser = MakeRequest<string, UserWeightListRequest, UserWeightListResponse, UserWeightListQueries>({
|
||||
method: "GET",
|
||||
routingFunction: (userToken: string) => `/v1/weights/by_user/${userToken}`,
|
||||
});
|
||||
@@ -18,6 +18,12 @@
|
||||
&.shadow {
|
||||
box-shadow: 0 0 12px rgba($bg, 0.4);
|
||||
}
|
||||
|
||||
&.badge-small {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $badge-colors {
|
||||
|
||||
@@ -6,6 +6,7 @@ interface BadgeProps {
|
||||
color: string;
|
||||
overlay?: boolean;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export default function Badge({
|
||||
@@ -13,10 +14,11 @@ export default function Badge({
|
||||
color,
|
||||
overlay = false,
|
||||
className = "",
|
||||
small = false,
|
||||
}: BadgeProps) {
|
||||
const badgeClass = `fy-badge badge-${color} ${
|
||||
overlay ? "shadow" : ""
|
||||
} mb-0 ${className}`.trim();
|
||||
} mb-0 ${className} ${small ? "badge-small" : ""}`.trim();
|
||||
|
||||
return <span className={badgeClass}>{label}</span>;
|
||||
}
|
||||
@@ -108,6 +108,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card .button-link {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px !important;
|
||||
color: $white !important;
|
||||
background-color: $primary;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($primary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disabled styles */
|
||||
.btn-primary:disabled,
|
||||
.btn-secondary:disabled,
|
||||
|
||||
@@ -6,17 +6,26 @@ import useTimeAgo from "hooks/useTimeAgo";
|
||||
import Badge from "components/common/Badge";
|
||||
import LikeButton from "components/common/LikeButton";
|
||||
import FavoriteButton from "components/common/FavoriteButton";
|
||||
import CreatorName from "../CreatorName";
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Button from "components/common/Button";
|
||||
|
||||
interface AudioCardProps {
|
||||
data: any;
|
||||
type: "media" | "weights";
|
||||
showCreator?: boolean;
|
||||
showCover?: boolean;
|
||||
}
|
||||
|
||||
export default function AudioCard({ data, type }: AudioCardProps) {
|
||||
export default function AudioCard({
|
||||
data,
|
||||
type,
|
||||
showCreator,
|
||||
showCover,
|
||||
}: AudioCardProps) {
|
||||
const history = useHistory();
|
||||
|
||||
// console.log("🐙",data);
|
||||
// console.log("🐙",data, data.public_bucket_path);
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (type === "media") {
|
||||
@@ -30,11 +39,11 @@ export default function AudioCard({ data, type }: AudioCardProps) {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const handleUseModel = () => {
|
||||
if (type === "weights") {
|
||||
history.push(`/weight/${data.token}`);
|
||||
}
|
||||
};
|
||||
// const handleUseModel = () => {
|
||||
// if (type === "weights") {
|
||||
// history.push(`/weights/${data.token}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
const timeAgo = useTimeAgo(data.created_at);
|
||||
|
||||
@@ -48,7 +57,7 @@ export default function AudioCard({ data, type }: AudioCardProps) {
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex flex-grow-1">
|
||||
<div className="d-flex flex-grow-1 align-items-center gap-2">
|
||||
<Badge label="Audio" color="teal" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,39 +72,96 @@ export default function AudioCard({ data, type }: AudioCardProps) {
|
||||
<AudioPlayer src={data.public_bucket_path} id={data.token} />
|
||||
</div>
|
||||
|
||||
<div className="mt-3" onClick={handleInnerClick}>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.like || 0 } />
|
||||
<hr className="my-3" />
|
||||
|
||||
<div
|
||||
className="d-flex align-items-center gap-2"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{showCreator && (
|
||||
<div className="flex-grow-1">
|
||||
<CreatorName
|
||||
displayName={data.maybe_creator.display_name}
|
||||
gravatarHash={data.maybe_creator.gravatar_hash}
|
||||
avatarIndex={data.maybe_creator.default_avatar.image_index}
|
||||
backgroundIndex={
|
||||
data.maybe_creator.default_avatar.color_index
|
||||
}
|
||||
username={data.maybe_creator.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.like || 0} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "weights" && (
|
||||
<>
|
||||
<div>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex flex-grow-1">
|
||||
<Badge label="RVC" color="orange" />
|
||||
<div className="d-flex">
|
||||
{showCover && (
|
||||
<div className="cover-img">
|
||||
{/* replace source image with api data */}
|
||||
<img src="/images/dummy-image-2.jpg" alt="Cover" width={100} />
|
||||
</div>
|
||||
<div onClick={handleInnerClick}>
|
||||
<FavoriteButton
|
||||
onToggle={handleLike}
|
||||
favoriteCount={data.likes}
|
||||
)}
|
||||
|
||||
<div className="flex-grow-1">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex flex-grow-1">
|
||||
<Badge label="RVC" color="orange" />
|
||||
</div>
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
variant="link"
|
||||
label="Use"
|
||||
onClick={handleCardClick}
|
||||
className="fs-7"
|
||||
/>
|
||||
</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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-3" />
|
||||
|
||||
<div
|
||||
className="d-flex align-items-center gap-2"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{showCreator && (
|
||||
<div className="flex-grow-1">
|
||||
<CreatorName
|
||||
displayName={data.maybe_creator_user.display_name}
|
||||
gravatarHash={data.maybe_creator_user.gravatar_hash}
|
||||
avatarIndex={
|
||||
data.maybe_creator_user.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
data.maybe_creator_user.default_avatar.color_index
|
||||
}
|
||||
username={data.maybe_creator_user.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.likes} />
|
||||
</div>
|
||||
<FavoriteButton onToggle={handleLike} favoriteCount={data.likes} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -82,4 +82,33 @@
|
||||
color: rgba($white, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.cover-img {
|
||||
height: 110px;
|
||||
width: 110px;
|
||||
min-width: 110px;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid rgba($white, 0.3);
|
||||
margin-right: 12px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
h6 {
|
||||
//2 lines of text then ellipsis
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface CreatorNameProps {
|
||||
displayName: string;
|
||||
username: string;
|
||||
gravatarHash: string;
|
||||
avatarIndex: number;
|
||||
backgroundIndex: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const handleInnerClick = (event: any) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
export default function CreatorName({
|
||||
displayName,
|
||||
gravatarHash,
|
||||
avatarIndex,
|
||||
backgroundIndex,
|
||||
username,
|
||||
className,
|
||||
}: CreatorNameProps) {
|
||||
return (
|
||||
<div
|
||||
className={`d-flex gap-2 align-items-center ${className}`}
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
<Gravatar
|
||||
size={22}
|
||||
email_hash={gravatarHash}
|
||||
avatarIndex={avatarIndex}
|
||||
backgroundIndex={backgroundIndex}
|
||||
/>
|
||||
<Link
|
||||
to={`/profile/${username}`}
|
||||
className="fw-medium fs-7 text-white opacity-75 text-truncate"
|
||||
>
|
||||
{displayName}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,15 +4,18 @@ 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";
|
||||
import CreatorName from "../CreatorName";
|
||||
import Button from "components/common/Button";
|
||||
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ImageCardProps {
|
||||
data: any;
|
||||
type: "media" | "weights";
|
||||
showCreator?: boolean;
|
||||
}
|
||||
|
||||
export default function ImageCard({ data, type }: ImageCardProps) {
|
||||
export default function ImageCard({ data, type, showCreator }: ImageCardProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleCardClick = () => {
|
||||
@@ -55,12 +58,32 @@ export default function ImageCard({ data, type }: ImageCardProps) {
|
||||
<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}
|
||||
/>
|
||||
|
||||
<hr className="my-2" />
|
||||
|
||||
<div
|
||||
className="d-flex align-items-center gap-2"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{showCreator && (
|
||||
<div className="flex-grow-1">
|
||||
<CreatorName
|
||||
displayName={data.maybe_creator.display_name}
|
||||
gravatarHash={data.maybe_creator.gravatar_hash}
|
||||
avatarIndex={
|
||||
data.maybe_creator.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
data.maybe_creator.default_avatar.color_index
|
||||
}
|
||||
username={data.maybe_creator.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.likes} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,18 +99,18 @@ export default function ImageCard({ data, type }: ImageCardProps) {
|
||||
/>
|
||||
<div className="card-img-overlay">
|
||||
<div className="card-img-gradient" />
|
||||
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex flex-grow-1">
|
||||
<Badge label="LORA" color="pink" overlay={true} />
|
||||
</div>
|
||||
<div onClick={handleInnerClick}>
|
||||
<FavoriteButton
|
||||
onToggle={handleLike}
|
||||
favoriteCount={data.likes}
|
||||
overlay={true}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
variant="link"
|
||||
label="Use"
|
||||
onClick={handleCardClick}
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-img-overlay-text">
|
||||
@@ -96,11 +119,39 @@ export default function ImageCard({ data, type }: ImageCardProps) {
|
||||
<h6 className="fw-semibold text-white mb-1">
|
||||
{data.weight_name}
|
||||
</h6>
|
||||
<p className="fs-7 opacity-75">{timeAgo}</p>
|
||||
<p className="fs-7 opacity-75 mb-0">{timeAgo}</p>
|
||||
</div>
|
||||
<div onClick={handleInnerClick}>
|
||||
<Button label="Use" small={true} />
|
||||
</div>
|
||||
|
||||
<hr className="my-2" />
|
||||
|
||||
<div
|
||||
className="d-flex align-items-center gap-2"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{showCreator && (
|
||||
<div className="flex-grow-1">
|
||||
<CreatorName
|
||||
displayName={data.maybe_creator_user.display_name}
|
||||
gravatarHash={data.maybe_creator_user.gravatar_hash}
|
||||
avatarIndex={
|
||||
data.maybe_creator_user.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
data.maybe_creator_user.default_avatar.color_index
|
||||
}
|
||||
username={data.maybe_creator_user.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.likes} />
|
||||
</div>
|
||||
<FavoriteButton
|
||||
onToggle={handleLike}
|
||||
favoriteCount={data.likes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,19 @@ 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 { faArrowRight, faPlayCircle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Badge from "components/common/Badge";
|
||||
import LikeButton from "components/common/LikeButton";
|
||||
import Button from "components/common/Button";
|
||||
import CreatorName from "../CreatorName";
|
||||
|
||||
interface VideoCardProps {
|
||||
data: any;
|
||||
type: "media" | "weights";
|
||||
showCreator?: boolean;
|
||||
}
|
||||
|
||||
export default function VideoCard({ data, type }: VideoCardProps) {
|
||||
export default function VideoCard({ data, type, showCreator }: VideoCardProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleCardClick = () => {
|
||||
@@ -53,12 +56,30 @@ export default function VideoCard({ data, type }: VideoCardProps) {
|
||||
{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}
|
||||
/>
|
||||
<hr className="my-2" />
|
||||
<div
|
||||
className="d-flex align-items-center gap-2"
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{showCreator && (
|
||||
<div className="flex-grow-1">
|
||||
<CreatorName
|
||||
displayName={data.maybe_creator.display_name}
|
||||
gravatarHash={data.maybe_creator.gravatar_hash}
|
||||
avatarIndex={
|
||||
data.maybe_creator.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
data.maybe_creator.default_avatar.color_index
|
||||
}
|
||||
username={data.maybe_creator.username}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<LikeButton onToggle={handleLike} likeCount={data.likes} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,6 +101,14 @@ export default function VideoCard({ data, type }: VideoCardProps) {
|
||||
<div className="d-flex flex-grow-1">
|
||||
<Badge label="Video" color="purple" overlay={true} />
|
||||
</div>
|
||||
<Button
|
||||
icon={faArrowRight}
|
||||
iconFlip={true}
|
||||
variant="link"
|
||||
label="Use"
|
||||
onClick={handleCardClick}
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faPlayCircle} className="card-video-play" />
|
||||
<div className="card-img-overlay-text">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
background: rgba(lighten($secondary, 10%), 0.9);
|
||||
}
|
||||
|
||||
&:hover .favorite-number {
|
||||
&:hover .favorite-text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
color: map-get($badge-colors, "yellow");
|
||||
}
|
||||
|
||||
.favorite-number {
|
||||
.favorite-text {
|
||||
color: $white;
|
||||
font-size: 14px;
|
||||
font-weight: 400 !important;
|
||||
|
||||
@@ -5,12 +5,12 @@ import { faBookmark as faBookmarkOutline } from "@fortawesome/pro-regular-svg-ic
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import "./FavoriteButton.scss";
|
||||
import useShortenNumber from "hooks/useShortenNumber";
|
||||
// import useShortenNumber from "hooks/useShortenNumber";
|
||||
|
||||
interface FavoriteButtonProps {
|
||||
initialToggled?: boolean;
|
||||
onToggle: (toggled: boolean) => Promise<void>;
|
||||
favoriteCount: number;
|
||||
favoriteCount?: number;
|
||||
overlay?: boolean;
|
||||
large?: boolean;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export default function FavoriteButton({
|
||||
const buttonShadow = overlay ? "shadow" : "";
|
||||
const iconClass = isToggled ? "icon-toggled" : "icon-default";
|
||||
const toolTip = isToggled ? "Unbookmark" : "Bookmark";
|
||||
let favoriteCountShort = useShortenNumber(favoriteCount);
|
||||
// let favoriteCountShort = useShortenNumber(favoriteCount || 0);
|
||||
|
||||
return (
|
||||
<div className="d-flex gap-2" onClick={handleClick}>
|
||||
@@ -52,6 +52,7 @@ export default function FavoriteButton({
|
||||
trigger="mouseenter"
|
||||
delay={[500, 0]}
|
||||
offset={[0, 12]}
|
||||
placement="bottom"
|
||||
>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
@@ -62,9 +63,7 @@ export default function FavoriteButton({
|
||||
icon={isToggled ? faBookmark : faBookmarkOutline}
|
||||
className={`${iconClass} me-2`}
|
||||
/>
|
||||
{favoriteCount && (
|
||||
<p className="favorite-number">{favoriteCountShort}</p>
|
||||
)}
|
||||
<p className="favorite-text">{isToggled ? "Saved" : "Save"}</p>
|
||||
</button>
|
||||
</Tippy>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,18 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Input({ label, icon, textArea, ...rest }: InputProps) {
|
||||
export default function Input({
|
||||
label,
|
||||
icon,
|
||||
textArea,
|
||||
className,
|
||||
...rest
|
||||
}: InputProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div className={`form-group ${icon ? "input-icon" : ""}`}>
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function MasonryGrid({
|
||||
masonryInstance = new Masonry(gridRef.current, {
|
||||
itemSelector: ".grid-item",
|
||||
percentPosition: true,
|
||||
transitionDuration: 0,
|
||||
});
|
||||
|
||||
//Needs this or images will overflow masonry card. It checks if image is loaded then after it is loaded, it resizes the card.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.search-bar-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
.search-field-group {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
.form-control {
|
||||
padding-top: 7.25px !important;
|
||||
padding-bottom: 7.25px !important;
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($white, 0.3) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-dropdown {
|
||||
position: absolute;
|
||||
border-color: lighten($panel-inner, 6%) !important;
|
||||
border: 1px solid;
|
||||
top: 46px;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
|
||||
.search-results-dropdown-item {
|
||||
background-color: $panel-inner;
|
||||
border-bottom: 1px solid lighten($panel-inner, 6%);
|
||||
transition: 0.05s;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($panel-inner, 6%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.view-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: $primary;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar-button {
|
||||
border-radius: 0 0.5rem 0.5rem 0 !important;
|
||||
border-color: lighten($secondary, 5%) !important;
|
||||
background-color: lighten($secondary, 5%) !important;
|
||||
|
||||
&:hover {
|
||||
border-color: lighten($secondary, 10%) !important;
|
||||
background-color: lighten($secondary, 10%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: rgba($white, 0.5);
|
||||
font-size: 15px;
|
||||
|
||||
&:hover {
|
||||
background-color: $panel-inner !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-results {
|
||||
&:hover {
|
||||
background-color: $panel-inner !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.search-bar-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
.form-control {
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import Button from "../Button";
|
||||
import { faSearch } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
SearchTtsModels,
|
||||
TtsModel,
|
||||
} from "@storyteller/components/src/api/tts/SearchTtsModels";
|
||||
import SearchResultsDropdown from "./SearchResultsDropdown";
|
||||
import SearchField from "./SearchField";
|
||||
import "./SearchBar.scss";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useSearch } from "context/SearchContext";
|
||||
|
||||
interface SearchBarProps {
|
||||
autoFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
isFocused?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchBar({
|
||||
autoFocus,
|
||||
onBlur,
|
||||
onFocus,
|
||||
isFocused,
|
||||
}: SearchBarProps) {
|
||||
let history = useHistory();
|
||||
let location = useLocation();
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearch();
|
||||
const [foundTtsModels, setFoundTtsModels] = useState<TtsModel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const isOnSearchPage = location.pathname.startsWith("/search");
|
||||
|
||||
const maybeSearch = useCallback(
|
||||
async (value: string) => {
|
||||
setSearchTerm(value);
|
||||
},
|
||||
[setSearchTerm]
|
||||
);
|
||||
|
||||
const doSearch = useCallback(
|
||||
async (value: string) => {
|
||||
let request = {
|
||||
search_term: value,
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
let response = await SearchTtsModels(request);
|
||||
|
||||
if (response.success) {
|
||||
let models = [...response.models];
|
||||
setFoundTtsModels(models);
|
||||
} else {
|
||||
setFoundTtsModels([]);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setFoundTtsModels]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnSearchPage) {
|
||||
const query = new URLSearchParams(location.search).get("query");
|
||||
if (query) {
|
||||
setSearchTerm(query);
|
||||
}
|
||||
}
|
||||
}, [isOnSearchPage, location.search, setSearchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnSearchPage) {
|
||||
history.push(`/search/weights?query=${encodeURIComponent(searchTerm)}`);
|
||||
} else {
|
||||
doSearch(searchTerm);
|
||||
}
|
||||
}, [searchTerm, history, location.pathname, doSearch, isOnSearchPage]);
|
||||
|
||||
const handleSearchButtonClick = useCallback(() => {
|
||||
if (searchTerm !== "") {
|
||||
history.push(`/search/weights?query=${encodeURIComponent(searchTerm)}`);
|
||||
}
|
||||
}, [searchTerm, history]);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearchButtonClick();
|
||||
}
|
||||
},
|
||||
[handleSearchButtonClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="search-bar-container">
|
||||
<div className="search-field-group">
|
||||
<SearchField
|
||||
value={searchTerm}
|
||||
onChange={maybeSearch}
|
||||
onKeyPress={handleKeyPress}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
{isFocused && !isOnSearchPage && (
|
||||
<SearchResultsDropdown
|
||||
data={foundTtsModels}
|
||||
isNoResults={foundTtsModels.length === 0 && searchTerm !== ""}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={faSearch}
|
||||
onClick={handleSearchButtonClick}
|
||||
variant="secondary"
|
||||
className="search-bar-button d-none d-lg-flex"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import Input from "../Input";
|
||||
|
||||
interface SearchFieldProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
autoFocus?: boolean;
|
||||
onKeyPress?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function SearchField({
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyPress,
|
||||
autoFocus = false,
|
||||
}: SearchFieldProps) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder="Search for a model weight"
|
||||
className="search-field"
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
autoFocus={autoFocus}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TtsModel } from "@storyteller/components/src/api/tts/SearchTtsModels";
|
||||
import React from "react";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import Badge from "../Badge";
|
||||
|
||||
interface SearchResultsDropdownProps {
|
||||
data: TtsModel[];
|
||||
isNoResults?: boolean;
|
||||
isLoading?: boolean;
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export default function SearchResultsDropdown({
|
||||
data,
|
||||
isNoResults,
|
||||
isLoading,
|
||||
searchTerm,
|
||||
}: SearchResultsDropdownProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleResultClick = (item: TtsModel, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
history.push(`/weight/${item.model_token}`);
|
||||
};
|
||||
|
||||
const handleInnerClick = (event: any) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const handleUrlPush = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
if (searchTerm) {
|
||||
history.push(`/search/weights?query=${encodeURIComponent(searchTerm)}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.length !== 0 && (
|
||||
<div className="search-results-dropdown">
|
||||
{data.map(item => {
|
||||
// let modelPageLink = `/weight/${item.model_token}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="search-results-dropdown-item p-3"
|
||||
key={item.model_token}
|
||||
onClick={event => handleResultClick(item, event)}
|
||||
>
|
||||
<h6 className="fw-semibold mb-1">{item.title}</h6>
|
||||
<div className="d-flex gap-2 align-items-center">
|
||||
<p className="fs-7">
|
||||
by{" "}
|
||||
<Link
|
||||
className="fw-medium"
|
||||
to={`/profile/${item.creator_username}`}
|
||||
onClick={handleInnerClick}
|
||||
>
|
||||
{item.creator_display_name}
|
||||
</Link>
|
||||
</p>
|
||||
<Badge label={"TTS"} color={"ultramarine"} small={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className="search-results-dropdown-item view-more p-3"
|
||||
onClick={handleUrlPush}
|
||||
>
|
||||
View more results
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data.length === 0 && !isLoading && isNoResults && (
|
||||
<div className="search-results-dropdown">
|
||||
<div className="search-results-dropdown-item p-3 no-results">
|
||||
No results found
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && isNoResults && (
|
||||
<div className="search-results-dropdown">
|
||||
<div className="search-results-dropdown-item p-3 loading-results">
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="spinner-border spinner-border-md opacity-75"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SearchBar";
|
||||
@@ -1,9 +1,5 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.mobile-menu-container {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
|
||||
@@ -27,7 +27,11 @@ export default function MobileMenu(props: MobileMenuProps) {
|
||||
|
||||
let signupOrProfileButton = (
|
||||
<>
|
||||
<Button label="Sign Up" small onClick={() => history.push("/signup")} />
|
||||
<Button
|
||||
label="Sign Up"
|
||||
small={true}
|
||||
onClick={() => history.push("/signup")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ interface PageHeaderProps {
|
||||
back?: BackConfig;
|
||||
titleIcon?: IconDefinition;
|
||||
title: string | React.ReactNode;
|
||||
subText: string | React.ReactNode;
|
||||
titleH2?: boolean;
|
||||
subText?: string | React.ReactNode;
|
||||
full?: boolean;
|
||||
showButton?: boolean;
|
||||
extension?: React.ReactNode;
|
||||
@@ -42,6 +43,7 @@ export default function PageHeader({
|
||||
back,
|
||||
titleIcon,
|
||||
title,
|
||||
titleH2 = false,
|
||||
subText,
|
||||
full,
|
||||
showButton,
|
||||
@@ -94,7 +96,7 @@ export default function PageHeader({
|
||||
|
||||
if (!panel) {
|
||||
return (
|
||||
<div className="py-3">
|
||||
<div className="py-4">
|
||||
<Panel clear={true}>
|
||||
{(back || showBackButton) && (
|
||||
<div className="d-flex my-2 my-lg-3">
|
||||
@@ -108,13 +110,18 @@ export default function PageHeader({
|
||||
<div
|
||||
className={`d-flex flex-column ${
|
||||
imageUrl ? "col-lg-7" : "col-12"
|
||||
} justify-content-center gap-4 py-2`}
|
||||
} justify-content-center gap-4`}
|
||||
>
|
||||
<div>
|
||||
<h1 className="fw-bold">
|
||||
{/* {icon} */}
|
||||
{title}
|
||||
</h1>
|
||||
{titleH2 ? (
|
||||
<h2 className="fw-bold">{title}</h2>
|
||||
) : (
|
||||
<h1 className="fw-bold">
|
||||
{/* {icon} */}
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
<p className={typeof subText === "string" ? "opacity-75" : ""}>
|
||||
{subText}
|
||||
</p>
|
||||
@@ -141,7 +148,7 @@ export default function PageHeader({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{extension && <div>{extension}</div>}
|
||||
{extension && <div className="mt-2">{extension}</div>}
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,26 +5,28 @@ import {
|
||||
faTrophy,
|
||||
faWaveformLines,
|
||||
faStar,
|
||||
faUser,
|
||||
faSignOutAlt,
|
||||
faFaceViewfinder,
|
||||
faCloudUpload,
|
||||
faWandMagicSparkles,
|
||||
faHome,
|
||||
faCompass,
|
||||
faUser,
|
||||
faSignOutAlt,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Button from "components/common/Button/Button";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, NavLink, useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { NavLink, useHistory } from "react-router-dom";
|
||||
import {
|
||||
GetQueueStats,
|
||||
GetQueueStatsIsOk,
|
||||
GetQueueStatsSuccessResponse,
|
||||
} from "@storyteller/components/src/api/stats/queues/GetQueueStats";
|
||||
import { WebUrl } from "common/WebUrl";
|
||||
import { Logout } from "@storyteller/components/src/api/session/Logout";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { FakeYouFrontendEnvironment } from "@storyteller/components/src/env/FakeYouFrontendEnvironment";
|
||||
import { useLocalize } from "hooks";
|
||||
import { Logout } from "@storyteller/components/src/api/session/Logout";
|
||||
import { Button } from "components/common";
|
||||
import { WebUrl } from "common/WebUrl";
|
||||
|
||||
const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000;
|
||||
|
||||
@@ -35,19 +37,28 @@ interface SideNavProps {
|
||||
querySessionSubscriptionsCallback: () => void;
|
||||
}
|
||||
|
||||
export default function SideNav(props: SideNavProps) {
|
||||
export default function SideNav({
|
||||
sessionWrapper,
|
||||
querySessionCallback,
|
||||
querySessionSubscriptionsCallback,
|
||||
}: SideNavProps) {
|
||||
const { t } = useLocalize("SideNav");
|
||||
let history = useHistory();
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const fakeYouFrontendEnv = FakeYouFrontendEnvironment.getInstance();
|
||||
const fakeYouFrontendEnv = FakeYouFrontendEnvironment.getInstance();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const isDevelopmentEnv = fakeYouFrontendEnv.isDevelopment()
|
||||
const handleNavLinkClick = () => {
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const isDevelopmentEnv = fakeYouFrontendEnv.isDevelopment();
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const isMenuOpen = wrapper?.classList.contains("toggled");
|
||||
|
||||
let history = useHistory();
|
||||
const handleNavLinkClick = () => {
|
||||
if (window.innerWidth < 992) {
|
||||
if (wrapper) {
|
||||
wrapper.classList.toggle("toggled");
|
||||
// Dispatch the event here after toggling the class (Links with the TopNav menu icon change)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("menuToggle", { detail: { isOpen: isMenuOpen } })
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -73,11 +84,6 @@ export default function SideNav(props: SideNavProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const matchHome = useRouteMatch({
|
||||
path: "/",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Update window width on resize
|
||||
const handleResize = () => {
|
||||
@@ -92,19 +98,17 @@ export default function SideNav(props: SideNavProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const shouldShowSidebar = windowWidth >= 992 && matchHome;
|
||||
const shouldShowSidebar = windowWidth >= 992;
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const contentWrapper = document.getElementById("page-content-wrapper");
|
||||
|
||||
if (windowWidth >= 992) {
|
||||
if (matchHome) {
|
||||
wrapper?.classList.add("no-padding");
|
||||
} else {
|
||||
wrapper?.classList.remove("no-padding");
|
||||
}
|
||||
contentWrapper?.classList.remove("no-padding");
|
||||
} else {
|
||||
contentWrapper?.classList.add("no-padding");
|
||||
}
|
||||
}, [matchHome, windowWidth]);
|
||||
}, [windowWidth]);
|
||||
|
||||
const [queueStats, setQueueStats] = useState<GetQueueStatsSuccessResponse>({
|
||||
success: true,
|
||||
@@ -148,24 +152,23 @@ export default function SideNav(props: SideNavProps) {
|
||||
|
||||
const logoutHandler = async () => {
|
||||
await Logout();
|
||||
props.querySessionCallback();
|
||||
props.querySessionSubscriptionsCallback();
|
||||
querySessionCallback();
|
||||
querySessionSubscriptionsCallback();
|
||||
// PosthogClient.reset();
|
||||
// Analytics.accountLogout();
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
const loggedIn = props.sessionWrapper.isLoggedIn();
|
||||
const loggedIn = sessionWrapper.isLoggedIn();
|
||||
|
||||
let userOrLoginButton = (
|
||||
<>
|
||||
<Button
|
||||
label={t("loginButton")}
|
||||
label="Login"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
handleNavLinkClick();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -174,18 +177,17 @@ export default function SideNav(props: SideNavProps) {
|
||||
let signupOrLogOutButton = (
|
||||
<>
|
||||
<Button
|
||||
label={t("signUpButton")}
|
||||
label="Sign Up"
|
||||
small
|
||||
onClick={() => {
|
||||
history.push("/signup");
|
||||
handleNavLinkClick();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (loggedIn) {
|
||||
let displayName = props.sessionWrapper.getDisplayName();
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
// let gravatarHash = props.sessionWrapper.getEmailGravatarHash();
|
||||
// let gravatar = <span />;
|
||||
|
||||
@@ -198,12 +200,11 @@ export default function SideNav(props: SideNavProps) {
|
||||
<>
|
||||
<Button
|
||||
icon={faUser}
|
||||
label={t("profileButton")}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push(url);
|
||||
handleNavLinkClick();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -213,189 +214,237 @@ export default function SideNav(props: SideNavProps) {
|
||||
<>
|
||||
<Button
|
||||
icon={faSignOutAlt}
|
||||
label={t("logOutButton")}
|
||||
label="Logout"
|
||||
small
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
handleNavLinkClick();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessionWrapper.isLoggedIn()) {
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
if (displayName === undefined) {
|
||||
displayName = "My Account";
|
||||
}
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
userOrLoginButton = (
|
||||
<Button
|
||||
icon={faUser}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => history.push(url)}
|
||||
className="d-block d-lg-none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// NB(bt,2023-11-28): These are representative of tacotron2 jobs handled by two queueing systems:
|
||||
// The legacy queue (tts-inference-job) and the modern queue (inference-job). We'll add both totals
|
||||
// while we migrate off of the legacy system, then eventually kill the legacy statistic.
|
||||
const ttsQueuedCount = queueStats.legacy_tts.pending_job_count + queueStats.inference.by_queue.pending_tacotron2_jobs;
|
||||
const ttsQueuedCount =
|
||||
queueStats.legacy_tts.pending_job_count +
|
||||
queueStats.inference.by_queue.pending_tacotron2_jobs;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="sidebar-wrapper"
|
||||
className={`sidebar ${shouldShowSidebar ? "" : "visible"}`}
|
||||
>
|
||||
<ul className="sidebar-nav">
|
||||
<div className="sidebar-brand">
|
||||
<Link to="/" onClick={handleNavLinkClick}>
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
height="36"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="sidebar-buttons d-flex gap-2 mt-4">
|
||||
{userOrLoginButton}
|
||||
{signupOrLogOutButton}
|
||||
</div>
|
||||
<hr className="my-4" />
|
||||
<>
|
||||
<div
|
||||
id="sidebar-wrapper"
|
||||
className={`sidebar ${shouldShowSidebar ? "visible" : ""}`}
|
||||
>
|
||||
<div>
|
||||
<ul className="sidebar-nav">
|
||||
<li>
|
||||
<NavLink
|
||||
exact={true}
|
||||
to="/"
|
||||
activeClassName="active-link"
|
||||
onClick={handleNavLinkClick}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faHome}
|
||||
className="sidebar-heading-icon"
|
||||
/>
|
||||
Home
|
||||
</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>
|
||||
<hr className="mb-3 mt-3" />
|
||||
<li className="sidebar-heading">AI Tools</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>
|
||||
<NavLink
|
||||
to="/face-animator"
|
||||
activeClassName="active-link"
|
||||
onClick={handleNavLinkClick}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFaceViewfinder}
|
||||
className="sidebar-heading-icon"
|
||||
/>
|
||||
{t("videoFaceAnimator")}
|
||||
</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>
|
||||
<hr className="mb-4 mt-3" />
|
||||
<li className="sidebar-heading">{t("videoTitle")}</li>
|
||||
<li>
|
||||
<NavLink
|
||||
to="/face-animator"
|
||||
activeClassName="active-link"
|
||||
onClick={handleNavLinkClick}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFaceViewfinder}
|
||||
className="sidebar-heading-icon"
|
||||
/>
|
||||
{t("videoFaceAnimator")}
|
||||
</NavLink>
|
||||
</li>
|
||||
<hr className="mb-3 mt-3" />
|
||||
<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>
|
||||
<a
|
||||
href="https://discord.gg/fakeyou"
|
||||
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>
|
||||
|
||||
<hr className="mb-4 mt-3" />
|
||||
<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>
|
||||
<a href="https://discord.gg/fakeyou" 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>
|
||||
<hr className="mb-3 mt-3" />
|
||||
<li>
|
||||
<NavLink
|
||||
to="/pricing"
|
||||
activeClassName="active-link"
|
||||
onClick={handleNavLinkClick}
|
||||
>
|
||||
<FontAwesomeIcon icon={faStar} className="sidebar-heading-icon" />
|
||||
{t("infoPricing")}
|
||||
</NavLink>
|
||||
</li>
|
||||
<hr className="mb-4 mt-3" />
|
||||
<li className="sidebar-heading">{t("queueTitle")}</li>
|
||||
<li className="ps-4 fs-7 mb-5">
|
||||
<div>
|
||||
{t("queueTts")}:{" "}
|
||||
<span className="text-red">
|
||||
{ttsQueuedCount}
|
||||
</span>
|
||||
<div className="mobile-fixed-bottom">
|
||||
<div className="sidebar-heading">Jobs Queue</div>
|
||||
<div className="ps-4">
|
||||
<div>
|
||||
{t("queueTts")}:{" "}
|
||||
<span className="text-red">{ttsQueuedCount}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t("queueRvc")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_rvc_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{t("queueSvc")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_svc_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{t("queueFaceAnimator")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_face_animation_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Voice Designer:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_voice_designer}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{t("queueRvc")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_rvc_jobs}
|
||||
</span>
|
||||
|
||||
<div className="px-4 d-flex d-lg-none gap-2 mt-3">
|
||||
{userOrLoginButton}
|
||||
{signupOrLogOutButton}
|
||||
</div>
|
||||
<div>
|
||||
{t("queueSvc")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_svc_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{t("queueFaceAnimator")}:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_face_animation_jobs}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Voice Designer:{" "}
|
||||
<span className="text-red">
|
||||
{queueStats.inference.by_queue.pending_voice_designer}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
faBars,
|
||||
faSearch,
|
||||
faSignOutAlt,
|
||||
faUser,
|
||||
faXmark,
|
||||
} 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 { WebUrl } from "common/WebUrl";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Logout } from "@storyteller/components/src/api/session/Logout";
|
||||
|
||||
interface TopNavProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
logoutHandler: () => void;
|
||||
querySessionCallback: () => void;
|
||||
querySessionSubscriptionsCallback: () => void;
|
||||
}
|
||||
|
||||
export default function TopNav({
|
||||
sessionWrapper,
|
||||
querySessionCallback,
|
||||
querySessionSubscriptionsCallback,
|
||||
}: TopNavProps) {
|
||||
let history = useHistory();
|
||||
const [isMobileSearchBarVisible, setIsMobileSearchBarVisible] =
|
||||
useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const wrapper = document.getElementById("wrapper");
|
||||
const [menuButtonIcon, setMenuButtonIcon] = useState(faBars);
|
||||
|
||||
const handleMenuButtonClick = () => {
|
||||
if (window.innerWidth < 1200) {
|
||||
if (wrapper) {
|
||||
wrapper.classList.toggle("toggled");
|
||||
if (menuButtonIcon === faBars) {
|
||||
setMenuButtonIcon(faXmark);
|
||||
} else {
|
||||
setMenuButtonIcon(faBars);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchButtonClick = () => {
|
||||
setIsMobileSearchBarVisible(true);
|
||||
setMenuButtonIcon(faBars);
|
||||
if (window.innerWidth < 1200) {
|
||||
if (wrapper) {
|
||||
wrapper.classList.remove("toggled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onFocusHandler = () => {
|
||||
setIsFocused(true);
|
||||
};
|
||||
|
||||
const onBlurHandler = () => {
|
||||
// Search field blur/Unfocusing hack: needs a little bit of delay for the result click event to register
|
||||
setTimeout(() => {
|
||||
setIsFocused(false);
|
||||
|
||||
if (isMobileSearchBarVisible) {
|
||||
setIsMobileSearchBarVisible(false);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleMenuToggle = (event: any) => {
|
||||
setMenuButtonIcon(event.detail.isOpen ? faXmark : faBars);
|
||||
};
|
||||
|
||||
window.addEventListener("menuToggle", handleMenuToggle);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("menuToggle", handleMenuToggle);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const logoutHandler = async () => {
|
||||
await Logout();
|
||||
querySessionCallback();
|
||||
querySessionSubscriptionsCallback();
|
||||
// PosthogClient.reset();
|
||||
// Analytics.accountLogout();
|
||||
history.push("/");
|
||||
};
|
||||
|
||||
const loggedIn = sessionWrapper.isLoggedIn();
|
||||
|
||||
let userOrLoginButton = (
|
||||
<>
|
||||
<Button
|
||||
label="Login"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
let signupOrLogOutButton = (
|
||||
<>
|
||||
<Button
|
||||
label="Sign Up"
|
||||
small
|
||||
onClick={() => {
|
||||
history.push("/signup");
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (loggedIn) {
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
// let gravatarHash = props.sessionWrapper.getEmailGravatarHash();
|
||||
// let gravatar = <span />;
|
||||
|
||||
if (displayName === undefined) {
|
||||
displayName = "My Account";
|
||||
}
|
||||
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
userOrLoginButton = (
|
||||
<>
|
||||
<Button
|
||||
icon={faUser}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push(url);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
signupOrLogOutButton = (
|
||||
<>
|
||||
<Button
|
||||
icon={faSignOutAlt}
|
||||
label="Logout"
|
||||
small
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessionWrapper.isLoggedIn()) {
|
||||
let displayName = sessionWrapper.getDisplayName();
|
||||
if (displayName === undefined) {
|
||||
displayName = "My Account";
|
||||
}
|
||||
let url = WebUrl.userProfilePage(displayName);
|
||||
userOrLoginButton = (
|
||||
<Button
|
||||
icon={faUser}
|
||||
label="My Profile"
|
||||
small
|
||||
variant="secondary"
|
||||
onClick={() => history.push(url)}
|
||||
className="d-none d-lg-block"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="topbar-wrapper" className="position-fixed">
|
||||
<div className="topbar-nav">
|
||||
<div className="topbar-nav-left">
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
height="34"
|
||||
className="mb-2 d-none d-lg-block"
|
||||
/>
|
||||
<img
|
||||
src="/fakeyou/FakeYou-Logo-Mobile.png"
|
||||
alt="FakeYou: Cartoon and Celebrity Text to Speech"
|
||||
height="36"
|
||||
className="mb-0 d-block d-lg-none"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="topbar-nav-center">
|
||||
{/* Search Bar */}
|
||||
<div className="d-none d-lg-block">
|
||||
<SearchBar
|
||||
onFocus={onFocusHandler}
|
||||
onBlur={onBlurHandler}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="topbar-nav-right">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<div className="d-none d-lg-flex gap-2">
|
||||
{userOrLoginButton}
|
||||
{signupOrLogOutButton}
|
||||
</div>
|
||||
<Button
|
||||
icon={faSearch}
|
||||
variant="secondary"
|
||||
small={true}
|
||||
square={true}
|
||||
onClick={handleSearchButtonClick}
|
||||
className="d-lg-none"
|
||||
/>
|
||||
<Button
|
||||
icon={menuButtonIcon}
|
||||
variant="secondary"
|
||||
small={true}
|
||||
square={true}
|
||||
onClick={handleMenuButtonClick}
|
||||
className="d-lg-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Searchbar */}
|
||||
{isMobileSearchBarVisible && (
|
||||
<div className="topbar-mobile-search-bar-container">
|
||||
<div className="topbar-mobile-search-bar">
|
||||
<SearchBar
|
||||
onFocus={onFocusHandler}
|
||||
onBlur={onBlurHandler}
|
||||
isFocused={isFocused}
|
||||
autoFocus={true}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon={faXmark}
|
||||
className="close-search-button"
|
||||
onClick={() => {
|
||||
setIsMobileSearchBarVisible(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
|
||||
interface SearchContextType {
|
||||
searchTerm: string;
|
||||
setSearchTerm: (value: string) => void;
|
||||
}
|
||||
|
||||
const SearchContext = createContext<SearchContextType>({
|
||||
searchTerm: "",
|
||||
setSearchTerm: () => {},
|
||||
});
|
||||
|
||||
export const useSearch = () => useContext(SearchContext);
|
||||
|
||||
export const SearchProvider: React.FC = ({ children }) => {
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={{ searchTerm, setSearchTerm }}>
|
||||
{children}
|
||||
</SearchContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import InferenceJobsContext from "./InferenceJobsContext";
|
||||
import SessionContext from "./SessionContext";
|
||||
|
||||
export { InferenceJobsContext, SessionContext };
|
||||
export { InferenceJobsContext, SessionContext };
|
||||
@@ -1,15 +1,25 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "hooks";
|
||||
|
||||
interface Props {
|
||||
addQueries?: any;
|
||||
fetcher: any;
|
||||
list: any;
|
||||
listSet: any;
|
||||
pagePreset?: number;
|
||||
requestList?: boolean;
|
||||
isInfiniteScroll?: boolean;
|
||||
}
|
||||
|
||||
export default function useListContent({ fetcher, list, listSet, pagePreset = 0, requestList = false }: Props) {
|
||||
export default function useListContent({
|
||||
addQueries,
|
||||
fetcher,
|
||||
list,
|
||||
listSet,
|
||||
pagePreset = 0,
|
||||
requestList = false,
|
||||
isInfiniteScroll = false,
|
||||
}: Props) {
|
||||
const { user } = useSession();
|
||||
const [filter, filterSet] = useState("all");
|
||||
const [page, pageSet] = useState(pagePreset);
|
||||
@@ -19,12 +29,17 @@ export default function useListContent({ fetcher, list, listSet, pagePreset = 0,
|
||||
|
||||
const pageChange = (page: number) => {
|
||||
pageSet(page);
|
||||
if (!isInfiniteScroll || page === 1) {
|
||||
listSet([]); // Reset list if not in infinite scroll mode or starting from first page
|
||||
}
|
||||
statusSet(1);
|
||||
};
|
||||
|
||||
const onChange = ({ target }: { target: { name: string, value: any} }) => {
|
||||
const todo: { [ key: string]: (x: any) => void } = { filterSet, sortSet };
|
||||
const onChange = ({ target }: { target: { name: string; value: any } }) => {
|
||||
const todo: { [key: string]: (x: any) => void } = { filterSet, sortSet };
|
||||
todo[target.name + "Set"](target.value);
|
||||
pageSet(1); // Reset to first page on filter/sort change
|
||||
listSet([]); // Reset list on filter/sort change
|
||||
statusSet(1);
|
||||
};
|
||||
|
||||
@@ -32,21 +47,40 @@ export default function useListContent({ fetcher, list, listSet, pagePreset = 0,
|
||||
if (user && user.username) {
|
||||
if (status === 1) {
|
||||
statusSet(2);
|
||||
fetcher(user.username, {}, {
|
||||
page_index: page,
|
||||
...filter !== "all" ? { filter_media_type: filter } : {},
|
||||
...sort !== "newest" ? { sort_ascending: true } : {},
|
||||
}).then((res: any) => {
|
||||
fetcher(
|
||||
user.username,
|
||||
{},
|
||||
{
|
||||
page_index: page,
|
||||
...addQueries, // eventually we should provide a way to type this ... or not. It works
|
||||
...(filter !== "all" ? { filter_media_type: filter } : {}),
|
||||
...(sort !== "newest" ? { sort_ascending: true } : {}),
|
||||
}
|
||||
).then((res: any) => {
|
||||
statusSet(3);
|
||||
console.log("🧶",res);
|
||||
console.log("🧶", res);
|
||||
if (res.results && res.pagination) {
|
||||
pageCountSet(res.pagination.total_page_count)
|
||||
listSet(res.results);
|
||||
pageCountSet(res.pagination.total_page_count);
|
||||
if (isInfiniteScroll && page !== 1) {
|
||||
listSet((prevList: any) => [...prevList, ...res.results]); // Append to list for infinite scroll
|
||||
} else {
|
||||
listSet(res.results); // Replace list for regular pagination
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [fetcher, filter, listSet, page, user, sort, status]);
|
||||
}, [
|
||||
addQueries,
|
||||
fetcher,
|
||||
filter,
|
||||
listSet,
|
||||
page,
|
||||
user,
|
||||
sort,
|
||||
status,
|
||||
isInfiniteScroll,
|
||||
]);
|
||||
|
||||
return {
|
||||
filter,
|
||||
@@ -61,6 +95,6 @@ export default function useListContent({ fetcher, list, listSet, pagePreset = 0,
|
||||
sort,
|
||||
sortSet,
|
||||
status,
|
||||
statusSet
|
||||
statusSet,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -10,22 +10,23 @@
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#wrapper.toggled {
|
||||
padding-left: 260px;
|
||||
}
|
||||
$topbar-height: 66px;
|
||||
|
||||
#sidebar-wrapper {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
left: 260px;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
height: calc(100% - $topbar-height);
|
||||
margin-left: -260px;
|
||||
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;
|
||||
@@ -47,43 +48,36 @@
|
||||
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
width: 260px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#page-content-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#wrapper.toggled #page-content-wrapper {
|
||||
position: absolute;
|
||||
margin-right: -260px;
|
||||
padding-left: 260px;
|
||||
padding-top: $topbar-height;
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
|
||||
.sidebar-nav {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 260px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: $panel;
|
||||
border-color: rgba(255, 255, 255, 0.0705882353) !important;
|
||||
border-right: 1px solid;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
// .sidebar-nav li {
|
||||
// text-indent: 2rem;
|
||||
// line-height: 38px;
|
||||
// }
|
||||
|
||||
.sidebar-nav li a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: rgba($white, 0.7);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
margin: 0rem 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
@@ -137,12 +131,12 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-black background */
|
||||
z-index: 900; /* Appear on top of the content */
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 900;
|
||||
}
|
||||
.toggled #page-content-wrapper {
|
||||
position: relative;
|
||||
z-index: 2; /* Ensure content is above the overlay */
|
||||
z-index: 2;
|
||||
}
|
||||
.toggled #overlay {
|
||||
display: block;
|
||||
@@ -153,16 +147,91 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
transform: translateX(
|
||||
-100%
|
||||
); /* move the sidebar out of the screen to the left */
|
||||
transition: transform 0.3s ease-out; /* transition effect */
|
||||
/* other necessary styles for the sidebar */
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Visible state for the sidebar */
|
||||
.sidebar.visible {
|
||||
transform: translateX(0); /* sidebar occupies its regular position */
|
||||
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.0705882353) !important;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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: 1.5rem 0rem;
|
||||
width: 100%;
|
||||
background-color: $panel-inner;
|
||||
border-top: 1px solid lighten($panel-inner, 5%) !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
@@ -191,3 +260,18 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
#wrapper.toggled #sidebar-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar-wrapper {
|
||||
left: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,17 @@ styles with important tags:
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: rgba($bg, 1) !important;
|
||||
background-color: $panel !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
margin-bottom: 0;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,58 @@
|
||||
import { Panel } from "components/common";
|
||||
import Container from "components/common/Container";
|
||||
import Tabs from "components/common/Tabs";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import React from "react";
|
||||
import {
|
||||
faFire,
|
||||
faLayerGroup,
|
||||
faPhotoFilmMusic,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import FeaturedTab from "./tabs/FeaturedTab";
|
||||
import WeightsTab from "./tabs/WeightsTab";
|
||||
import MediaTab from "./tabs/MediaTab";
|
||||
import { Redirect, useLocation } from "react-router-dom";
|
||||
|
||||
interface ExplorePageProps {}
|
||||
|
||||
export default function ExplorePage(props: ExplorePageProps) {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (pathname === `/explore` || pathname === `/explore/`) {
|
||||
return <Redirect to={`/explore/featured`} />;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Featured",
|
||||
icon: faFire,
|
||||
content: <FeaturedTab />,
|
||||
to: "/explore/featured",
|
||||
padding: true,
|
||||
},
|
||||
{
|
||||
label: "Weights",
|
||||
icon: faLayerGroup,
|
||||
content: <WeightsTab />,
|
||||
to: "/explore/weights",
|
||||
padding: true,
|
||||
},
|
||||
{
|
||||
label: "Media",
|
||||
icon: faPhotoFilmMusic,
|
||||
content: <MediaTab />,
|
||||
to: "/explore/media",
|
||||
padding: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader title="Explore" subText="View community created content" />
|
||||
|
||||
<Panel mb={true}>
|
||||
<Tabs tabs={tabs} />
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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 SkeletonCard from "components/common/Card/SkeletonCard";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import ImageCard from "components/common/Card/ImageCard";
|
||||
|
||||
export default function FeaturedTab() {
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [data] = useState(mockWeightsData);
|
||||
const [isLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<h3 className="fw-semibold mb-0 flex-grow-1">
|
||||
Featured Voice Weights
|
||||
</h3>
|
||||
<Link to="/explore/weights">
|
||||
View all
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ms-2" />
|
||||
</Link>
|
||||
</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 = (
|
||||
<AudioCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="weights"
|
||||
showCreator={true}
|
||||
showCover={true}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="col-12 col-lg-6 col-xxl-4 grid-item"
|
||||
>
|
||||
{card}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</MasonryGrid>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<h3 className="fw-semibold mb-0 flex-grow-1">
|
||||
Featured Image Weights
|
||||
</h3>
|
||||
<Link to="/explore/weights">
|
||||
View all
|
||||
<FontAwesomeIcon icon={faChevronRight} className="ms-2" />
|
||||
</Link>
|
||||
</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 = (
|
||||
<ImageCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="weights"
|
||||
showCreator={true}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div key={index} className="col-12 col-sm-6 col-xl-4 grid-item">
|
||||
{card}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</MasonryGrid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { useCallback, 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 { 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 { ListMediaFiles } from "@storyteller/components/src/api/media_files/ListMediaFiles";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import { useListContent } from "hooks";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
|
||||
export default function MediaTab() {
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isLoading] = useState(false);
|
||||
|
||||
const [list, listSet] = useState<MediaFile[]>([]);
|
||||
const media = useListContent({
|
||||
fetcher: ListMediaFiles,
|
||||
list,
|
||||
listSet,
|
||||
pagePreset: 1,
|
||||
requestList: true,
|
||||
isInfiniteScroll: true,
|
||||
});
|
||||
|
||||
// Fetch more data for infinite scroll
|
||||
const fetchMoreData = useCallback(() => {
|
||||
if (media.page < media.pageCount) {
|
||||
media.pageChange(media.page + 1);
|
||||
}
|
||||
}, [media]);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="d-flex flex-wrap gap-3 mb-3">
|
||||
<div className="d-flex gap-2 flex-grow-1">
|
||||
<TempSelect
|
||||
{...{
|
||||
icon: faArrowDownWideShort,
|
||||
options: sortOptions,
|
||||
name: "sort",
|
||||
onChange: media.onChange,
|
||||
value: media.sort,
|
||||
}}
|
||||
/>
|
||||
<TempSelect
|
||||
{...{
|
||||
icon: faFilter,
|
||||
options: filterOptions,
|
||||
name: "filter",
|
||||
onChange: media.onChange,
|
||||
value: media.filter,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AudioPlayerProvider>
|
||||
{isLoading ? (
|
||||
<div className="row gx-3 gy-3">
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<SkeletonCard key={index} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
dataLength={list.length}
|
||||
next={fetchMoreData}
|
||||
hasMore={media.page < media.pageCount}
|
||||
loader={
|
||||
<div className="mt-4 d-flex justify-content-center">
|
||||
<div className="spinner-border text-light" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
endMessage={
|
||||
<p className="text-center mt-4 opacity-75">No more results.</p>
|
||||
}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<MasonryGrid
|
||||
gridRef={gridContainerRef}
|
||||
onLayoutComplete={() => console.log("Layout complete!")}
|
||||
>
|
||||
{media.list.map((data: MediaFile, index: number) => {
|
||||
let card;
|
||||
switch (data.media_type) {
|
||||
case "audio":
|
||||
card = (
|
||||
<AudioCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="media"
|
||||
showCreator={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "image":
|
||||
card = (
|
||||
<ImageCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="media"
|
||||
showCreator={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "video":
|
||||
card = (
|
||||
<VideoCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="media"
|
||||
showCreator={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
card = <div>Unsupported media type</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="col-12 col-sm-6 col-xl-4 grid-item"
|
||||
>
|
||||
{card}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</MasonryGrid>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</AudioPlayerProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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 Select from "components/common/Select";
|
||||
import {
|
||||
faArrowDownWideShort,
|
||||
faFilter,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import SkeletonCard from "components/common/Card/SkeletonCard";
|
||||
|
||||
export default function WeightsTab() {
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [data] = useState(mockWeightsData);
|
||||
const [isLoading] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState("all");
|
||||
|
||||
const filterOptions = [
|
||||
{ value: "all", label: "All Weights" },
|
||||
{ value: "tts", label: "Text to Speech" },
|
||||
{ value: "vc", label: "Voice to Voice" },
|
||||
{ value: "sd", label: "Image Generation" },
|
||||
];
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "newest", label: "Newest" },
|
||||
{ value: "oldest", label: "Oldest" },
|
||||
{ value: "mostliked", label: "Most Bookmarked" },
|
||||
];
|
||||
|
||||
const modelTtsOptions = [
|
||||
{ value: "all", label: "All Types" },
|
||||
{ value: "tt2", label: "Tacotron 2" },
|
||||
];
|
||||
|
||||
const modelVcOptions = [
|
||||
{ value: "all", label: "All Types" },
|
||||
{ value: "rvc", label: "RVCv2" },
|
||||
{ value: "svc", label: "SoVitsSvc" },
|
||||
];
|
||||
|
||||
const modelSdOptions = [
|
||||
{ value: "all", label: "All Types" },
|
||||
{ value: "lora", label: "LoRA" },
|
||||
{ value: "SD15", label: "SD 1.5" },
|
||||
{ value: "SDXL", label: "SD XL" },
|
||||
];
|
||||
|
||||
const handleFilterChange = (option: any) => {
|
||||
const selectedOption = option as { value: string; label: string };
|
||||
setSelectedFilter(selectedOption.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex flex-wrap gap-3 mb-3">
|
||||
<div className="d-flex gap-2 flex-grow-1">
|
||||
<Select
|
||||
icon={faArrowDownWideShort}
|
||||
options={sortOptions}
|
||||
defaultValue={sortOptions[0]}
|
||||
isSearchable={false}
|
||||
/>
|
||||
|
||||
<Select
|
||||
icon={faFilter}
|
||||
options={filterOptions}
|
||||
defaultValue={filterOptions[0]}
|
||||
isSearchable={false}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
{selectedFilter === "tts" && (
|
||||
<Select
|
||||
options={modelTtsOptions}
|
||||
defaultValue={modelTtsOptions[0]}
|
||||
isSearchable={false}
|
||||
/>
|
||||
)}
|
||||
{selectedFilter === "sd" && (
|
||||
<Select
|
||||
options={modelSdOptions}
|
||||
defaultValue={modelSdOptions[0]}
|
||||
isSearchable={false}
|
||||
/>
|
||||
)}
|
||||
{selectedFilter === "vc" && (
|
||||
<Select
|
||||
options={modelVcOptions}
|
||||
defaultValue={modelVcOptions[0]}
|
||||
isSearchable={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</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"
|
||||
showCreator={true}
|
||||
showCover={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "image":
|
||||
card = (
|
||||
<ImageCard
|
||||
key={index}
|
||||
data={data}
|
||||
type="weights"
|
||||
showCreator={true}
|
||||
/>
|
||||
);
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
const mockMediaData = [
|
||||
{
|
||||
token: "token1",
|
||||
weight_name: "Queen Elizabeth Queen Elizabeth 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",
|
||||
maybe_creator_user: {
|
||||
user_token: "1234",
|
||||
username: "MelodyMaker123",
|
||||
display_name: "MelodyMaker",
|
||||
gravatar_hash: "1234",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "5678",
|
||||
username: "ArtisticExplorer",
|
||||
display_name: "ArtisticExplorer",
|
||||
gravatar_hash: "5678",
|
||||
default_avatar: {
|
||||
image_index: 2,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "9012",
|
||||
username: "SoccerStar90",
|
||||
display_name: "SoccerStar",
|
||||
gravatar_hash: "9012",
|
||||
default_avatar: {
|
||||
image_index: 3,
|
||||
color_index: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "3456",
|
||||
username: "DesertWanderer",
|
||||
display_name: "DesertWanderer",
|
||||
gravatar_hash: "3456",
|
||||
default_avatar: {
|
||||
image_index: 4,
|
||||
color_index: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "7890",
|
||||
username: "CatLover78",
|
||||
display_name: "CatLover",
|
||||
gravatar_hash: "7890",
|
||||
default_avatar: {
|
||||
image_index: 5,
|
||||
color_index: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "2345",
|
||||
username: "SuperheroFan23",
|
||||
display_name: "SuperheroFan",
|
||||
gravatar_hash: "2345",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "5678",
|
||||
username: "DBZFan5678",
|
||||
display_name: "DBZFan",
|
||||
gravatar_hash: "5678",
|
||||
default_avatar: {
|
||||
image_index: 2,
|
||||
color_index: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "9012",
|
||||
username: "SpaceExplorer90",
|
||||
display_name: "SpaceExplorer",
|
||||
gravatar_hash: "9012",
|
||||
default_avatar: {
|
||||
image_index: 3,
|
||||
color_index: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "1234",
|
||||
username: "MysteriousMask",
|
||||
display_name: "MysteriousMask",
|
||||
gravatar_hash: "1234",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "2345",
|
||||
username: "NatureLover23",
|
||||
display_name: "NatureLover",
|
||||
gravatar_hash: "2345",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default mockMediaData;
|
||||
@@ -0,0 +1,200 @@
|
||||
let mockWeightsData = [
|
||||
{
|
||||
token: "token1",
|
||||
weight_name:
|
||||
"Queen Elizabeth Queen Elizabeth Queen Elizabeth 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",
|
||||
maybe_creator_user: {
|
||||
user_token: "1234",
|
||||
username: "MelodyMaker123",
|
||||
display_name: "MelodyMaker",
|
||||
gravatar_hash: "abcd",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "5678",
|
||||
username: "ArtisticExplorer",
|
||||
display_name: "ArtisticExplorer",
|
||||
gravatar_hash: "efgh",
|
||||
default_avatar: {
|
||||
image_index: 2,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "9012",
|
||||
username: "SoccerStar90",
|
||||
display_name: "SoccerStar",
|
||||
gravatar_hash: "ijkl",
|
||||
default_avatar: {
|
||||
image_index: 3,
|
||||
color_index: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
token: "token4",
|
||||
weight_name: "Desert",
|
||||
media_type: "image",
|
||||
public_bucket_path: "/images/dummy-video.jpg",
|
||||
likes: 150,
|
||||
isLiked: true,
|
||||
created_at: "2023-08-10T09:00:00.000Z",
|
||||
maybe_creator_user: {
|
||||
user_token: "3456",
|
||||
username: "DesertWanderer",
|
||||
display_name: "DesertWanderer",
|
||||
gravatar_hash: "mnop",
|
||||
default_avatar: {
|
||||
image_index: 4,
|
||||
color_index: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "7890",
|
||||
username: "CatLover78",
|
||||
display_name: "CatLover",
|
||||
gravatar_hash: "qrst",
|
||||
default_avatar: {
|
||||
image_index: 5,
|
||||
color_index: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "2345",
|
||||
username: "SuperheroFan23",
|
||||
display_name: "SuperheroFan",
|
||||
gravatar_hash: "uvwx",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "5678",
|
||||
username: "DBZFan5678",
|
||||
display_name: "DBZFan",
|
||||
gravatar_hash: "yzab",
|
||||
default_avatar: {
|
||||
image_index: 2,
|
||||
color_index: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "9012",
|
||||
username: "SpaceExplorer90",
|
||||
display_name: "SpaceExplorer",
|
||||
gravatar_hash: "cdex",
|
||||
default_avatar: {
|
||||
image_index: 3,
|
||||
color_index: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "1234",
|
||||
username: "MysteriousMask",
|
||||
display_name: "MysteriousMask",
|
||||
gravatar_hash: "efgh",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
maybe_creator_user: {
|
||||
user_token: "2345",
|
||||
username: "NatureLover23",
|
||||
display_name: "NatureLover",
|
||||
gravatar_hash: "ghij",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
color_index: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default mockWeightsData;
|
||||
@@ -4,30 +4,30 @@ import AudioCard from "components/common/Card/AudioCard";
|
||||
import ImageCard from "components/common/Card/ImageCard";
|
||||
import VideoCard from "components/common/Card/VideoCard";
|
||||
import SkeletonCard from "components/common/Card/SkeletonCard";
|
||||
import mockWeightsData from "./mockWeightsData";
|
||||
import Select from "components/common/Select";
|
||||
import {
|
||||
faArrowDownWideShort,
|
||||
faFilter,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowDownWideShort, faFilter } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Pagination from "components/common/Pagination";
|
||||
|
||||
import { useListContent } from "hooks";
|
||||
import { GetBookmarksByUser } from "@storyteller/components/src/api/bookmarks/GetBookmarksByUser";
|
||||
|
||||
export default function BookmarksTab() {
|
||||
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [data] = useState(mockWeightsData);
|
||||
const [isLoading] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState("all");
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const [list, listSet] = useState<any[]>([]);
|
||||
const bookmarks = useListContent({ fetcher: GetBookmarksByUser, list, listSet, pagePreset: 1, requestList: true });
|
||||
|
||||
const handlePageClick = (selectedItem: { selected: number }) => {
|
||||
setCurrentPage(selectedItem.selected);
|
||||
bookmarks.pageChange(selectedItem.selected + 1);
|
||||
};
|
||||
|
||||
const currentItems = data.slice(
|
||||
currentPage * itemsPerPage,
|
||||
(currentPage + 1) * itemsPerPage
|
||||
);
|
||||
const paginationProps = {
|
||||
onPageChange: handlePageClick,
|
||||
pageCount: bookmarks.pageCount - 1,
|
||||
currentPage: bookmarks.page - 1
|
||||
};
|
||||
|
||||
const filterOptions = [
|
||||
{ value: "all", label: "All Weights" },
|
||||
@@ -60,11 +60,11 @@ export default function BookmarksTab() {
|
||||
{ value: "SDXL", label: "SD XL" },
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
onPageChange: handlePageClick,
|
||||
pageCount: 0, // replace with proper fecth value
|
||||
currentPage: 0 // replace with proper fecth value
|
||||
};
|
||||
// const paginationProps = {
|
||||
// onPageChange: handlePageClick,
|
||||
// pageCount: 0, // replace with proper fecth value
|
||||
// currentPage: 0 // replace with proper fecth value
|
||||
// };
|
||||
|
||||
const handleFilterChange = (option: any) => {
|
||||
const selectedOption = option as { value: string; label: string };
|
||||
@@ -125,7 +125,7 @@ export default function BookmarksTab() {
|
||||
gridRef={gridContainerRef}
|
||||
onLayoutComplete={() => console.log("Layout complete!")}
|
||||
>
|
||||
{currentItems.map((data, index) => {
|
||||
{bookmarks.list.map((data: any, index: number) => {
|
||||
let card;
|
||||
switch (data.media_type) {
|
||||
case "audio":
|
||||
|
||||
@@ -18,7 +18,14 @@ export default function MediaTab() {
|
||||
const [isLoading] = useState(false);
|
||||
|
||||
const [list, listSet] = useState<MediaFile[]>([]);
|
||||
const media = useListContent({ fetcher: GetMediaByUser, list, listSet, pagePreset: 1, requestList: true });
|
||||
const media = useListContent({
|
||||
// addQueries: { abc: "anything" }, an example
|
||||
fetcher: GetMediaByUser,
|
||||
list,
|
||||
listSet,
|
||||
pagePreset: 1,
|
||||
requestList: true
|
||||
});
|
||||
|
||||
const handlePageClick = (selectedItem: { selected: number }) => {
|
||||
media.pageChange(selectedItem.selected + 1);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import Container from "components/common/Container";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import { faClock } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Panel from "components/common/Panel";
|
||||
import ModelSearchResults from "components/common/ModelSearchResults";
|
||||
import ModelTags from "components/common/ModelTags";
|
||||
import Select from "components/common/Select";
|
||||
import {
|
||||
SearchTtsModels,
|
||||
TtsModel,
|
||||
} from "@storyteller/components/src/api/tts/SearchTtsModels";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const allTags = [
|
||||
"English",
|
||||
"Spanish",
|
||||
"Portuguese",
|
||||
"High-pitched",
|
||||
"Low-pitched",
|
||||
"Character",
|
||||
];
|
||||
|
||||
export default function SearchPage() {
|
||||
const [foundTtsModels, setFoundTtsModels] = useState<TtsModel[]>([]);
|
||||
|
||||
const useQuery = () => {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
};
|
||||
|
||||
const doSearch = useCallback(
|
||||
async (value: string) => {
|
||||
let request = {
|
||||
search_term: value,
|
||||
};
|
||||
|
||||
let response = await SearchTtsModels(request);
|
||||
|
||||
if (response.success) {
|
||||
let models = [...response.models];
|
||||
setFoundTtsModels(models);
|
||||
} else {
|
||||
setFoundTtsModels([]);
|
||||
}
|
||||
},
|
||||
[setFoundTtsModels]
|
||||
);
|
||||
|
||||
const query = useQuery();
|
||||
const urlSearchTerm = query.get("query") || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (urlSearchTerm) {
|
||||
doSearch(urlSearchTerm);
|
||||
}
|
||||
}, [urlSearchTerm, doSearch]);
|
||||
|
||||
let selectedTags: any = [];
|
||||
let handleSelectTag = () => {};
|
||||
|
||||
const tags = (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<ModelTags
|
||||
tags={allTags}
|
||||
selectedTags={selectedTags}
|
||||
onSelectTag={handleSelectTag}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "most liked", label: "Most Liked" },
|
||||
{ value: "most used", label: "Most Used" },
|
||||
{ value: "moset recent", label: "Most Recent" },
|
||||
];
|
||||
const sortTimeOptions = [
|
||||
{ value: "all time", label: "All Time" },
|
||||
{ value: "today", label: "Today" },
|
||||
{ value: "this week", label: "This Week" },
|
||||
{ value: "this month", label: "This Month" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container type="panel" className="mb-5">
|
||||
<PageHeader
|
||||
title={`${foundTtsModels.length || "0"} results for "${urlSearchTerm}"`}
|
||||
titleH2={true}
|
||||
extension={tags}
|
||||
panel={false}
|
||||
/>
|
||||
<Panel padding={true}>
|
||||
<div className="d-flex gap-2 mb-4">
|
||||
<Select
|
||||
small={true}
|
||||
options={sortOptions}
|
||||
defaultValue={sortOptions[0]}
|
||||
/>
|
||||
<Select
|
||||
small={true}
|
||||
icon={faClock}
|
||||
options={sortTimeOptions}
|
||||
defaultValue={sortTimeOptions[0]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<ModelSearchResults data={filteredData} />*/}
|
||||
<ModelSearchResults data={foundTtsModels} />
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user