mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-11 20:32:12 +00:00
Feature/video media page (#570)
* video page initial progress * added MediaPageSwitch --------- Co-authored-by: Victor <victor@storyteller.ai>
This commit is contained in:
1 parent
fd0164ea6f
commit
8713426256
32 files changed
+1633
-565
No files matched your search
@@ -0,0 +1,83 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-action-button {
|
||||
appearance: none;
|
||||
background-color: rgba(lighten($secondary, 5%), 0.9);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
align-items: center;
|
||||
// background: red;
|
||||
line-height: 1;
|
||||
font-size: 0.875rem;
|
||||
text-align: right;
|
||||
min-height: 2.25rem;
|
||||
min-width: 1rem;
|
||||
position: relative;
|
||||
padding: 0 0 0 0.5rem;
|
||||
// text-align: right;
|
||||
|
||||
&:hover {
|
||||
background: rgba(lighten($secondary, 10%), 0.9);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1em;
|
||||
color: $primary;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.fy-action-button-icon-toggled {
|
||||
animation: pulse 0.3s ease;
|
||||
}
|
||||
}
|
||||
.bookmark-action-button {
|
||||
svg {
|
||||
color: map-get($badge-colors, "yellow");
|
||||
}
|
||||
&.action-button-toggled {
|
||||
background-color: darken(rgba(map-get($badge-colors, "yellow"), 0.9), 32%);
|
||||
|
||||
&:hover {
|
||||
background-color: darken(
|
||||
rgba(map-get($badge-colors, "yellow"), 0.9),
|
||||
25%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-workdots-label {
|
||||
min-width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-action-button {
|
||||
svg {
|
||||
color: map-get($badge-colors, "purple");
|
||||
}
|
||||
&.action-button-toggled {
|
||||
background-color: darken(rgba(map-get($badge-colors, "purple"), 0.9), 32%);
|
||||
|
||||
&:hover {
|
||||
background-color: darken(
|
||||
rgba(map-get($badge-colors, "purple"), 0.9),
|
||||
25%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.like-action-button {
|
||||
svg {
|
||||
color: $primary;
|
||||
}
|
||||
&.action-button-toggled {
|
||||
background-color: rgba(#593a48, 0.9);
|
||||
|
||||
&:hover {
|
||||
background-color: lighten(rgba(#593a48, 0.9), 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimationStatus, useAnimationStatus } from "hooks";
|
||||
import { WorkDots } from "components/svg";
|
||||
import "./ActionButton.scss";
|
||||
|
||||
export interface ActionButtonProps {
|
||||
actionType: "bookmark" | "feature" | "like";
|
||||
busy?: boolean;
|
||||
entityToken?: string;
|
||||
entityType?: string;
|
||||
iconOff: IconDefinition;
|
||||
iconOn: IconDefinition;
|
||||
isToggled: boolean;
|
||||
labelOff: string | number;
|
||||
labelOn: string | number;
|
||||
toggle: (entityToken: string, entityType: string) => any;
|
||||
toolTipOff?: string;
|
||||
toolTipOn?: string;
|
||||
}
|
||||
|
||||
export default function ActionButton({
|
||||
actionType = "like",
|
||||
busy,
|
||||
entityToken = "",
|
||||
entityType = "",
|
||||
iconOff,
|
||||
iconOn,
|
||||
isToggled,
|
||||
labelOff,
|
||||
labelOn,
|
||||
toggle,
|
||||
toolTipOff,
|
||||
toolTipOn,
|
||||
}: ActionButtonProps) {
|
||||
const { events, status } = useAnimationStatus();
|
||||
const onClick = () => {
|
||||
if (status === AnimationStatus.paused) {
|
||||
toggle(entityToken, entityType);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tippy
|
||||
{...{
|
||||
content: isToggled ? toolTipOn : toolTipOff,
|
||||
delay: [500, 0],
|
||||
hideOnClick: false,
|
||||
offset: [0, 12],
|
||||
placement: "bottom",
|
||||
theme: "fakeyou",
|
||||
trigger: "mouseenter",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
{...{
|
||||
className: `fy-action-button ${actionType}-action-button ${
|
||||
isToggled ? "action-button-toggled" : ""
|
||||
}`,
|
||||
disabled: busy,
|
||||
onClick,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
{...{
|
||||
className: `fy-action-button-icon${isToggled ? "-toggled" : ""}`,
|
||||
icon: isToggled ? iconOn : iconOff,
|
||||
}}
|
||||
/>
|
||||
<WorkDots
|
||||
{...{
|
||||
events,
|
||||
labels: [labelOn, labelOff],
|
||||
index: busy ? 0 : isToggled ? 1 : 2,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./ActionButton";
|
||||
export type { ActionButtonProps } from "./ActionButton";
|
||||
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBookmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faBookmark as faBookmarkOutline } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { WorkDots } from "components/svg";
|
||||
import { AnimationStatus, useAnimationStatus } from "hooks";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import "./BookmarkButton.scss";
|
||||
@@ -29,24 +30,19 @@ export default function BookmarkButton({
|
||||
overlay,
|
||||
toggle,
|
||||
}: BookmarkButtonProps) {
|
||||
const { events, status } = useAnimationStatus();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClick = async (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// setIsLoading(true);
|
||||
toggle(entityToken, entityType).then((isToggled: boolean) => {
|
||||
// setIsToggled(isToggled);
|
||||
setIsLoading(false);
|
||||
});
|
||||
// try {
|
||||
// await onToggle(!isToggled);
|
||||
// setIsToggled(!isToggled);
|
||||
// } catch (error) {
|
||||
// console.error("Error calling API", error);
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
if (status === AnimationStatus.paused) {
|
||||
toggle(entityToken, entityType).then((isToggled: boolean) => {
|
||||
// setIsToggled(isToggled);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const buttonClass = isToggled ? "favorite-button toggled" : "favorite-button";
|
||||
@@ -79,7 +75,9 @@ export default function BookmarkButton({
|
||||
/>
|
||||
<div className="favorite-text">
|
||||
<div {...{ className: "favorite-text-wrapper" }}>
|
||||
<WorkDots {...{ labels: ["Saved", "Save"], index }} />
|
||||
<WorkDots
|
||||
{...{ events, labels: ["Saved", "Save"], noPad: true, index }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faHeart } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faHeart as faHeartOutline } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { WorkDots } from "components/svg";
|
||||
import { AnimationStatus, useAnimationStatus } from "hooks";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import "./LikeButton.scss";
|
||||
@@ -29,18 +30,19 @@ export default function LikeButton({
|
||||
large,
|
||||
toggle,
|
||||
}: LikeButtonProps) {
|
||||
const { events, status } = useAnimationStatus();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// console.log("😎",busy);
|
||||
|
||||
const handleClick = async (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// setIsLoading(true);
|
||||
toggle(entityToken, entityType).then((isToggled: boolean) => {
|
||||
// setIsToggled(isToggled);
|
||||
setIsLoading(false);
|
||||
});
|
||||
if (status === AnimationStatus.paused) {
|
||||
toggle(entityToken, entityType).then((isToggled: boolean) => {
|
||||
// setIsToggled(isToggled);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
// try {
|
||||
// await onToggle(!isToggled);
|
||||
// setIsToggled(!isToggled);
|
||||
@@ -80,7 +82,12 @@ export default function LikeButton({
|
||||
<div className="like-number">
|
||||
<div className="like-number-wrapper">
|
||||
<WorkDots
|
||||
{...{ labels: [likeCountShort], index: busy ? 0 : 1 }}
|
||||
{...{
|
||||
events,
|
||||
labels: [likeCountShort],
|
||||
noPad: true,
|
||||
index: busy ? 0 : 1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,39 +22,7 @@
|
||||
margin-bottom: 10px;
|
||||
transition: 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.bg-facebook {
|
||||
color: $white;
|
||||
background-color: #4267b2;
|
||||
}
|
||||
|
||||
&.bg-reddit {
|
||||
color: $white;
|
||||
background-color: #ff5700;
|
||||
}
|
||||
|
||||
&.bg-reddit {
|
||||
color: $white;
|
||||
background-color: #ff5700;
|
||||
}
|
||||
|
||||
&.bg-whatsapp {
|
||||
color: $white;
|
||||
background-color: #25d366;
|
||||
}
|
||||
|
||||
&.bg-x {
|
||||
color: $white;
|
||||
background-color: #1da1f2;
|
||||
}
|
||||
|
||||
&.bg-email {
|
||||
color: $white;
|
||||
background-color: #858585;
|
||||
}
|
||||
@include social-colors;
|
||||
}
|
||||
|
||||
.social-button-text {
|
||||
|
||||
@@ -9,13 +9,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import "./SocialButton.scss";
|
||||
|
||||
export type Socials = "x" | "whatsapp" | "facebook" | "reddit" | "email";
|
||||
|
||||
interface SocialButtonProps {
|
||||
social: "x" | "whatsapp" | "facebook" | "reddit" | "email";
|
||||
hideLabel?: boolean;
|
||||
social: Socials;
|
||||
shareUrl: string;
|
||||
shareText: string;
|
||||
}
|
||||
|
||||
export default function SocialButton({
|
||||
hideLabel,
|
||||
social,
|
||||
shareUrl,
|
||||
shareText,
|
||||
@@ -64,13 +68,18 @@ export default function SocialButton({
|
||||
|
||||
return (
|
||||
<button className="social-button" onClick={handleClick}>
|
||||
<div className={`social-button-icon bg-${social}`}>
|
||||
<div
|
||||
className={`${
|
||||
!hideLabel ? "social-button-icon" : "social-button-icon-no-style"
|
||||
} bg-${social}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={socialIcon} />
|
||||
</div>
|
||||
|
||||
<p className="social-button-text">
|
||||
{social.charAt(0).toUpperCase() + social.slice(1)}
|
||||
</p>
|
||||
{!hideLabel ? (
|
||||
<p className="social-button-text">
|
||||
{social.charAt(0).toUpperCase() + social.slice(1)}
|
||||
</p>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from "./SocialButton";
|
||||
export type { Socials } from "./SocialButton";
|
||||
@@ -1,8 +1,10 @@
|
||||
import Accordion from "./Accordion";
|
||||
import ActionButton, { ActionButtonProps } from "./ActionButton";
|
||||
import AudioBlobPreview from "./AudioBlobPreview";
|
||||
import AudioInput from "./AudioInput";
|
||||
import Badge from "./Badge";
|
||||
import BasicTabs from "./BasicTabs";
|
||||
import BookmarkButton from "./BookmarkButton";
|
||||
import Button from "./Button";
|
||||
import ButtonRevertToDefault from "./ButtonRevertToDefault";
|
||||
import Checkbox from "./Checkbox";
|
||||
@@ -39,6 +41,7 @@ import SelectModalVideoTabs from "./SelectModalVideoTabs";
|
||||
import SelectModalWeightsTabs from "./SelectModalWeightsTabs";
|
||||
import Skeleton from "./Skeleton";
|
||||
import Slider from "./Slider";
|
||||
import SocialButton, { Socials } from "./SocialButton";
|
||||
import Spinner from "./Spinner";
|
||||
import SplitPanel from "./SplitPanel";
|
||||
import Tabs, { NonRouteTabs } from "./Tabs";
|
||||
@@ -56,10 +59,12 @@ import WeightCoverImage from "./WeightCoverImage";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
ActionButton,
|
||||
AudioBlobPreview,
|
||||
AudioInput,
|
||||
Badge,
|
||||
BasicTabs,
|
||||
BookmarkButton,
|
||||
Button,
|
||||
//Cards
|
||||
MediaCards,
|
||||
@@ -101,6 +106,7 @@ export {
|
||||
SelectModalWeightsTabs,
|
||||
Skeleton,
|
||||
Slider,
|
||||
SocialButton,
|
||||
Spinner,
|
||||
SplitPanel,
|
||||
Tabs,
|
||||
@@ -117,7 +123,4 @@ export {
|
||||
WeightCoverImage,
|
||||
};
|
||||
|
||||
export type {
|
||||
ModalUtilities,
|
||||
SelectModalData
|
||||
};
|
||||
export type { ActionButtonProps, ModalUtilities, SelectModalData, Socials };
|
||||
@@ -3,9 +3,11 @@ import { SlideProps } from "./EntityInput";
|
||||
import {
|
||||
// useMediaUploader,
|
||||
useModal,
|
||||
useSession
|
||||
useSession,
|
||||
} from "hooks";
|
||||
import MediaBrowser, { MediaBrowserProps } from "components/modals/MediaBrowser";
|
||||
import MediaBrowser, {
|
||||
MediaBrowserProps,
|
||||
} from "components/modals/MediaBrowser";
|
||||
import { FileWrapper } from "components/common";
|
||||
import {
|
||||
AcceptTypes,
|
||||
@@ -14,7 +16,7 @@ import {
|
||||
getMediaTypesByCategory,
|
||||
isSelectedType,
|
||||
mediaCategoryfromString,
|
||||
MediaFilters
|
||||
MediaFilters,
|
||||
} from "components/entities/EntityTypes";
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@@ -24,66 +26,112 @@ import {
|
||||
faGrid,
|
||||
faImage,
|
||||
faPersonWalking,
|
||||
faWaveform
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
faWaveform,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface EmptySlideProps extends SlideProps {
|
||||
accept: AcceptTypes | AcceptTypes[],
|
||||
inputProps?: any,
|
||||
queryUser?: string,
|
||||
type: EntityModeProp,
|
||||
};
|
||||
accept: AcceptTypes | AcceptTypes[];
|
||||
inputProps?: any;
|
||||
queryUser?: string;
|
||||
type: EntityModeProp;
|
||||
}
|
||||
|
||||
export default function EntityInputEmpty({ accept, inputProps, queryUser, type, ...rest }: EmptySlideProps) {
|
||||
export default function EntityInputEmpty({
|
||||
accept,
|
||||
inputProps,
|
||||
queryUser,
|
||||
type,
|
||||
...rest
|
||||
}: EmptySlideProps) {
|
||||
const { open } = useModal();
|
||||
const { user } = useSession();
|
||||
console.log("😎", user);
|
||||
const accepted = Array.isArray(accept) ? accept : [accept];
|
||||
const inputMode = EntityInputMode[type];
|
||||
const isMedia = inputMode === EntityInputMode.media;
|
||||
const fileTypes = isMedia ? accepted.map((mediaCategory,i) => {
|
||||
return mediaCategory ? getMediaTypesByCategory(mediaCategoryfromString(mediaCategory)) : [];
|
||||
}).flat() : [];
|
||||
const fileTypes = isMedia
|
||||
? accepted
|
||||
.map((mediaCategory, i) => {
|
||||
return mediaCategory
|
||||
? getMediaTypesByCategory(mediaCategoryfromString(mediaCategory))
|
||||
: [];
|
||||
})
|
||||
.flat()
|
||||
: [];
|
||||
|
||||
const props: MediaBrowserProps = {
|
||||
accept: accepted,
|
||||
inputMode,
|
||||
username: queryUser || user?.username || "",
|
||||
...rest
|
||||
...rest,
|
||||
};
|
||||
|
||||
const browserClick = () => open({
|
||||
component: MediaBrowser,
|
||||
props
|
||||
});
|
||||
const browserClick = () =>
|
||||
open({
|
||||
component: MediaBrowser,
|
||||
props,
|
||||
});
|
||||
|
||||
const mediaIcons = () => {
|
||||
if (isSelectedType(MediaFilters.audio,accepted[0])) return faWaveform;
|
||||
if (isSelectedType(MediaFilters.engine_asset,accepted[0])) return faPersonWalking;
|
||||
if (isSelectedType(MediaFilters.image,accepted[0])) return faImage;
|
||||
if (isSelectedType(MediaFilters.video,accepted[0])) return faFile;
|
||||
if (isSelectedType(MediaFilters.audio, accepted[0])) return faWaveform;
|
||||
if (isSelectedType(MediaFilters.engine_asset, accepted[0]))
|
||||
return faPersonWalking;
|
||||
if (isSelectedType(MediaFilters.image, accepted[0])) return faImage;
|
||||
if (isSelectedType(MediaFilters.video, accepted[0])) return faFile;
|
||||
return faFile;
|
||||
};
|
||||
|
||||
const supported = `${ fileTypes.length ? fileTypes.join(", ") : fileTypes[0] } files supported`;
|
||||
const supported = `${
|
||||
fileTypes.length ? fileTypes.join(", ") : fileTypes[0]
|
||||
} files supported`;
|
||||
|
||||
return <>
|
||||
<Icon {...{ className: "fy-entity-input-icon", icon: [faFile,mediaIcons(),faDiagramSankey,mediaIcons()][inputMode] }}/>
|
||||
<div {...{ className: "fy-entity-input-empty-controls" }}>
|
||||
{ isMedia &&
|
||||
<FileWrapper {...{ containerClass: "fy-entity-input-row", fileTypes, panelClass: "fy-entity-input-button", noStyle: true, ...inputProps }}>
|
||||
<>
|
||||
<Icon {...{ className: "fy-entity-input-label-icon", icon: faFileArrowUp }}/>
|
||||
<div {...{ className: "fy-entity-input-upload-detail" }}>
|
||||
Upload, click or drag here
|
||||
{ accept && <span>{ supported }</span> }
|
||||
</div>
|
||||
</>
|
||||
</FileWrapper> }
|
||||
<div {...{ className: "fy-entity-input-row fy-entity-input-button", onClick: browserClick }}>
|
||||
<Icon {...{ className: "fy-entity-input-label-icon", icon: faGrid }}/>
|
||||
Choose from your { ["bookmarks","media","weights"][inputMode] }
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
{...{
|
||||
className: "fy-entity-input-icon",
|
||||
icon: [faFile, mediaIcons(), faDiagramSankey, mediaIcons()][
|
||||
inputMode
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div {...{ className: "fy-entity-input-empty-controls" }}>
|
||||
{isMedia && (
|
||||
<FileWrapper
|
||||
{...{
|
||||
containerClass: "fy-entity-input-row",
|
||||
fileTypes,
|
||||
panelClass: "fy-entity-input-button",
|
||||
noStyle: true,
|
||||
...inputProps,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<Icon
|
||||
{...{
|
||||
className: "fy-entity-input-label-icon",
|
||||
icon: faFileArrowUp,
|
||||
}}
|
||||
/>
|
||||
<div {...{ className: "fy-entity-input-upload-detail" }}>
|
||||
Upload, click or drag here
|
||||
{accept && <span>{supported}</span>}
|
||||
</div>
|
||||
</>
|
||||
</FileWrapper>
|
||||
)}
|
||||
<div
|
||||
{...{
|
||||
className: "fy-entity-input-row fy-entity-input-button",
|
||||
onClick: browserClick,
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
{...{ className: "fy-entity-input-label-icon", icon: faGrid }}
|
||||
/>
|
||||
Choose from your {["bookmarks", "media", "weights"][inputMode]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,59 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from "react";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { basicTransition } from "resources";
|
||||
import { AnimationEvents } from "hooks";
|
||||
|
||||
type Label = string | number;
|
||||
|
||||
interface Props {
|
||||
debug?: boolean,
|
||||
labels: string[],
|
||||
index: number
|
||||
debug?: boolean;
|
||||
events: AnimationEvents;
|
||||
labels: Label[];
|
||||
noPad?: boolean;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export default function WorkDots({ debug, labels = [], index }: Props) {
|
||||
const [animating,animatingSet] = useState(false);
|
||||
const transitions = useTransition(index, basicTransition({
|
||||
onRest: () => animatingSet(false),
|
||||
onStart: () => animatingSet(true)
|
||||
}));
|
||||
export default function WorkDots({
|
||||
debug,
|
||||
events,
|
||||
labels = [],
|
||||
noPad,
|
||||
index,
|
||||
}: Props) {
|
||||
const transitions = useTransition(
|
||||
index,
|
||||
basicTransition({
|
||||
...events,
|
||||
})
|
||||
);
|
||||
|
||||
if (debug) console.log("WorkDots ... Debug",index);
|
||||
if (debug) console.log("WorkDots ... Debug", index);
|
||||
|
||||
return transitions((style: any, i: number, state: any) => {
|
||||
let isLeaving = state.phase === "leave";
|
||||
const content = (txt = "") =>
|
||||
<a.div {...{ style: {
|
||||
...style,
|
||||
position: isLeaving && animating ? "absolute" : "relative"
|
||||
} }}>{
|
||||
txt ? txt : <svg {...{ className: "fy-workdots" }}>
|
||||
<circle cx="2" cy="8" r="2" />
|
||||
<circle cx="8" cy="8" r="2" />
|
||||
<circle cx="14" cy="8" r="2" />
|
||||
</svg>
|
||||
}</a.div>;
|
||||
return [ content(""), ...labels.map((label,i) => content(label))][i];
|
||||
// let isLeaving = state.phase === "leave";
|
||||
const content = (txt: Label) => {
|
||||
return (
|
||||
<a.div
|
||||
{...{
|
||||
className: "fy-workdots-label",
|
||||
style: {
|
||||
...style,
|
||||
...(!noPad ? { right: ".5rem" } : {}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{txt !== undefined ? (
|
||||
txt
|
||||
) : (
|
||||
<svg {...{ className: "fy-workdots" }}>
|
||||
<circle cx="2" cy="8" r="2" />
|
||||
<circle cx="8" cy="8" r="2" />
|
||||
<circle cx="14" cy="8" r="2" />
|
||||
</svg>
|
||||
)}
|
||||
</a.div>
|
||||
);
|
||||
};
|
||||
return [content(""), ...labels.map((label: Label, i) => content(label))][i];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,34 +1,40 @@
|
||||
import useBatchContent from './useBatchContent';
|
||||
import useBookmarks from './useBookmarks';
|
||||
import useChanger from './useChanger';
|
||||
import useCoverImgUpload from './useCoverImgUpload';
|
||||
import useDebounce from './useDebounce';
|
||||
import useFile from './useFile';
|
||||
import useHover from './useHover';
|
||||
import useId from './useId';
|
||||
import useIdempotency from './useIdempotency';
|
||||
import useInferenceJobs from './useInferenceJobs';
|
||||
import useInterval from './useInterval';
|
||||
import useJobStatus from './useJobStatus';
|
||||
import useLazyLists from './useLazyLists';
|
||||
import useListContent from './useListContent';
|
||||
import useLocalize from './useLocalize';
|
||||
import useMedia from './useMedia';
|
||||
import useMediaUploader from './useMediaUploader';
|
||||
import useModal from './useModal';
|
||||
import useNotifications from './useNotifications';
|
||||
import useAnimationStatus, {
|
||||
AnimationEvents,
|
||||
AnimationStatus,
|
||||
} from "./useAnimationStatus";
|
||||
import useBatchContent from "./useBatchContent";
|
||||
import useBookmarks, { MakeBookmarksProps } from "./useBookmarks";
|
||||
import useChanger from "./useChanger";
|
||||
import useCoverImgUpload from "./useCoverImgUpload";
|
||||
import useDebounce from "./useDebounce";
|
||||
import useFile from "./useFile";
|
||||
import useHover from "./useHover";
|
||||
import useId from "./useId";
|
||||
import useIdempotency from "./useIdempotency";
|
||||
import useInferenceJobs from "./useInferenceJobs";
|
||||
import useInterval from "./useInterval";
|
||||
import useJobStatus from "./useJobStatus";
|
||||
import useLazyLists from "./useLazyLists";
|
||||
import useListContent from "./useListContent";
|
||||
import useLocalize from "./useLocalize";
|
||||
import useMedia from "./useMedia";
|
||||
import useMediaUploader from "./useMediaUploader";
|
||||
import useModal from "./useModal";
|
||||
import useNotifications from "./useNotifications";
|
||||
import useOnScreen from "./useOnScreen";
|
||||
import usePrevious from './usePrevious';
|
||||
import useQueuePoll from './useQueuePoll';
|
||||
import useRatings from './useRatings';
|
||||
import useSdUpload from './useSdUpload';
|
||||
import useSession from './useSession';
|
||||
import useSlides from './useSlides';
|
||||
import useStatusPoll from './useStatusPoll';
|
||||
import useVideo from './useVideo';
|
||||
import useWeightFetch from './useWeightFetch';
|
||||
import usePrevious from "./usePrevious";
|
||||
import useQueuePoll from "./useQueuePoll";
|
||||
import useRatings, { MakeRatingsProps } from "./useRatings";
|
||||
import useSdUpload from "./useSdUpload";
|
||||
import useSession from "./useSession";
|
||||
import useSlides from "./useSlides";
|
||||
import useStatusPoll from "./useStatusPoll";
|
||||
import useVideo from "./useVideo";
|
||||
import useWeightFetch from "./useWeightFetch";
|
||||
|
||||
export {
|
||||
AnimationStatus,
|
||||
useAnimationStatus,
|
||||
useBatchContent,
|
||||
useBookmarks,
|
||||
useChanger,
|
||||
@@ -57,5 +63,7 @@ export {
|
||||
useSlides,
|
||||
useStatusPoll,
|
||||
useVideo,
|
||||
useWeightFetch
|
||||
useWeightFetch,
|
||||
};
|
||||
|
||||
export type { AnimationEvents, MakeBookmarksProps, MakeRatingsProps };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export enum AnimationStatus {
|
||||
paused,
|
||||
animating,
|
||||
}
|
||||
|
||||
export interface AnimationEvents {
|
||||
onStart: () => void;
|
||||
onRest: () => void;
|
||||
}
|
||||
|
||||
export default function useAnimationStatus() {
|
||||
const [status, statusSet] = useState(AnimationStatus.paused);
|
||||
|
||||
return {
|
||||
status,
|
||||
events: {
|
||||
onStart: () => statusSet(AnimationStatus.animating),
|
||||
onRest: () => statusSet(AnimationStatus.paused),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,37 +3,40 @@ import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetch
|
||||
import { useSession } from "hooks";
|
||||
|
||||
interface Props {
|
||||
checker: any,
|
||||
debug?: string,
|
||||
fetcher: any,
|
||||
modLibrary?: any,
|
||||
onPass?: any,
|
||||
onFail?: any,
|
||||
resultsKey: string,
|
||||
toggleCheck: (x: any) => any
|
||||
checker: any;
|
||||
debug?: string;
|
||||
fetcher: any;
|
||||
modLibrary?: any;
|
||||
onPass?: any;
|
||||
onFail?: any;
|
||||
resultsKey: string;
|
||||
toggleCheck: (x: any) => any;
|
||||
}
|
||||
|
||||
interface Library {
|
||||
[key: string]: any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface Gather {
|
||||
expand?: boolean,
|
||||
modLibrary?: any,
|
||||
key: string,
|
||||
res: any
|
||||
expand?: boolean;
|
||||
modLibrary?: any;
|
||||
key: string;
|
||||
res: any;
|
||||
}
|
||||
|
||||
type BatchToggle = (entity_token: string, entity_type: string) => boolean;
|
||||
|
||||
export interface MakePropsParams { entityToken: string, entityType: string }
|
||||
export interface MakePropsParams {
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
}
|
||||
|
||||
export interface BatchInputProps {
|
||||
busy: boolean,
|
||||
entityToken: string,
|
||||
entityType: string,
|
||||
isToggled: boolean,
|
||||
toggle: BatchToggle
|
||||
busy: boolean;
|
||||
entityToken: string;
|
||||
entityType: string;
|
||||
isToggled: boolean;
|
||||
toggle: BatchToggle;
|
||||
}
|
||||
|
||||
export type MakeBatchProps = (x: MakePropsParams) => BatchInputProps;
|
||||
@@ -42,57 +45,68 @@ export default function useBatchContent({
|
||||
checker,
|
||||
debug,
|
||||
fetcher,
|
||||
modLibrary = (current: any, res: any, entity_token: string ) => current,
|
||||
modLibrary = (current: any, res: any, entity_token: string) => current,
|
||||
onPass,
|
||||
onFail = { fetch: () => new Promise(() => {}) },
|
||||
resultsKey,
|
||||
toggleCheck
|
||||
toggleCheck,
|
||||
}: Props) {
|
||||
const session = useSession();
|
||||
const [library, librarySet] = useState<Library>({});
|
||||
const [busyList, busyListSet] = useState<Library>({});
|
||||
const [status, statusSet] = useState(FetchStatus.ready);
|
||||
const [tokenType,tokenTypeSet] = useState("");
|
||||
const [tokenType, tokenTypeSet] = useState("");
|
||||
|
||||
const dlog = (...dbg: any) => debug ? console.log(...dbg) : {};
|
||||
const dlog = (...dbg: any) => (debug ? console.log(...dbg) : {});
|
||||
|
||||
const gather = ({ expand, key, res }: Gather) => {
|
||||
let tokens = res.results ? res.results.map((item: any) => (item.details || item)[key]) : [res[key]];
|
||||
let loadingTokens = tokens.reduce((obj = {},token = "") => ({ ...obj, [token]: true }),{})
|
||||
dlog("🪙",fetcher);
|
||||
tokenTypeSet(key)
|
||||
let tokens = res.results
|
||||
? res.results.map((item: any) => (item.details || item)[key])
|
||||
: [res[key]];
|
||||
let loadingTokens = tokens.reduce(
|
||||
(obj = {}, token = "") => ({ ...obj, [token]: true }),
|
||||
{}
|
||||
);
|
||||
dlog("🪙", fetcher);
|
||||
tokenTypeSet(key);
|
||||
busyListSet(loadingTokens); // add current batch to busy list
|
||||
fetcher("",{},{ tokens }).then((batchRes: any) => {
|
||||
|
||||
// console.log("🦄",resultsKey, res, modLibrary);
|
||||
fetcher("", {}, { tokens }).then((batchRes: any) => {
|
||||
// console.log("🦄", resultsKey, res, modLibrary);
|
||||
if (batchRes.success && !!batchRes[resultsKey]) {
|
||||
// console.log("🥏",resultsKey);
|
||||
let newBatch = batchRes[resultsKey].reduce((obj = {}, { entity_token = "", ...current }) => {
|
||||
// console.log("🧲", resultsKey, {current, res, entity_token, obj});
|
||||
let newCurrent = {
|
||||
...obj,
|
||||
[entity_token]: { ...modLibrary(current, res, entity_token, key) }
|
||||
};
|
||||
let newBatch = batchRes[resultsKey].reduce(
|
||||
(obj = {}, { entity_token = "", ...current }) => {
|
||||
// console.log("🧲", resultsKey, {current, res, entity_token, obj});
|
||||
let newCurrent = {
|
||||
...obj,
|
||||
[entity_token]: {
|
||||
...modLibrary(current, res, entity_token, key),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// console.log("😡", resultsKey, newCurrent);
|
||||
return newCurrent;
|
||||
},{});
|
||||
// console.log("😡", resultsKey, newCurrent);
|
||||
return newCurrent;
|
||||
},
|
||||
{}
|
||||
);
|
||||
// console.log("🐸",busyList);
|
||||
busyListSet({}); // this should be a for each key in tokens delete from busyList, but this is fine for now
|
||||
librarySet((library: any) => expand ? { ...library, ...newBatch } : newBatch);
|
||||
librarySet((library: any) =>
|
||||
expand ? { ...library, ...newBatch } : newBatch
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const busyAdd = (entity_token: string) =>
|
||||
busyListSet(state => ({ ...state, [entity_token]: true }));
|
||||
|
||||
const busyAdd = (entity_token: string) => busyListSet(state => ({ ...state, [entity_token]: true }));
|
||||
|
||||
const busyRemove = (entity_token: string) => busyListSet(state => {
|
||||
let newState = { ...state };
|
||||
delete newState[entity_token];
|
||||
return newState;
|
||||
});
|
||||
const busyRemove = (entity_token: string) =>
|
||||
busyListSet(state => {
|
||||
let newState = { ...state };
|
||||
delete newState[entity_token];
|
||||
return newState;
|
||||
});
|
||||
|
||||
const toggle: BatchToggle = (entity_token, entity_type) => {
|
||||
if (session.check()) {
|
||||
@@ -100,43 +114,50 @@ export default function useBatchContent({
|
||||
statusSet(FetchStatus.in_progress);
|
||||
busyAdd(entity_token);
|
||||
|
||||
console.log(`⏳ toggling entity ${ entity_token }, in library?: ${ !!inLibrary }`);
|
||||
console.log(
|
||||
`⏳ toggling entity ${entity_token}, in library?: ${!!inLibrary}`
|
||||
);
|
||||
|
||||
if (inLibrary && checker(inLibrary)) {
|
||||
// console.log("⭕️");
|
||||
return onPass.fetch(entity_token, entity_type, library)
|
||||
.then((res: any) => {
|
||||
console.log("⭕️",res);
|
||||
busyRemove(entity_token);
|
||||
librarySet(onPass.modLibrary(res, entity_token, entity_type, library));
|
||||
statusSet(FetchStatus.ready);
|
||||
return false;
|
||||
});
|
||||
return onPass
|
||||
.fetch(entity_token, entity_type, library)
|
||||
.then((res: any) => {
|
||||
console.log("⭕️", res);
|
||||
busyRemove(entity_token);
|
||||
librarySet(
|
||||
onPass.modLibrary(res, entity_token, entity_type, library)
|
||||
);
|
||||
statusSet(FetchStatus.ready);
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// console.log("❌");
|
||||
return onFail.fetch(entity_token, entity_type, library)
|
||||
.then((res: any) => {
|
||||
console.log("❌",res);
|
||||
busyRemove(entity_token);
|
||||
librarySet(onFail.modLibrary(res, entity_token, entity_type, library));
|
||||
statusSet(FetchStatus.ready);
|
||||
return true;
|
||||
});
|
||||
return onFail
|
||||
.fetch(entity_token, entity_type, library)
|
||||
.then((res: any) => {
|
||||
console.log("❌", res);
|
||||
busyRemove(entity_token);
|
||||
librarySet(
|
||||
onFail.modLibrary(res, entity_token, entity_type, library)
|
||||
);
|
||||
statusSet(FetchStatus.ready);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggled = ( entity_token = "" ) => toggleCheck(library[entity_token]);
|
||||
const toggled = (entity_token = "") => toggleCheck(library[entity_token]);
|
||||
|
||||
const makeProps = ({ entityToken, entityType }: MakePropsParams) => ({
|
||||
busy: busyList[entityToken],
|
||||
entityToken,
|
||||
entityType,
|
||||
isToggled: toggled(entityToken),
|
||||
toggle
|
||||
toggle,
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
busyAdd,
|
||||
busyRemove,
|
||||
@@ -150,6 +171,6 @@ export default function useBatchContent({
|
||||
statusSet,
|
||||
tokenType,
|
||||
toggle,
|
||||
toggled
|
||||
toggled,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { GetBookmarks } from "@storyteller/components/src/api/bookmarks/GetBookmarks";
|
||||
import { CreateBookmark } from "@storyteller/components/src/api/bookmarks/CreateBookmark";
|
||||
import { DeleteBookmark } from "@storyteller/components/src/api/bookmarks/DeleteBookmark";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faBookmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faBookmark as faBookmarkOutline } from "@fortawesome/pro-regular-svg-icons";
|
||||
|
||||
import useBatchContent, {
|
||||
BatchInputProps,
|
||||
MakePropsParams,
|
||||
} from "hooks/useBatchContent";
|
||||
|
||||
export interface BookmarksProps extends BatchInputProps {
|
||||
actionType: "bookmark";
|
||||
iconOff: IconDefinition;
|
||||
iconOn: IconDefinition;
|
||||
labelOff: string | number;
|
||||
labelOn: string | number;
|
||||
}
|
||||
|
||||
export type MakeBookmarksProps = (x: MakePropsParams) => BookmarksProps;
|
||||
|
||||
export default function useBookmarks() {
|
||||
const toggleList =
|
||||
(toBookmark: boolean) =>
|
||||
(res: any, entity_token: string, entity_type: string, lib: any) => {
|
||||
return {
|
||||
...lib,
|
||||
[entity_token]: {
|
||||
entity_type,
|
||||
is_bookmarked: toBookmark,
|
||||
maybe_bookmark_token: toBookmark ? res.user_bookmark_token : null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const bookmarks = useBatchContent({
|
||||
debug: "bookmarks",
|
||||
checker: ({ maybe_bookmark_token }: any) => !!maybe_bookmark_token,
|
||||
fetcher: GetBookmarks,
|
||||
onPass: {
|
||||
fetch: (entity_token: string, entity_type: string, lib: any) => {
|
||||
let bookmarkToken = lib[entity_token].maybe_bookmark_token;
|
||||
return DeleteBookmark(bookmarkToken, { as_mod: false });
|
||||
},
|
||||
modLibrary: toggleList(false),
|
||||
},
|
||||
onFail: {
|
||||
fetch: (entity_token: string, entity_type: string) =>
|
||||
CreateBookmark("", {
|
||||
entity_token,
|
||||
entity_type,
|
||||
}),
|
||||
modLibrary: toggleList(true),
|
||||
},
|
||||
resultsKey: "bookmarks",
|
||||
toggleCheck: (entity: any) => !!entity?.maybe_bookmark_token,
|
||||
});
|
||||
|
||||
const makeProps: MakeBookmarksProps = ({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: MakePropsParams) => {
|
||||
return {
|
||||
...bookmarks.makeProps({ entityToken, entityType }),
|
||||
actionType: "bookmark",
|
||||
iconOff: faBookmarkOutline,
|
||||
iconOn: faBookmark,
|
||||
labelOff: "Save",
|
||||
labelOn: "Saved",
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...bookmarks,
|
||||
makeProps,
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./useBookmarks";
|
||||
@@ -1,47 +0,0 @@
|
||||
import { GetBookmarks } from "@storyteller/components/src/api/bookmarks/GetBookmarks";
|
||||
import { CreateBookmark } from "@storyteller/components/src/api/bookmarks/CreateBookmark";
|
||||
import { DeleteBookmark } from "@storyteller/components/src/api/bookmarks/DeleteBookmark";
|
||||
// import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
|
||||
import { useBatchContent } from "hooks";
|
||||
// import { useSession } from "hooks";
|
||||
|
||||
// interface BookmarkLibrary {
|
||||
// [key: string]: any
|
||||
// }
|
||||
|
||||
export default function useBookmarks() {
|
||||
|
||||
const toggleList = (toBookmark: boolean) => (res: any, entity_token: string, entity_type: string, lib: any) => {
|
||||
return {
|
||||
...lib,
|
||||
[entity_token]: {
|
||||
entity_type,
|
||||
is_bookmarked: toBookmark,
|
||||
maybe_bookmark_token: toBookmark ? res.user_bookmark_token : null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const bookmarks = useBatchContent({
|
||||
checker: ({ maybe_bookmark_token }: any) => !!maybe_bookmark_token,
|
||||
fetcher: GetBookmarks,
|
||||
onPass: {
|
||||
fetch: (entity_token: string, entity_type: string, lib: any) => {
|
||||
let bookmarkToken = lib[entity_token].maybe_bookmark_token;
|
||||
return DeleteBookmark(bookmarkToken,{ as_mod: false });
|
||||
},
|
||||
modLibrary: toggleList(false)
|
||||
},
|
||||
onFail: {
|
||||
fetch: (entity_token: string, entity_type: string) => CreateBookmark("",{
|
||||
entity_token,
|
||||
entity_type
|
||||
}),
|
||||
modLibrary: toggleList(true)
|
||||
},
|
||||
resultsKey: "bookmarks",
|
||||
toggleCheck: (entity: any) => !!entity?.maybe_bookmark_token
|
||||
});
|
||||
|
||||
return bookmarks;
|
||||
};
|
||||
@@ -33,8 +33,17 @@ export default function useMedia({
|
||||
|
||||
const previousToken = usePrevious(mediaToken);
|
||||
|
||||
const reload = () => {
|
||||
statusSet(FetchStatus.ready);
|
||||
mediaSet(undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaToken && (!media || previousToken !== mediaToken)) {
|
||||
if (
|
||||
status === FetchStatus.ready &&
|
||||
mediaToken &&
|
||||
(!media || previousToken !== mediaToken)
|
||||
) {
|
||||
statusSet(FetchStatus.in_progress);
|
||||
GetMedia(mediaToken, {})
|
||||
.then(res => {
|
||||
@@ -64,5 +73,14 @@ export default function useMedia({
|
||||
}
|
||||
}, [media, mediaToken, previousToken, prompt, onSuccess, status, statusSet]);
|
||||
|
||||
return { media, mediaSet, prompt, remove, status, writeStatus };
|
||||
return {
|
||||
media,
|
||||
mediaFile: media,
|
||||
mediaSet,
|
||||
prompt,
|
||||
remove,
|
||||
reload,
|
||||
status,
|
||||
writeStatus,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { GetRatings } from "@storyteller/components/src/api/user_ratings/GetRatings";
|
||||
import { SetRating } from "@storyteller/components/src/api/user_ratings/SetRating";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faHeart } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faHeart as faHeartOutline } from "@fortawesome/pro-regular-svg-icons";
|
||||
import useBatchContent, {
|
||||
BatchInputProps,
|
||||
MakePropsParams,
|
||||
} from "hooks/useBatchContent";
|
||||
|
||||
export interface RatingsProps extends BatchInputProps {
|
||||
actionType: "like";
|
||||
iconOff: IconDefinition;
|
||||
iconOn: IconDefinition;
|
||||
likeCount: 0;
|
||||
labelOff: string | number;
|
||||
labelOn: string | number;
|
||||
}
|
||||
|
||||
export type MakeRatingsProps = (x: MakePropsParams) => RatingsProps;
|
||||
|
||||
export default function useRatings() {
|
||||
const fetch = (entity_token: string, entity_type: string, lib: any) => {
|
||||
const newRating = {
|
||||
entity_token,
|
||||
entity_type,
|
||||
rating_value:
|
||||
lib[entity_token]?.rating_value !== "positive" ? "positive" : "neutral",
|
||||
};
|
||||
return SetRating("", newRating);
|
||||
};
|
||||
|
||||
const modLibrary = (
|
||||
res: any,
|
||||
entity_token: string,
|
||||
entity_type: string,
|
||||
lib: any
|
||||
) => {
|
||||
return {
|
||||
...lib,
|
||||
[entity_token]: {
|
||||
entity_type,
|
||||
rating_value:
|
||||
lib[entity_token].rating_value === "neutral" ? "positive" : "neutral",
|
||||
positive_rating_count: res.new_positive_rating_count_for_entity,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ratings = useBatchContent({
|
||||
fetcher: GetRatings,
|
||||
checker: () => true,
|
||||
// debug: "useRatings",
|
||||
modLibrary: (
|
||||
current: any,
|
||||
res: any,
|
||||
entity_token: string,
|
||||
tokenType: string
|
||||
) => {
|
||||
let result = res.results
|
||||
? res.results.find((item: any, i: number) => {
|
||||
return (item.details || item)[tokenType] === entity_token;
|
||||
})
|
||||
: res;
|
||||
|
||||
let { positive_rating_count } = (result.details || result).stats;
|
||||
|
||||
return { ...current, positive_rating_count };
|
||||
},
|
||||
onFail: { fetch, modLibrary },
|
||||
onPass: { fetch, modLibrary },
|
||||
resultsKey: "ratings",
|
||||
toggleCheck: (entity: any) => (entity?.rating_value || "") === "positive",
|
||||
});
|
||||
|
||||
const makeProps: MakeRatingsProps = ({
|
||||
entityToken,
|
||||
entityType,
|
||||
}: MakePropsParams) => {
|
||||
const likeCount = ratings.library[entityToken]?.positive_rating_count || 0;
|
||||
return {
|
||||
...ratings.makeProps({ entityToken, entityType }),
|
||||
actionType: "like",
|
||||
iconOn: faHeart,
|
||||
iconOff: faHeartOutline,
|
||||
labelOff: likeCount,
|
||||
labelOn: likeCount,
|
||||
likeCount,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...ratings,
|
||||
makeProps,
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from "./useRatings";
|
||||
export type { MakeRatingsProps } from "./useRatings";
|
||||
@@ -1,63 +0,0 @@
|
||||
import { GetRatings } from "@storyteller/components/src/api/user_ratings/GetRatings";
|
||||
import { SetRating } from "@storyteller/components/src/api/user_ratings/SetRating";
|
||||
import useBatchContent, { BatchInputProps, MakePropsParams } from "hooks/useBatchContent";
|
||||
|
||||
export interface RatingsProps extends BatchInputProps {
|
||||
likeCount: 0
|
||||
}
|
||||
|
||||
export type MakeRatingsProps = (x: MakePropsParams) => RatingsProps;
|
||||
|
||||
export default function useRatings() {
|
||||
const fetch = (entity_token: string, entity_type: string, lib: any) => {
|
||||
const newRating = {
|
||||
entity_token,
|
||||
entity_type,
|
||||
rating_value: lib[entity_token]?.rating_value !== "positive" ? "positive" : "neutral"
|
||||
}
|
||||
return SetRating("", newRating);
|
||||
};
|
||||
|
||||
const modLibrary = (res: any, entity_token: string, entity_type: string, lib: any) => {
|
||||
return {
|
||||
...lib,
|
||||
[entity_token]: {
|
||||
entity_type,
|
||||
rating_value: lib[entity_token].rating_value === "neutral" ? "positive" : "neutral",
|
||||
positive_rating_count: res.new_positive_rating_count_for_entity
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ratings = useBatchContent({
|
||||
fetcher: GetRatings,
|
||||
checker: () => true,
|
||||
// debug: "useRatings",
|
||||
modLibrary: (current: any, res: any, entity_token: string, tokenType: string) => {
|
||||
|
||||
let result = res.results ? res.results.find((item: any, i: number) =>
|
||||
{
|
||||
return (item.details || item)[tokenType] === entity_token
|
||||
}
|
||||
) : res;
|
||||
|
||||
let { positive_rating_count } = (result.details || result).stats;
|
||||
|
||||
return { ...current, positive_rating_count };
|
||||
},
|
||||
onFail: { fetch, modLibrary },
|
||||
onPass: { fetch, modLibrary },
|
||||
resultsKey: "ratings",
|
||||
toggleCheck: (entity: any) => (entity?.rating_value || "") === "positive"
|
||||
});
|
||||
|
||||
const makeProps: MakeRatingsProps = ({ entityToken, entityType }: MakePropsParams) => ({
|
||||
...ratings.makeProps({ entityToken, entityType }),
|
||||
likeCount: ratings.library[entityToken]?.positive_rating_count || 0,
|
||||
});
|
||||
|
||||
return {
|
||||
...ratings,
|
||||
makeProps
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
import { config } from "@react-spring/web";
|
||||
const n = (x:any) => {};
|
||||
const n = (x: any) => {};
|
||||
|
||||
const basicTransition = ({ ...overwrite }, onRest = n, onStart = n) => ({
|
||||
config: config.gentle,
|
||||
from: { opacity: 0, },
|
||||
enter: { opacity: 1, },
|
||||
leave: { opacity: 0, },
|
||||
// config: { duration: 3000 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1.0, position: "relative" },
|
||||
leave: { opacity: 0, position: "absolute" },
|
||||
onRest,
|
||||
onStart,
|
||||
...overwrite
|
||||
...overwrite,
|
||||
});
|
||||
|
||||
export default basicTransition;
|
||||
export default basicTransition;
|
||||
@@ -1,2 +1,3 @@
|
||||
$under1280: max-width 1280px;
|
||||
$under992: max-width 992px;
|
||||
$over992: min-width 992px;
|
||||
$over992: min-width 992px;
|
||||
@@ -81,3 +81,35 @@ $badge-colors: (
|
||||
"purple": #8f49de,
|
||||
"pink": #cc1d92,
|
||||
);
|
||||
|
||||
@mixin social-colors {
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.bg-facebook {
|
||||
background-color: #4267b2;
|
||||
}
|
||||
|
||||
&.bg-reddit {
|
||||
background-color: #ff5700;
|
||||
}
|
||||
|
||||
&.bg-reddit {
|
||||
background-color: #ff5700;
|
||||
}
|
||||
|
||||
&.bg-whatsapp {
|
||||
background-color: #25d366;
|
||||
}
|
||||
|
||||
&.bg-x {
|
||||
background-color: #1da1f2;
|
||||
}
|
||||
|
||||
&.bg-email {
|
||||
background-color: #858585;
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,12 @@ styles with important tags:
|
||||
color: rgba($white, 0.8);
|
||||
font-weight: 400;
|
||||
border-color: rgba($white, 0.12);
|
||||
|
||||
// this is to remove the lingering border on the comment component when there is only one comment -V
|
||||
|
||||
&:only-of-type {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-mw {
|
||||
|
||||
@@ -98,7 +98,9 @@ import VcModelEditPage from "./pages/vc/vc_model_edit/VcModelEditPage";
|
||||
import VcModelDeletePage from "./pages/vc/vc_model_delete/VcModelDeletePage";
|
||||
// import { StorytellerStudioListPage } from "./pages/storyteller_studio/StorytellerStudioPage";
|
||||
import TopNav from "components/layout/TopNav/TopNav";
|
||||
import MediaPage from "./pages/media/MediaPage";
|
||||
// import MediaPage from "./pages/media/MediaPage";
|
||||
import MediaPageSwitch from "./pages/media/MediaPageSwitch";
|
||||
// import DevMediaPage from "./pages/media/DevMediaPage";
|
||||
import EditCoverImage from "./pages/media/EditCoverImage";
|
||||
import MediaRenamePage from "./pages/media/MediaRenamePage";
|
||||
import { VoiceDesignerFormPage } from "./pages/voice_designer/VoiceDesignerFormPage";
|
||||
@@ -403,9 +405,17 @@ class PageContainer extends React.Component<
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<MediaPage />
|
||||
<MediaPageSwitch />
|
||||
</Route>
|
||||
|
||||
{/* <Route path="/media/:token">
|
||||
<MediaPage />
|
||||
</Route>*/}
|
||||
|
||||
{/* <Route path="/dev-media/:token">
|
||||
<DevMediaPage />
|
||||
</Route>*/}
|
||||
|
||||
<Route path="/edit-cover-image/:token">
|
||||
<EditCoverImage />
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
@import "scss/_breakpoint.scss";
|
||||
@import "scss/breakpoints.scss";
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.media-page-container {
|
||||
@include breakpoint($under992) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.video-page-main-content {
|
||||
width: 75%;
|
||||
|
||||
&.video-page-theater-mode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
margin: 1rem 0;
|
||||
justify-content: space-between;
|
||||
|
||||
.video-page-header-button {
|
||||
height: 3rem;
|
||||
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.video-page-file-tools {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint($under992) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-page-title {
|
||||
line-height: 1;
|
||||
span {
|
||||
opacity: 0.66;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-title-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.fy-input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 3rem;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.title-editor-spinner {
|
||||
position: absolute;
|
||||
height: 3rem;
|
||||
// justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
// display: inline;
|
||||
margin: 0 auto;
|
||||
font-size: 1.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.video-page-media-content {
|
||||
border-radius: 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.video-page-media-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.media-share-label {
|
||||
font-size: 0.825rem;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
// padding: 1rem 0;
|
||||
|
||||
.video-page-creator {
|
||||
// background-color: seagreen;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// flex-wrap: wrap;
|
||||
.gravatar-img {
|
||||
// height: 56px;
|
||||
margin-right: 0.5rem;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.video-page-creator-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
h3 {
|
||||
color: $primary;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// @include breakpoint($under1280) { // replace when related videos returns
|
||||
@include breakpoint($under992) {
|
||||
.video-page-actions-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.media-share-label {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.video-page-share-links {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
.social-button {
|
||||
// margin: 0 0.5rem 0 0;
|
||||
// height: 2.5rem;
|
||||
// width: 2.5rem;
|
||||
svg {
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.social-button-icon-no-style {
|
||||
background-color: red;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
|
||||
@include social-colors;
|
||||
}
|
||||
|
||||
.like-button,
|
||||
.favorite-button {
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
max-height: 2.25rem;
|
||||
margin: 0 0.5rem 0 0;
|
||||
height: 2.25rem;
|
||||
|
||||
.favorite-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
// width: 2.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.video-page-comments {
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.video-page-related-videos {
|
||||
// background-color: green;
|
||||
// display: grid;
|
||||
// grid-template-columns: 50% 50%;
|
||||
padding: 0 1rem;
|
||||
flex-grow: 1;
|
||||
gap: 1rem;
|
||||
|
||||
@include breakpoint($under992) {
|
||||
margin-top: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.video-page-related-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.video-page-related-details {
|
||||
width: 40%;
|
||||
|
||||
h6 {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.video-page-related-cover-image {
|
||||
background-color: purple;
|
||||
border-radius: 0.5rem;
|
||||
width: 60%;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import MediaAudioPlayer from "./MediaAudioPlayer";
|
||||
import MediaVideoComponent from "./MediaVideoComponent";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaFile";
|
||||
import Container from "components/common/Container";
|
||||
import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Skeleton from "components/common/Skeleton";
|
||||
import Button from "components/common/Button";
|
||||
import { MediaSubViewProps } from "./MediaPageSwitch";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
faLink,
|
||||
faFileCircleXmark,
|
||||
faArrowRightArrowLeft,
|
||||
faCopy,
|
||||
faFilm,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import Accordion from "components/common/Accordion";
|
||||
@@ -29,10 +28,7 @@ import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import moment from "moment";
|
||||
import WeightCoverImage from "components/common/WeightCoverImage";
|
||||
import SocialButton from "components/common/SocialButton";
|
||||
import { Badge, Input, Modal } from "components/common";
|
||||
import BookmarkButton from "components/common/BookmarkButton";
|
||||
import LikeButton from "components/common/LikeButton";
|
||||
import { useBookmarks, useMedia, useRatings, useSession } from "hooks";
|
||||
import { ActionButton, Badge, Input } from "components/common";
|
||||
import { WeightCategory } from "@storyteller/components/src/api/_common/enums/WeightCategory";
|
||||
import SdBatchMediaPanel from "./components/SdBatchMediaPanel/SdBatchMediaPanel";
|
||||
import { GetMediaBatchImages } from "@storyteller/components/src/api/media_files/GetMediaBatchImages";
|
||||
@@ -41,41 +37,28 @@ import { DeleteFeaturedItem } from "@storyteller/components/src/api/featured_ite
|
||||
import { mediaTypeLabels } from "utils/mediaTypeLabels";
|
||||
import { EngineMediaPanel } from "./components/EngineMediaPanel/EngineMediaPanel";
|
||||
import { GetMediaFileTitle } from "common/GetMediaFileTitle";
|
||||
import { STYLES_BY_KEY } from "common/StyleOptions";
|
||||
import { faCube, faStarShooting } from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
|
||||
import { GetWebsiteLink } from "@storyteller/components/src/env/GetWebsiteLink";
|
||||
|
||||
export default function MediaPage() {
|
||||
const { canAccessStudio, canEditMediaFile, canBanUsers, user } = useSession();
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const bookmarks = useBookmarks();
|
||||
const ratings = useRatings();
|
||||
const {
|
||||
media: mediaFile,
|
||||
prompt,
|
||||
remove,
|
||||
status,
|
||||
} = useMedia({
|
||||
mediaToken: token,
|
||||
onSuccess: (res: any) => {
|
||||
ratings.gather({ res, key: "token" });
|
||||
},
|
||||
});
|
||||
export default function MediaPage({
|
||||
bookmarkButtonProps,
|
||||
canAccessStudio,
|
||||
canBanUsers,
|
||||
canEdit,
|
||||
mediaFile,
|
||||
openDeleteModal,
|
||||
ratingButtonProps,
|
||||
status,
|
||||
urlToken,
|
||||
}: MediaSubViewProps) {
|
||||
const batchToken = mediaFile?.maybe_batch_token;
|
||||
const [images, setImages] = useState<{ url: string; token: string }[]>([]);
|
||||
const bucketConfig = new BucketConfig();
|
||||
const timeCreated = moment(mediaFile?.created_at || "").fromNow();
|
||||
const dateCreated = moment(mediaFile?.created_at || "").format("LLL");
|
||||
const [buttonLabel, setButtonLabel] = useState("Copy");
|
||||
const [copyPositiveButtonText, setCopyPositiveButtonText] = useState("Copy");
|
||||
const [copyNegativeButtonText, setCopyNegativeButtonText] = useState("Copy");
|
||||
const [activeSlide, setActiveSlide] = useState({ url: "", token: "" });
|
||||
const viewerCanEdit = canEditMediaFile(
|
||||
mediaFile?.maybe_creator_user?.user_token || ""
|
||||
);
|
||||
|
||||
const isModerator = canBanUsers() || false;
|
||||
const viewerCanMakeFeatured = canBanUsers() || false;
|
||||
|
||||
// Inside MediaPage.tsx
|
||||
@@ -99,14 +82,6 @@ export default function MediaPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [batchToken]);
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const closeDeleteModal = () => setIsDeleteModalOpen(false);
|
||||
|
||||
const openDeleteModal = () => setIsDeleteModalOpen(true);
|
||||
|
||||
const deleteMedia = () => remove(!!user?.can_ban_users);
|
||||
|
||||
const handleFeatureMedia = async () => {
|
||||
setFeatureMedia(!mediaFile?.is_featured);
|
||||
};
|
||||
@@ -132,19 +107,6 @@ export default function MediaPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (
|
||||
text: string,
|
||||
setButtonText: React.Dispatch<React.SetStateAction<string>>
|
||||
) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setButtonText("Copied!");
|
||||
setTimeout(() => setButtonText("Copy"), 2000); // Change back after 2 seconds
|
||||
} catch (err) {
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveSlideChange = (image: any) => {
|
||||
setActiveSlide(image);
|
||||
};
|
||||
@@ -168,113 +130,6 @@ export default function MediaPage() {
|
||||
|
||||
usePrefixedDocumentTitle(pageTitle);
|
||||
|
||||
const promptSection = (
|
||||
<>
|
||||
{prompt?.maybe_positive_prompt && (
|
||||
<Panel padding={true} className="mt-3">
|
||||
<div className="d-flex gap-3 align-items-center mb-2">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Prompt</h6>
|
||||
<Button
|
||||
icon={faCopy}
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
prompt.maybe_positive_prompt || "",
|
||||
setCopyPositiveButtonText
|
||||
)
|
||||
}
|
||||
label={copyPositiveButtonText}
|
||||
variant="link"
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt.maybe_positive_prompt}</p>
|
||||
</div>
|
||||
|
||||
{prompt?.maybe_negative_prompt && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">
|
||||
Negative Prompt
|
||||
</h6>
|
||||
<Button
|
||||
icon={faCopy}
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
prompt.maybe_negative_prompt || "",
|
||||
setCopyNegativeButtonText
|
||||
)
|
||||
}
|
||||
label={copyNegativeButtonText}
|
||||
variant="link"
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt.maybe_negative_prompt}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{prompt?.maybe_style_name && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Style Name</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
{STYLES_BY_KEY.get(prompt.maybe_style_name)?.label}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{prompt?.used_face_detailer && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">
|
||||
Used Face Detailer
|
||||
</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
{prompt?.used_face_detailer ? "Yes" : "No"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{prompt?.used_upscaler && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Used Upscaler</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt?.used_upscaler ? "Yes" : "No"}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isModerator &&
|
||||
mediaFile?.maybe_moderator_fields
|
||||
?.maybe_style_transfer_source_media_file_token && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Source Video</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
<Link
|
||||
to={`/media/${mediaFile?.maybe_moderator_fields?.maybe_style_transfer_source_media_file_token}`}
|
||||
>
|
||||
Link to source (staff only)
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function renderMediaComponent(mediaFile: MediaFile) {
|
||||
switch (mediaFile.media_type) {
|
||||
case MediaFileType.Audio:
|
||||
@@ -299,17 +154,6 @@ export default function MediaPage() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case MediaFileType.Video:
|
||||
return (
|
||||
<>
|
||||
<div className="panel panel-clear">
|
||||
<div className="ratio ratio-16x9 video-bg panel-border rounded">
|
||||
<MediaVideoComponent mediaFile={mediaFile} />
|
||||
</div>
|
||||
</div>
|
||||
{promptSection}
|
||||
</>
|
||||
);
|
||||
|
||||
case MediaFileType.Image:
|
||||
let sdMediaImage = [
|
||||
@@ -331,7 +175,6 @@ export default function MediaPage() {
|
||||
images={mediaFile.maybe_batch_token ? images : sdMediaImage}
|
||||
onActiveSlideChange={handleActiveSlideChange}
|
||||
/>
|
||||
{promptSection}
|
||||
</>
|
||||
);
|
||||
case MediaFileType.Pmd:
|
||||
@@ -563,7 +406,7 @@ export default function MediaPage() {
|
||||
},
|
||||
];
|
||||
|
||||
if (user?.canBanUsers) {
|
||||
if (canBanUsers()) {
|
||||
modMediaDetails = (
|
||||
<Accordion.Item title="Moderator Details" defaultOpen={false}>
|
||||
<DataTable data={modDetails} />
|
||||
@@ -614,7 +457,7 @@ export default function MediaPage() {
|
||||
...(coverMediaPath
|
||||
? { src: bucketConfig.getGcsUrl(coverMediaPath) }
|
||||
: {}),
|
||||
...(viewerCanEdit ? { to: `/edit-cover-image/${token}` } : {}),
|
||||
...(canEdit ? { to: `/edit-cover-image/${urlToken}` } : {}),
|
||||
coverIndex: mediaFile.cover_image.default_cover.image_index,
|
||||
}}
|
||||
/>
|
||||
@@ -626,7 +469,7 @@ export default function MediaPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Container type="panel" className="mb-5">
|
||||
<Panel clear={true} className="py-4">
|
||||
<div className="d-flex flex-column flex-lg-row gap-3 gap-lg-2">
|
||||
@@ -657,24 +500,8 @@ export default function MediaPage() {
|
||||
)}
|
||||
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<LikeButton
|
||||
{...{
|
||||
large: true,
|
||||
...ratings.makeProps({
|
||||
entityToken: token,
|
||||
entityType: "media_file",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<BookmarkButton
|
||||
{...{
|
||||
large: true,
|
||||
...bookmarks.makeProps({
|
||||
entityToken: token,
|
||||
entityType: "media_file",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<ActionButton {...ratingButtonProps} />
|
||||
<ActionButton {...bookmarkButtonProps} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -946,7 +773,7 @@ export default function MediaPage() {
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{viewerCanEdit && (
|
||||
{canEdit && (
|
||||
<>
|
||||
<div className="d-flex gap-2">
|
||||
<Button
|
||||
@@ -1011,16 +838,6 @@ export default function MediaPage() {
|
||||
</Panel>
|
||||
</Container>
|
||||
</div>
|
||||
<Modal
|
||||
show={isDeleteModalOpen}
|
||||
handleClose={closeDeleteModal}
|
||||
title="Delete Media"
|
||||
content={() => (
|
||||
<>{`Are you sure you want to delete this media file? This action cannot be undone.`}</> // replace w/ dynamic later -V
|
||||
// <>{`Are you sure you want to delete "${title}"? This action cannot be undone.`}</>
|
||||
)}
|
||||
onConfirm={deleteMedia}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useBookmarks, useMedia, useRatings, useSession } from "hooks";
|
||||
import { MediaFileType } from "@storyteller/components/src/api/_common/enums/MediaFileType";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
|
||||
import { CreateFeaturedItem } from "@storyteller/components/src/api/featured_items/CreateFeaturedItem";
|
||||
import { DeleteFeaturedItem } from "@storyteller/components/src/api/featured_items/DeleteFeaturedItem";
|
||||
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
|
||||
import { ActionButtonProps, Container, Modal, Panel } from "components/common";
|
||||
import MediaPage from "./MediaPage";
|
||||
import VideoMediaPage from "./VideoMediaPage";
|
||||
|
||||
import { faStarShooting } from "@fortawesome/pro-duotone-svg-icons";
|
||||
import { faStarShooting as faStarShootingOutline } from "@fortawesome/pro-regular-svg-icons";
|
||||
|
||||
import "./MediaPage.scss";
|
||||
|
||||
export interface MediaSubViewProps {
|
||||
bookmarkButtonProps: ActionButtonProps;
|
||||
canAccessStudio: () => boolean;
|
||||
canBanUsers: () => boolean;
|
||||
canEdit: boolean;
|
||||
featureButtonProps: ActionButtonProps;
|
||||
isFeatured: boolean;
|
||||
isModerator: boolean;
|
||||
mediaFile?: MediaFile;
|
||||
openDeleteModal: () => void;
|
||||
prompt?: Prompt;
|
||||
ratingButtonProps: ActionButtonProps;
|
||||
status: FetchStatus;
|
||||
title: string;
|
||||
titleSet: (title: string) => void;
|
||||
urlToken: string;
|
||||
}
|
||||
|
||||
export default function MediaPageSwitch() {
|
||||
const { token: urlToken } = useParams<{ token: string }>();
|
||||
const { canAccessStudio, canBanUsers, canEditMediaFile, user } = useSession();
|
||||
const bookmarks = useBookmarks();
|
||||
const ratings = useRatings();
|
||||
|
||||
const [isFeatured, isFeaturedSet] = useState(false);
|
||||
const [title, titleSet] = useState("");
|
||||
|
||||
const { mediaFile, prompt, remove, status } = useMedia({
|
||||
mediaToken: urlToken,
|
||||
onSuccess: (res: any) => {
|
||||
isFeaturedSet(res.is_featured);
|
||||
titleSet(res.maybe_title || "Untitled Video");
|
||||
ratings.gather({ res, key: "token" });
|
||||
bookmarks.gather({ res, key: "token" });
|
||||
},
|
||||
});
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const canEdit = canEditMediaFile(urlToken);
|
||||
const isModerator = canBanUsers() || false;
|
||||
|
||||
const closeDeleteModal = () => setIsDeleteModalOpen(false);
|
||||
const openDeleteModal = () => setIsDeleteModalOpen(true);
|
||||
const deleteMedia = () => remove(!!user?.can_ban_users);
|
||||
|
||||
const bookmarkButtonProps: ActionButtonProps = {
|
||||
...bookmarks.makeProps({
|
||||
entityToken: urlToken,
|
||||
entityType: "media_file",
|
||||
}),
|
||||
toolTipOff: "Add to bookmarks",
|
||||
toolTipOn: "Remove bookmark",
|
||||
};
|
||||
|
||||
const ratingButtonProps: ActionButtonProps = {
|
||||
...ratings.makeProps({
|
||||
entityToken: urlToken,
|
||||
entityType: "media_file",
|
||||
}),
|
||||
toolTipOff: "Like this",
|
||||
toolTipOn: "Unlike this",
|
||||
};
|
||||
|
||||
const featureButtonProps: ActionButtonProps = {
|
||||
actionType: "feature",
|
||||
isToggled: isFeatured,
|
||||
labelOff: "Feature",
|
||||
labelOn: "Featured",
|
||||
iconOn: faStarShooting,
|
||||
iconOff: faStarShootingOutline,
|
||||
toggle: () => {
|
||||
(isFeatured ? DeleteFeaturedItem : CreateFeaturedItem)("", {
|
||||
entity_type: "media_file",
|
||||
entity_token: mediaFile?.token || "",
|
||||
}).then(() => {
|
||||
isFeaturedSet((current: boolean) => {
|
||||
return !current;
|
||||
});
|
||||
});
|
||||
},
|
||||
toolTipOff: "Feature this",
|
||||
toolTipOn: "Remove from featured",
|
||||
};
|
||||
|
||||
const subViewProps: MediaSubViewProps = {
|
||||
bookmarkButtonProps,
|
||||
canAccessStudio,
|
||||
canBanUsers,
|
||||
canEdit,
|
||||
featureButtonProps,
|
||||
isFeatured,
|
||||
isModerator,
|
||||
mediaFile,
|
||||
openDeleteModal,
|
||||
prompt,
|
||||
ratingButtonProps,
|
||||
status,
|
||||
title,
|
||||
titleSet,
|
||||
urlToken,
|
||||
};
|
||||
|
||||
const MediaSwitch = () => {
|
||||
switch (mediaFile?.media_type) {
|
||||
case MediaFileType.Video:
|
||||
return <VideoMediaPage {...subViewProps} />;
|
||||
default:
|
||||
return <MediaPage {...subViewProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container type="panel" className="mb-5">
|
||||
<Panel clear={true} className="py-4 media-page-container d-flex">
|
||||
{MediaSwitch()}
|
||||
</Panel>
|
||||
</Container>
|
||||
<Modal
|
||||
{...{
|
||||
content: () => (
|
||||
<>{`Are you sure you want to delete this media file? This action cannot be undone.`}</> // replace w/ dynamic later -V
|
||||
// <>{`Are you sure you want to delete "${title}"? This action cannot be undone.`}</>
|
||||
),
|
||||
handleClose: closeDeleteModal,
|
||||
onConfirm: deleteMedia,
|
||||
show: isDeleteModalOpen,
|
||||
title: "Delete Media",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,20 +3,20 @@ import { MediaFile } from "@storyteller/components/src/api/media_files/GetMediaF
|
||||
import React from "react";
|
||||
|
||||
interface MediaVideoComponentProps {
|
||||
mediaFile: MediaFile;
|
||||
mediaFile?: MediaFile;
|
||||
}
|
||||
|
||||
export default function MediaVideoComponent({
|
||||
mediaFile,
|
||||
}: MediaVideoComponentProps) {
|
||||
let mediaLink = new BucketConfig().getGcsUrl(
|
||||
mediaFile.public_bucket_path
|
||||
);
|
||||
let mediaLink = mediaFile
|
||||
? new BucketConfig().getGcsUrl(mediaFile.public_bucket_path)
|
||||
: "";
|
||||
|
||||
return (
|
||||
return mediaFile && mediaFile.public_bucket_path ? (
|
||||
<video className="rounded" controls width="100%" height="auto">
|
||||
<source src={mediaLink} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
|
||||
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
|
||||
import { Button, Panel } from "components/common";
|
||||
import { STYLES_BY_KEY } from "common/StyleOptions";
|
||||
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface PromptViewerProps {
|
||||
isModerator: boolean;
|
||||
mediaFile?: MediaFile;
|
||||
prompt: Prompt;
|
||||
}
|
||||
|
||||
export default function PromptViewer({
|
||||
isModerator,
|
||||
mediaFile,
|
||||
prompt,
|
||||
}: PromptViewerProps) {
|
||||
const [copyPositiveButtonText, setCopyPositiveButtonText] = useState("Copy");
|
||||
const [copyNegativeButtonText, setCopyNegativeButtonText] = useState("Copy");
|
||||
|
||||
const copyToClipboard = async (
|
||||
text: string,
|
||||
copyTextSet: React.Dispatch<React.SetStateAction<string>>
|
||||
) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyTextSet("Copied!");
|
||||
setTimeout(() => copyTextSet("Copy"), 2000); // Change back after 2 seconds
|
||||
} catch (err) {
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{prompt.maybe_positive_prompt && (
|
||||
<Panel padding={true} className="mt-3">
|
||||
<div className="d-flex gap-3 align-items-center mb-2">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Prompt</h6>
|
||||
<Button
|
||||
icon={faCopy}
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
prompt.maybe_positive_prompt || "",
|
||||
setCopyPositiveButtonText
|
||||
)
|
||||
}
|
||||
label={copyPositiveButtonText}
|
||||
variant="link"
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt.maybe_positive_prompt}</p>
|
||||
</div>
|
||||
|
||||
{prompt.maybe_negative_prompt && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">
|
||||
Negative Prompt
|
||||
</h6>
|
||||
<Button
|
||||
icon={faCopy}
|
||||
onClick={() =>
|
||||
copyToClipboard(
|
||||
prompt.maybe_negative_prompt || "",
|
||||
setCopyNegativeButtonText
|
||||
)
|
||||
}
|
||||
label={copyNegativeButtonText}
|
||||
variant="link"
|
||||
className="fs-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt.maybe_negative_prompt}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{prompt.maybe_style_name && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Style Name</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
{STYLES_BY_KEY.get(prompt.maybe_style_name)?.label}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{prompt?.used_face_detailer && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">
|
||||
Used Face Detailer
|
||||
</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
{prompt?.used_face_detailer ? "Yes" : "No"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{prompt?.used_upscaler && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Used Upscaler</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">{prompt?.used_upscaler ? "Yes" : "No"}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isModerator &&
|
||||
mediaFile?.maybe_moderator_fields
|
||||
?.maybe_style_transfer_source_media_file_token && (
|
||||
<>
|
||||
<div className="d-flex gap-3 align-items-center mb-2 mt-3">
|
||||
<h6 className="fw-semibold mb-0 flex-grow-1">Source Video</h6>
|
||||
</div>
|
||||
<div className="panel-inner p-2 rounded">
|
||||
<p className="fs-7">
|
||||
<Link
|
||||
to={`/media/${mediaFile?.maybe_moderator_fields?.maybe_style_transfer_source_media_file_token}`}
|
||||
>
|
||||
Link to source (staff only)
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import React, { useState } from "react";
|
||||
import moment from "moment";
|
||||
import { AnimationStatus, useAnimationStatus } from "hooks";
|
||||
import {
|
||||
ActionButton,
|
||||
Button,
|
||||
TempInput as Input,
|
||||
SocialButton,
|
||||
Socials,
|
||||
Spinner,
|
||||
} from "components/common";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
import { CommentComponent } from "v2/view/_common/comments/CommentComponent";
|
||||
import MediaVideoComponent from "./MediaVideoComponent";
|
||||
import {
|
||||
RenameMedia,
|
||||
RenameMediaResponse,
|
||||
} from "@storyteller/components/src/api/media_files/RenameMedia";
|
||||
import PromptViewer from "./PromptViewer";
|
||||
import { MediaSubViewProps } from "./MediaPageSwitch";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { basicTransition } from "resources";
|
||||
|
||||
import {
|
||||
faArrowRightArrowLeft,
|
||||
faCheck,
|
||||
faPen,
|
||||
faTrash,
|
||||
faX,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
export enum EditingTitleState {
|
||||
closed,
|
||||
editing,
|
||||
saving,
|
||||
}
|
||||
|
||||
export default function DevMediaPage({
|
||||
bookmarkButtonProps,
|
||||
canAccessStudio,
|
||||
canEdit,
|
||||
featureButtonProps,
|
||||
isModerator,
|
||||
mediaFile,
|
||||
openDeleteModal,
|
||||
prompt,
|
||||
ratingButtonProps,
|
||||
title,
|
||||
titleSet,
|
||||
urlToken,
|
||||
}: MediaSubViewProps) {
|
||||
const [editingTitle, editingTitleSet] = useState(EditingTitleState.closed);
|
||||
const { events, status: animationStatus } = useAnimationStatus();
|
||||
const titlePaused = animationStatus === AnimationStatus.paused;
|
||||
|
||||
const transitions = useTransition(
|
||||
editingTitle,
|
||||
basicTransition({ ...events })
|
||||
);
|
||||
|
||||
const saveTitle = () => {
|
||||
editingTitleSet(EditingTitleState.saving);
|
||||
RenameMedia(urlToken, {
|
||||
name: title,
|
||||
}).then((res: RenameMediaResponse) => {
|
||||
if (res.success) {
|
||||
editingTitleSet(EditingTitleState.closed);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// for later reference and eventual replacement
|
||||
|
||||
// const fakeVideos = [
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "Lorem ipsum",
|
||||
// creator: "Alfredo Someguy",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "The Quick Brown Fox",
|
||||
// creator: "Cheese McGee",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "An Anime Video",
|
||||
// creator: "Georgia Jones",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "Some Other Video",
|
||||
// creator: "Brain Problems",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "Kachow",
|
||||
// creator: "Mr Smith",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "Lorem ipsum",
|
||||
// creator: "Alfredo Someguy",
|
||||
// },
|
||||
// {
|
||||
// coverImage: "",
|
||||
// title: "Lorem ipsum",
|
||||
// creator: "Alfredo Someguy",
|
||||
// },
|
||||
// ];
|
||||
|
||||
const shareLinks: Socials[] = [
|
||||
"x",
|
||||
"whatsapp",
|
||||
"facebook",
|
||||
"reddit",
|
||||
"email",
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
{...{
|
||||
className: `video-page-main-content video-page-theater-mode`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-media-content",
|
||||
}}
|
||||
>
|
||||
<MediaVideoComponent mediaFile={mediaFile} />
|
||||
</div>
|
||||
<header>
|
||||
{transitions(
|
||||
(style: any, yah) =>
|
||||
[
|
||||
<a.div {...{ className: "fy-title-editor", style }}>
|
||||
<div {...{ className: "video-page-title" }}>
|
||||
<h2>{title}</h2>
|
||||
<span>{`Created ${moment(
|
||||
mediaFile?.created_at || ""
|
||||
).fromNow()}`}</span>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<Button
|
||||
{...{
|
||||
icon: faPen,
|
||||
onClick: () => {
|
||||
if (titlePaused) {
|
||||
editingTitleSet(EditingTitleState.editing);
|
||||
}
|
||||
},
|
||||
square: true,
|
||||
variant: "secondary",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</a.div>,
|
||||
<a.div {...{ className: "fy-title-editor", style }}>
|
||||
<Input
|
||||
{...{
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
titleSet(e.target.value);
|
||||
},
|
||||
value: title,
|
||||
}}
|
||||
/>{" "}
|
||||
<Button
|
||||
{...{
|
||||
square: true,
|
||||
icon: faCheck,
|
||||
onClick: () => saveTitle(),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
{...{
|
||||
square: true,
|
||||
icon: faX,
|
||||
onClick: () => {
|
||||
if (titlePaused) {
|
||||
editingTitleSet(EditingTitleState.closed);
|
||||
titleSet(mediaFile?.maybe_title || "Untitled video");
|
||||
}
|
||||
},
|
||||
variant: "secondary",
|
||||
}}
|
||||
/>
|
||||
</a.div>,
|
||||
<a.div
|
||||
{...{
|
||||
className: "fy-title-editor title-editor-spinner",
|
||||
style,
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</a.div>,
|
||||
][yah]
|
||||
)}
|
||||
<div {...{ className: "video-page-file-tools" }}>
|
||||
{canEdit && (
|
||||
<Button
|
||||
{...{
|
||||
icon: faTrash,
|
||||
square: true,
|
||||
onClick: openDeleteModal,
|
||||
variant: "danger",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canAccessStudio() && (
|
||||
<Button
|
||||
{...{
|
||||
icon: faArrowRightArrowLeft,
|
||||
label: "Style Transfer",
|
||||
to: `/style-video/${mediaFile?.token || ""}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-media-details",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-creator",
|
||||
}}
|
||||
>
|
||||
<Gravatar
|
||||
{...{
|
||||
noHeight: true,
|
||||
size: 56,
|
||||
}}
|
||||
username={mediaFile?.maybe_creator_user?.username || ""}
|
||||
email_hash={mediaFile?.maybe_creator_user?.gravatar_hash || ""}
|
||||
avatarIndex={
|
||||
mediaFile?.maybe_creator_user?.default_avatar.image_index || 0
|
||||
}
|
||||
backgroundIndex={
|
||||
mediaFile?.maybe_creator_user?.default_avatar.color_index || 0
|
||||
}
|
||||
/>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-creator-info",
|
||||
}}
|
||||
>
|
||||
<h3>{mediaFile?.maybe_creator_user?.username || ""}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-share-container",
|
||||
}}
|
||||
>
|
||||
<div {...{ className: "media-share-label" }}>Share</div>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-share-links",
|
||||
}}
|
||||
>
|
||||
{shareLinks.map((social, i) => (
|
||||
<SocialButton
|
||||
{...{
|
||||
hideLabel: true,
|
||||
social,
|
||||
shareUrl: "abc",
|
||||
shareText: "xyz",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-actions-container",
|
||||
}}
|
||||
>
|
||||
<div {...{ className: "media-share-label" }}>Actions</div>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-share-links",
|
||||
}}
|
||||
>
|
||||
<ActionButton {...ratingButtonProps} />
|
||||
<ActionButton {...bookmarkButtonProps} />
|
||||
{isModerator && <ActionButton {...featureButtonProps} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{prompt && (
|
||||
<PromptViewer
|
||||
{...{
|
||||
prompt,
|
||||
isModerator,
|
||||
mediaFile,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div {...{ className: "video-page-comments panel" }}>
|
||||
<h5>Comments</h5>
|
||||
<CommentComponent
|
||||
entityType="media_file"
|
||||
entityToken={mediaFile?.token || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div
|
||||
{...{
|
||||
className: "video-page-related-videos",
|
||||
}}
|
||||
>
|
||||
{fakeVideos.map((relatedVideo, i) => (
|
||||
<div {...{ className: "video-page-related-item" }}>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-related-cover-image",
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
{...{
|
||||
className: "video-page-related-details",
|
||||
}}
|
||||
>
|
||||
<h6>{relatedVideo.title}</h6>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>*/}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user