mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 21:32:18 +00:00
edit weight page cover image input functional and presentable
This commit is contained in:
1 parent
9ab82f3874
commit
142111c49b
26 files changed
+411
-201
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
|
||||
export interface UpdateWeightRequest {
|
||||
cover_image_media_file_token: string;
|
||||
description_markdown: string,
|
||||
description_rendered_html: string,
|
||||
title: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
import { Check } from "components/common";
|
||||
import { Check } from "components/svg";
|
||||
import { useHover, useId } from "hooks";
|
||||
import makeClass from "resources/makeClass";
|
||||
import "./Checkbox.scss";
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from 'react';
|
||||
import { a, useSpring } from '@react-spring/web';
|
||||
import { FontAwesomeIcon as Icon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import "./FileDetails.scss";
|
||||
|
||||
interface Props {
|
||||
clear?: (file?: any) => void;
|
||||
disabled?: boolean;
|
||||
file?: any;
|
||||
hideClearDetails?: boolean;
|
||||
icon?: any;
|
||||
}
|
||||
|
||||
export default function FileDetails({ clear = () => {}, file, hideClearDetails, icon }: Props) {
|
||||
export default function FileDetails({ clear = () => {}, disabled, file, hideClearDetails, icon }: Props) {
|
||||
const fileSize =
|
||||
file && file.size >= 1024 * 1024
|
||||
? (file.size / 1024 / 1024).toFixed(2) + " MB"
|
||||
@@ -18,7 +20,12 @@ export default function FileDetails({ clear = () => {}, file, hideClearDetails,
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
return <div {...{ className: "fy-uploader-layout upload-details" }}>
|
||||
const style = useSpring({
|
||||
config: { mass: 1, tension: 120, friction: 14 },
|
||||
opacity: disabled ? 0 : 1
|
||||
});
|
||||
|
||||
return <a.div {...{ className: "fy-uploader-layout upload-details", style }}>
|
||||
{ icon && <Icon {...{ className: "fy-uploader-layout-icon", icon }}/> }
|
||||
<div>
|
||||
<div {...{ className: "filename" }}>
|
||||
@@ -32,5 +39,5 @@ export default function FileDetails({ clear = () => {}, file, hideClearDetails,
|
||||
{ !hideClearDetails && <button {...{ className: "upload-details-clear btn btn-destructive align-items-center justify-content-center", onClick: e => { e.preventDefault(); clear() } }}>
|
||||
<Icon {...{ icon: faTrash }}/>
|
||||
</button> }
|
||||
</div>;
|
||||
</a.div>;
|
||||
};
|
||||
@@ -42,6 +42,11 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
height: 10rem;
|
||||
opacity: .15;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
// background-color: red;
|
||||
fill: $secondary;
|
||||
|
||||
@@ -2,18 +2,23 @@ import React, { useState } from "react";
|
||||
import { a, useSpring } from '@react-spring/web';
|
||||
import { FileDetails, FileWrapper, FileLabel } from "components/common";
|
||||
import { faImagePortrait } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import "./ImageInput.scss";
|
||||
|
||||
const fileTypes = ["JPG", "GIF", "PNG"];
|
||||
|
||||
interface Props {
|
||||
blob?: string;
|
||||
children?: any;
|
||||
clear?: (file?: any) => void;
|
||||
disabled: boolean;
|
||||
file?: any;
|
||||
hideActions?: boolean;
|
||||
hideClearDetails?: boolean;
|
||||
inputProps?: any;
|
||||
onRest?: () => void;
|
||||
placeholderIcon?: IconDefinition;
|
||||
success?: boolean;
|
||||
submit?: () => void;
|
||||
working?: boolean;
|
||||
@@ -22,7 +27,7 @@ interface Props {
|
||||
|
||||
const n = () => {};
|
||||
|
||||
export default function ImageInput({ blob = "", clear = n, file, hideActions, hideClearDetails, inputProps, onRest = n, success = false, submit = n, working = false, ...rest }: Props) {
|
||||
export default function ImageInput({ blob = "", children, clear = n, disabled, file, hideActions, hideClearDetails, inputProps, onRest = n, placeholderIcon, success = false, submit = n, working = false, ...rest }: Props) {
|
||||
const [loaded,loadedSet] = useState<number>();
|
||||
const onLoad = () => loadedSet(1);
|
||||
const style = useSpring({
|
||||
@@ -31,15 +36,17 @@ export default function ImageInput({ blob = "", clear = n, file, hideActions, hi
|
||||
opacity: loaded ? 1 : 0
|
||||
});
|
||||
|
||||
return <FileWrapper {...{ fileTypes, panelClass: "image-upload-frame", ...inputProps, ...rest }}>
|
||||
return <FileWrapper {...{ disabled, fileTypes, panelClass: "image-upload-frame", ...inputProps, ...rest }}>
|
||||
<div {...{ className: "fy-image-uploader" }}>
|
||||
{ file ? <>
|
||||
<a.img {...{ alt: "file preview", className: "file-preview", onLoad, src: blob, style }} />
|
||||
<FileDetails {...{ clear, file, hideClearDetails, icon: faImagePortrait }}/>
|
||||
<FileDetails {...{ clear, disabled, file, hideClearDetails, icon: faImagePortrait }}/>
|
||||
{ children }
|
||||
</> : <>
|
||||
<svg {...{ className: "image-placeholder", height: 400, viewBox: "0 0 300 300", width: 400 }}>
|
||||
<path d="m152.42 226c41.8 0 69.49-43.57 73.21-90.87 3.78-47.97-26.06-87.13-75.69-87.13-49.64 0-79.28 39.15-75.69 87.13 3.72 49.79 36.37 90.87 78.17 90.87zm-29.19-50.93c1.62-1.51 4.15-1.42 5.65.2 4.1 4.41 15.31 6.97 21.88 6.97s17.77-2.56 21.88-6.97c1.5-1.62 4.04-1.71 5.65-.2 1.62 1.5 1.92 3.95.2 5.65-6.31 6.27-18.31 9.52-27.74 9.52s-22.16-3.54-27.74-9.52c-1.49-1.61-1.4-4.14.22-5.65zm115.25 112.93h-176.62c-37.67 0-38.24-17.41-27.56-26.36 21.49-18.01 57.18-23.64 114.81-23.64 60.01 0 96.24 4.84 117.41 24.06 10.09 9.16 7.67 25.94-28.04 25.94z"/>
|
||||
</svg>
|
||||
{ placeholderIcon ? <FontAwesomeIcon {...{ className: "placeholder-icon", icon: placeholderIcon }} /> :
|
||||
<svg {...{ className: "image-placeholder", height: 400, viewBox: "0 0 300 300", width: 400 }}>
|
||||
<path d="m152.42 226c41.8 0 69.49-43.57 73.21-90.87 3.78-47.97-26.06-87.13-75.69-87.13-49.64 0-79.28 39.15-75.69 87.13 3.72 49.79 36.37 90.87 78.17 90.87zm-29.19-50.93c1.62-1.51 4.15-1.42 5.65.2 4.1 4.41 15.31 6.97 21.88 6.97s17.77-2.56 21.88-6.97c1.5-1.62 4.04-1.71 5.65-.2 1.62 1.5 1.92 3.95.2 5.65-6.31 6.27-18.31 9.52-27.74 9.52s-22.16-3.54-27.74-9.52c-1.49-1.61-1.4-4.14.22-5.65zm115.25 112.93h-176.62c-37.67 0-38.24-17.41-27.56-26.36 21.49-18.01 57.18-23.64 114.81-23.64 60.01 0 96.24 4.84 117.41 24.06 10.09 9.16 7.67 25.94-28.04 25.94z"/>
|
||||
</svg> }
|
||||
<div {...{ className: "image-upload-label" }}>
|
||||
{ !file && <FileLabel {...{ fileTypes }} /> }
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
interface TextAreaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
}
|
||||
|
||||
export default function TempTextArea({ label, textArea, ...rest }: TextAreaProps) {
|
||||
return <>
|
||||
{ label && <label className="sub-title">{ label }</label>}
|
||||
<div className="form-group">
|
||||
<textarea className="form-control" { ...rest } />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TempTextArea";
|
||||
@@ -2,8 +2,8 @@ import AudioBlobPreview from "./AudioBlobPreview";
|
||||
import AudioInput from "./AudioInput";
|
||||
import BasicVideo from "./BasicVideo";
|
||||
import Button from "./Button";
|
||||
import Check from "./Check";
|
||||
import Checkbox from "./Checkbox";
|
||||
import Container from "./Container";
|
||||
import FileActions from "./FileActions";
|
||||
import FileDetails from "./FileDetails";
|
||||
import FileWrapper from "./FileWrapper";
|
||||
@@ -15,19 +15,22 @@ import Panel from "./Panel";
|
||||
import Radio from "./Radio";
|
||||
import SegmentButtons from "./SegmentButtons";
|
||||
import Select from "./Select";
|
||||
import Skeleton from "./Skeleton";
|
||||
import Slider from "./Slider";
|
||||
import Spinner from "./Spinner";
|
||||
import SplitPanel from "./SplitPanel";
|
||||
import TempAudioPlayer from "./TempAudioPlayer";
|
||||
import TempInput from "./TempInput";
|
||||
import TempSelect from "./TempSelect";
|
||||
import TempTextArea from "./TempTextArea";
|
||||
|
||||
export {
|
||||
AudioBlobPreview,
|
||||
AudioInput,
|
||||
BasicVideo,
|
||||
Button,
|
||||
Check,
|
||||
Checkbox,
|
||||
Container,
|
||||
FileActions,
|
||||
FileDetails,
|
||||
FileWrapper,
|
||||
@@ -39,9 +42,12 @@ export {
|
||||
Radio,
|
||||
SegmentButtons,
|
||||
Select,
|
||||
Skeleton,
|
||||
Slider,
|
||||
Spinner,
|
||||
SplitPanel,
|
||||
TempAudioPlayer,
|
||||
TempInput,
|
||||
TempSelect
|
||||
TempSelect,
|
||||
TempTextArea
|
||||
};
|
||||
+25
-60
@@ -1,72 +1,37 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
a.face-animator-job {
|
||||
&:hover {
|
||||
background-color: $panel-inner;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $panel-inner;
|
||||
}
|
||||
}
|
||||
|
||||
.face-animator-job {
|
||||
border-radius: .5rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
margin-top: .5rem;
|
||||
padding: .5rem;
|
||||
|
||||
.job-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
margin-top: .5rem;
|
||||
padding: .5rem;
|
||||
// height: 100%;
|
||||
width: min-content;
|
||||
|
||||
svg {
|
||||
// background-color: rgba(#ccc, .05);
|
||||
margin-right: .5rem;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
circle, line, polyline {
|
||||
.work-indicator { margin-right: .5rem; }
|
||||
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: round;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
polyline {
|
||||
transform: translate(2px,2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ckw {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.work-indicator-circle-track {
|
||||
stroke: rgba(#fff, .15);
|
||||
stroke-width: 4px;
|
||||
}
|
||||
.work-indicator-circle-marker {
|
||||
transform-origin: 18px 18px;
|
||||
animation: ckw 1.5s linear infinite;
|
||||
// strokeDashoffset: 4;
|
||||
// stroke: #fff;
|
||||
}
|
||||
|
||||
.job-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
// height: 100%;
|
||||
width: min-content;
|
||||
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: #fff;
|
||||
line-height: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap
|
||||
}
|
||||
span { font-weight: lighter; line-height: 1rem; }
|
||||
h6 {
|
||||
color: #fff;
|
||||
line-height: 1rem;
|
||||
height: 1rem;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
span { font-weight: lighter; line-height: 1rem; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
import { Check } from 'components/common';
|
||||
import { WorkIndicator } from 'components/svg';
|
||||
import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
|
||||
import { JobState } from "@storyteller/components/src/jobs/JobStates";
|
||||
|
||||
@@ -15,27 +15,8 @@ interface JobListItem extends InferenceJob {
|
||||
t?: any
|
||||
}
|
||||
|
||||
const AniX = ({ checked = false }) => {
|
||||
const sharedStyle = {
|
||||
config: { tension: 280, friction: 45 },
|
||||
strokeDashoffset: 1,
|
||||
strokeDasharray: checked ? '13, 0' : '0,13'
|
||||
};
|
||||
const line1 = useSpring({
|
||||
...sharedStyle,
|
||||
});
|
||||
const line2 = useSpring({
|
||||
delay: 450,
|
||||
...sharedStyle
|
||||
});
|
||||
|
||||
return <>
|
||||
<a.line {...{ style: line1, x1: 14, y1: 14, x2: 22, y2: 22 }}/>
|
||||
<a.line {...{ style: line2, x1: 22, y1: 14, x2: 14, y2: 22 }}/>
|
||||
</>;
|
||||
}
|
||||
|
||||
const OuterItem = ({ className, children, isComplete, jobToken, maybeResultToken, onSelect = () => {}, refSet }: { className?: string, children?: any, jobToken: string, isComplete: boolean, maybeResultToken?: any, onSelect?: any, refSet?: any }) => isComplete ?
|
||||
const OuterItem = ({ className, children, success, jobToken, maybeResultToken, onSelect = () => {}, refSet }: { className?: string, children?: any, jobToken: string, success: boolean, maybeResultToken?: any, onSelect?: any, refSet?: any }) => success ?
|
||||
<a.a {...{ className, href: `/media/${maybeResultToken}`, id: `ijobitem-${ jobToken }`, onClick: () => onSelect(), ref: refSet }}>{ children }</a.a> :
|
||||
<a.div {...{ className, id: `ijobitem-${ jobToken }`, ref: refSet }}>{ children }</a.div>;
|
||||
|
||||
@@ -48,7 +29,7 @@ export default function JobItem({ failures, maybeFailureCategory, maybeResultTok
|
||||
const jobType = FrontendInferenceJobType[inputType];
|
||||
const jobStatus = jobStatusDescription(jobState);
|
||||
const jobStatusClass = jobStatus.toLowerCase().replace("_","-");
|
||||
const circle = { cx: 18, cy: 18, r: 14, strokeWidth: 4 };
|
||||
|
||||
const dashStatus = () => {
|
||||
switch (jobState) {
|
||||
case JobState.COMPLETE_SUCCESS:
|
||||
@@ -61,19 +42,10 @@ export default function JobItem({ failures, maybeFailureCategory, maybeResultTok
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
const isComplete = jobState === JobState.COMPLETE_SUCCESS;
|
||||
const isProblemo = jobState === JobState.COMPLETE_FAILURE || jobState === JobState.DEAD;
|
||||
const dashes = [ // the arity (amount of numbers) must remain the same or react-spring will freakout
|
||||
"1 7 1 7 1 70", // UKNOWN / PENDING - 3x 1pt strokes with 7 point gaps, then a big 70pt gap
|
||||
"6 0 6 0 6 70", // STARTED / ATTEMPT_FAILED - 6pt strokes with 0pt gaps to appear as one, big gap
|
||||
"0 0 50 0 0 0" // COMPLETE_SUCCESS / COMPLETE_FAILURE / DEAD - One big 50pt stroke
|
||||
];
|
||||
const success = jobState === JobState.COMPLETE_SUCCESS;
|
||||
const failure = jobState === JobState.COMPLETE_FAILURE || jobState === JobState.DEAD;
|
||||
const [bounce,bounceSet] = useState(false);
|
||||
const dashy = useSpring({
|
||||
config: { tension: 280, friction: 60 },
|
||||
opacity: [.5,1,1][dashStatus()],
|
||||
strokeDasharray: dashes[dashStatus()]
|
||||
});
|
||||
|
||||
const makeBounce = (amount = 0, delay = 0) => ({
|
||||
delay,
|
||||
config: { tension: 250, friction: 12 },
|
||||
@@ -84,27 +56,21 @@ export default function JobItem({ failures, maybeFailureCategory, maybeResultTok
|
||||
const subtitle = maybeFailureCategory ?`${ failures(maybeFailureCategory) }` : t(`subtitles.${jobStatus}`);
|
||||
const className = `face-animator-job job-status-${jobStatusClass}`;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!bounce && !hasBounced && isComplete) {
|
||||
if (!bounce && !hasBounced && success) {
|
||||
hasBouncedSet(true);
|
||||
bounceSet(true);
|
||||
setTimeout(() => bounceSet(false),250);
|
||||
}
|
||||
},[bounce, hasBounced, isComplete ]);
|
||||
},[bounce, hasBounced, success ]);
|
||||
|
||||
return <OuterItem {...{ className, isComplete, jobToken, maybeResultToken, onSelect, refSet }}>
|
||||
<svg {...{ }}>
|
||||
<circle {...{ ...circle, className: "work-indicator-circle-track" }} />
|
||||
<a.circle {...{ ...circle, className: "work-indicator-circle-marker", style: dashy }} />
|
||||
<AniX checked={isProblemo}/>
|
||||
<Check checked={isComplete}/>
|
||||
</svg>
|
||||
return <OuterItem {...{ className, jobToken, maybeResultToken, onSelect, refSet, success }}>
|
||||
<WorkIndicator {...{ failure, stage: dashStatus(), success }}/>
|
||||
<div {...{ className: "job-details" }}>
|
||||
<a.h6 {...{ style: headingBounce }}>
|
||||
{ t(`${jobType}.${jobStatus}`) }
|
||||
</a.h6>
|
||||
<a.span {...{ style: subtitleBounce }}>{ isComplete ? subtitle + " >" : subtitle }</a.span>
|
||||
<a.span {...{ style: subtitleBounce }}>{ success ? subtitle + " >" : subtitle }</a.span>
|
||||
</div>
|
||||
</OuterItem>;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
|
||||
interface Props {
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export default function AniX({ checked = false }: Props) {
|
||||
const sharedStyle = {
|
||||
config: { tension: 280, friction: 45 },
|
||||
strokeDashoffset: 1,
|
||||
strokeDasharray: checked ? '13, 0' : '0,13'
|
||||
};
|
||||
const line1 = useSpring({
|
||||
...sharedStyle,
|
||||
});
|
||||
const line2 = useSpring({
|
||||
delay: 450,
|
||||
...sharedStyle
|
||||
});
|
||||
|
||||
return <>
|
||||
<a.line {...{ style: line1, x1: 14, y1: 14, x2: 22, y2: 22 }}/>
|
||||
<a.line {...{ style: line2, x1: 22, y1: 14, x2: 14, y2: 22 }}/>
|
||||
</>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './AniX';
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { a, useSpring } from "@react-spring/web";
|
||||
import { circle } from "../sharedSVGProps";
|
||||
|
||||
interface Props {
|
||||
className?: string,
|
||||
stage: number
|
||||
}
|
||||
|
||||
export default function DashedCircle({ className = "", stage = 0 }: Props) {
|
||||
const dashes = [ // the arity (amount of numbers) must remain the same or react-spring will freakout
|
||||
"1 7 1 7 1 70", // UKNOWN / PENDING - 3x 1pt strokes with 7 point gaps, then a big 70pt gap
|
||||
"6 0 6 0 6 70", // STARTED / ATTEMPT_FAILED - 6pt strokes with 0pt gaps to appear as one, big gap
|
||||
"0 0 50 0 0 0" // COMPLETE_SUCCESS / COMPLETE_FAILURE / DEAD - One big 50pt stroke
|
||||
];
|
||||
const style = useSpring({
|
||||
config: { tension: 280, friction: 60 },
|
||||
opacity: [.5,1,1][stage],
|
||||
strokeDasharray: dashes[stage]
|
||||
});
|
||||
|
||||
return <a.circle {...{ ...circle, className, style }} />;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DashedCircle';
|
||||
@@ -0,0 +1,28 @@
|
||||
.work-indicator {
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
fill: none;
|
||||
|
||||
circle, line, polyline {
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: round;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
polyline { transform: translate(2px,2px); }
|
||||
|
||||
.work-indicator-circle-track {
|
||||
stroke: rgba(#fff, .15);
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.work-indicator-circle-marker {
|
||||
transform-origin: 18px 18px;
|
||||
animation: work-indicator-spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes work-indicator-spin {
|
||||
0% { transform: rotate(0deg) }
|
||||
100% { transform: rotate(360deg) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { AniX, Check, DashedCircle } from 'components/svg';
|
||||
import { circle } from "../sharedSVGProps";
|
||||
import "./WorkIndicator.scss";
|
||||
|
||||
interface Props {
|
||||
failure: boolean,
|
||||
stage: number,
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export default function WorkIndicator({ failure = false, stage = 0, success = false }: Props) {
|
||||
return <svg {...{ className: "work-indicator" }}>
|
||||
<circle {...{ ...circle, className: "work-indicator-circle-track" }} />
|
||||
<DashedCircle {...{ className: "work-indicator-circle-marker", stage }}/>
|
||||
<AniX {...{ checked: failure }}/>
|
||||
<Check {...{ checked: success }}/>
|
||||
</svg>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './WorkIndicator';
|
||||
@@ -0,0 +1,11 @@
|
||||
import AniX from "./AniX";
|
||||
import Check from "./Check";
|
||||
import DashedCircle from "./DashedCircle";
|
||||
import WorkIndicator from "./WorkIndicator";
|
||||
|
||||
export {
|
||||
AniX,
|
||||
Check,
|
||||
DashedCircle,
|
||||
WorkIndicator
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const circle = { cx: 18, cy: 18, r: 14, strokeWidth: 4 };
|
||||
@@ -4,6 +4,9 @@ import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetch
|
||||
import { GetWeight, Weight } from "@storyteller/components/src/api/weights/GetWeight";
|
||||
import { UpdateWeight } from "@storyteller/components/src/api/weights/UpdateWeight";
|
||||
import { DeleteWeight } from "@storyteller/components/src/api/weights/DeleteWeight";
|
||||
import { UploadMedia, UploadMediaResponse } from "@storyteller/components/src/api/media_files/UploadMedia";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useFile } from "hooks";
|
||||
|
||||
interface Props {
|
||||
onRemove?: (x: any) => void;
|
||||
@@ -17,6 +20,9 @@ export default function useWeightFetch({ onRemove = () => {}, token }: Props) {
|
||||
const [title, titleSet] = useState("");
|
||||
const [visibility, visibilitySet] = useState("public");
|
||||
const [descriptionMD, descriptionMDSet] = useState("");
|
||||
const [imgMediaFile, imgMediaFileSet] = useState("");
|
||||
const [imgUploadStatus, imgUploadStatusSet] = useState(FetchStatus.ready);
|
||||
const imageProps = useFile({});
|
||||
const isLoading = status === FetchStatus.ready || status === FetchStatus.in_progress;
|
||||
const fetchError = status === FetchStatus.error;
|
||||
const history = useHistory();
|
||||
@@ -29,6 +35,7 @@ export default function useWeightFetch({ onRemove = () => {}, token }: Props) {
|
||||
const update = () => {
|
||||
writeStatusSet(FetchStatus.in_progress);
|
||||
UpdateWeight(token,{
|
||||
cover_image_media_file_token: imgMediaFile,
|
||||
description_markdown: descriptionMD,
|
||||
description_rendered_html: data?.description_rendered_html || "",
|
||||
title,
|
||||
@@ -58,6 +65,25 @@ export default function useWeightFetch({ onRemove = () => {}, token }: Props) {
|
||||
onRemove(res);
|
||||
});
|
||||
};
|
||||
|
||||
const uploadCoverImg = (e: any) => {
|
||||
console.log("🧲",!!imageProps.file, imgUploadStatus);
|
||||
// e.stopPropigation();
|
||||
if (imageProps.file && imgUploadStatus < 2) {
|
||||
imgUploadStatusSet(FetchStatus.in_progress);
|
||||
UploadMedia({
|
||||
uuid_idempotency_token: uuidv4(),
|
||||
file: imageProps.file,
|
||||
source: "file",
|
||||
}) // if there an audio file it uploads here
|
||||
.then((res: UploadMediaResponse) => {
|
||||
if ("media_file_token" in res) {
|
||||
imgUploadStatusSet(FetchStatus.success);
|
||||
imgMediaFileSet(res.media_file_token);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (token && !data && status === FetchStatus.ready) {
|
||||
@@ -78,5 +104,5 @@ export default function useWeightFetch({ onRemove = () => {}, token }: Props) {
|
||||
}
|
||||
}, [status, token, data]);
|
||||
|
||||
return { data, fetchError, isLoading, descriptionMD, onChange, remove, status, title, update, visibility, writeStatus };
|
||||
return { data, fetchError, imgMediaFile, imgMediaFileSet, imageProps, imgUploadStatus, isLoading, descriptionMD, onChange, remove, status, title, update, uploadCoverImg, visibility, writeStatus };
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
$under992: max-width 992px;
|
||||
$over992: min-width 992px;
|
||||
@@ -1,9 +1,7 @@
|
||||
@import "scss/_breakpoint.scss";
|
||||
@import "scss/breakpoints.scss";
|
||||
@import "scss/colours.scss";
|
||||
|
||||
$under992: max-width 992px;
|
||||
$over992: min-width 992px;
|
||||
|
||||
@mixin FAltTxt { color: #b6b6bb; }
|
||||
@mixin FApanel {
|
||||
padding: 1.5rem;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
@import "scss/_breakpoint.scss";
|
||||
@import "scss/breakpoints.scss";
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.weight-editor {
|
||||
display: flex;
|
||||
|
||||
@include breakpoint($under992) {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
// background-color: red;
|
||||
|
||||
& > * {
|
||||
flex-shrink: 0;
|
||||
// margin-right: 1rem;
|
||||
@include breakpoint($over992) {
|
||||
width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@include breakpoint($under992) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 10rem;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
// background-color: purple;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
// background-color: green;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.fy-cover-img-input {
|
||||
// background-color: red;
|
||||
position: relative;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
|
||||
.fy-cover-control {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.upload-control-btn {
|
||||
background-color: rgba($secondary, .8);
|
||||
backdrop-filter: blur(2px);
|
||||
border: none;
|
||||
margin: auto auto 1rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.weight-initial-cover-img {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: .5rem;
|
||||
position: absolute;
|
||||
// display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
// padding-bottom: 100%;
|
||||
|
||||
div {
|
||||
// background-color: red;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.upload-control-indicator {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,42 @@
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faEye,
|
||||
// faLanguage,
|
||||
faWaveform,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { faCircleExclamation, faEye, faImage, faWaveform } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
|
||||
import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Container from "components/common/Container";
|
||||
import TempInput from "components/common/TempInput";
|
||||
import { Button, TempSelect } from "components/common";
|
||||
// import useVoiceRequests from "./useVoiceRequests";
|
||||
// import { useHistory } from "react-router-dom";
|
||||
import { Button, Container, Panel, Skeleton, SplitPanel,
|
||||
ImageInput, TempInput, TempSelect, TempTextArea } from "components/common";
|
||||
import { WorkIndicator } from "components/svg";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import TextArea from "components/common/TextArea";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
|
||||
import { useWeightFetch } from "hooks";
|
||||
import SplitPanel from "components/common/SplitPanel";
|
||||
import Skeleton from "components/common/Skeleton";
|
||||
import "./WeightEditPage.scss";
|
||||
|
||||
interface WeightEditPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
// UploadControl will eventually live elsewhere
|
||||
const UploadControl = ({ onClick, status }:{ onClick: (x: any) => any, status: FetchStatus }) => {
|
||||
const failure = status === FetchStatus.error;
|
||||
const success = status === FetchStatus.success;
|
||||
|
||||
return status === FetchStatus.ready ? <Button {...{
|
||||
className: "upload-control-btn",
|
||||
label: "Upload image",
|
||||
onClick,
|
||||
variant: "secondary"
|
||||
}}/> : <div {...{ className: "upload-control-indicator" }}>
|
||||
<WorkIndicator {...{
|
||||
failure,
|
||||
stage: success ? 2 : 1,
|
||||
success
|
||||
}}/>
|
||||
<span>{ success ? "Cover image uploaded" : "Uploading ..." }</span>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default function WeightEditPage({
|
||||
sessionWrapper,
|
||||
}: WeightEditPageProps) {
|
||||
@@ -34,30 +47,32 @@ export default function WeightEditPage({
|
||||
const [weightCreatorToken] = useState("");
|
||||
|
||||
const {
|
||||
// data: weight,
|
||||
data: weight,
|
||||
descriptionMD,
|
||||
// fetchError,
|
||||
// imgMediaFile,
|
||||
imageProps,
|
||||
imgUploadStatus,
|
||||
isLoading,
|
||||
onChange,
|
||||
title,
|
||||
uploadCoverImg,
|
||||
update,
|
||||
visibility,
|
||||
} = useWeightFetch({ token: weight_token });
|
||||
|
||||
usePrefixedDocumentTitle("Edit Voice");
|
||||
const src = new BucketConfig().getGcsUrl(weight?.maybe_cover_image_public_bucket_path || "");
|
||||
|
||||
// const onClick = () =>
|
||||
// voices
|
||||
// .update(weight_token, {
|
||||
// title,
|
||||
// creator_set_visibility: visibility,
|
||||
// ietf_language_tag: language,
|
||||
// })
|
||||
// .then((res: any) => {
|
||||
// if (res && res.success) {
|
||||
// history.push("/voice-designer");
|
||||
// }
|
||||
// });
|
||||
const [editingImg,editingImgSet] = useState(0);
|
||||
|
||||
const transitions = useTransition(editingImg, {
|
||||
config: { tension: 120, friction: 15 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
});
|
||||
|
||||
usePrefixedDocumentTitle("Edit Voice");
|
||||
|
||||
const visibilityOptions = [
|
||||
{ label: "Public", value: "public" },
|
||||
@@ -118,55 +133,44 @@ export default function WeightEditPage({
|
||||
) : (
|
||||
<SplitPanel dividerFooter={true}>
|
||||
<SplitPanel.Body padding={true}>
|
||||
<div className="d-flex flex-column flex-lg-row gap-4">
|
||||
{/* Replace this with an image component component */}
|
||||
<div className="bg-secondary rounded p-3">
|
||||
cover image select component here
|
||||
<div {...{ className: "weight-editor w-100 d-flex" }}>
|
||||
<div {...{ className: "weight-editor-column" }}>
|
||||
<div {...{ className: "fy-cover-img-input" }}> { weight?.maybe_cover_image_public_bucket_path &&
|
||||
transitions((style, i) => !i ? <a.div {...{
|
||||
className: "weight-initial-cover-img",
|
||||
style: { ...style, backgroundImage: `url(${src})`
|
||||
} }}>
|
||||
<div>
|
||||
<Button {...{
|
||||
className: "upload-control-btn",
|
||||
label: "Change cover image",
|
||||
onClick: () => editingImgSet(1),
|
||||
variant: "secondary"
|
||||
}}/>
|
||||
</div>
|
||||
</a.div> :
|
||||
<a.div {...{ style }}>
|
||||
<ImageInput {...{
|
||||
...imageProps,
|
||||
disabled: imgUploadStatus > 1,
|
||||
placeholderIcon: faImage
|
||||
}}>
|
||||
<div {...{ className: "fy-cover-control" }}>
|
||||
<UploadControl {...{ onClick: uploadCoverImg, status: imgUploadStatus }}/>
|
||||
</div>
|
||||
</ImageInput>
|
||||
</a.div> )
|
||||
} </div>
|
||||
</div>
|
||||
|
||||
<div className="w-100 d-flex flex-column gap-3">
|
||||
<div>
|
||||
<TempInput
|
||||
{...{
|
||||
label: "Title",
|
||||
name: "title",
|
||||
onChange,
|
||||
placeholder: "Title",
|
||||
value: title,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
{...{
|
||||
label: "Description",
|
||||
name: "descriptionMD",
|
||||
onChange,
|
||||
placeholder: "Description",
|
||||
value: descriptionMD,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
// <div className="mt-lg-2">
|
||||
// <TempSelect
|
||||
// options={[]}
|
||||
// {...{
|
||||
// icon: faLanguage,
|
||||
// label: "Language",
|
||||
// // placeholder: "Voice name",
|
||||
// // onChange: inputCtrl(languageSet),
|
||||
// // options: languages,
|
||||
// value: language,
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
}
|
||||
<div className="mt-3">
|
||||
<TempSelect
|
||||
{...{
|
||||
<div {...{ className: "weight-editor-column" }}>
|
||||
<TempInput {...{
|
||||
label: "Title",
|
||||
name: "title",
|
||||
onChange,
|
||||
placeholder: "Title",
|
||||
value: title,
|
||||
}} />
|
||||
<TempSelect {...{
|
||||
icon: faEye,
|
||||
label: "Visibility",
|
||||
name: "visibility",
|
||||
@@ -174,8 +178,15 @@ export default function WeightEditPage({
|
||||
onChange,
|
||||
placeholder: "Voice name",
|
||||
value: visibility,
|
||||
}}
|
||||
/>
|
||||
}} />
|
||||
<TempTextArea {...{
|
||||
label: "Description",
|
||||
name: "descriptionMD",
|
||||
onChange,
|
||||
placeholder: "Description",
|
||||
value: descriptionMD,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</SplitPanel.Body>
|
||||
<SplitPanel.Footer padding={true}>
|
||||
|
||||
Reference in new issue
Block a user