Merge pull request #432 from storytold/bflat-fy-explore-search-rebased

explore and search bar
This commit is contained in:
BFlat authored and GitHub committed 2023-12-22 00:20:05 +07:00
commit 3ed181cf45
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 -1
View File
@@ -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,
};
};
}
+110 -26
View File
@@ -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>
);
}