mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
added TintSpinner component to SplitPanel, which ise used WeightEditPage to block input while the POST is being made
This commit is contained in:
1 parent
fa4e8c7a73
commit
7aab2293b1
12 files changed
+127
-102
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import MakeRequest from "../MakeRequest";
|
||||
|
||||
export interface UpdateWeightRequest {
|
||||
cover_image_media_file_token: string;
|
||||
cover_image_media_file_token?: string;
|
||||
description_markdown: string,
|
||||
description_rendered_html: string,
|
||||
title: string,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
|
||||
import { Button, ImageInput } from "components/common";
|
||||
import { WorkIndicator } from "components/svg";
|
||||
import { faImage } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { basicTransition } from "resources";
|
||||
|
||||
interface Props {
|
||||
currentPath?: string,
|
||||
onClick: (x: any) => any,
|
||||
status: FetchStatus
|
||||
}
|
||||
|
||||
interface UploadCtrlProps {
|
||||
onClick: (x: any) => any,
|
||||
status: FetchStatus
|
||||
}
|
||||
|
||||
const UploadControl = ({ onClick, status }: UploadCtrlProps) => {
|
||||
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 CoverImageInput({ currentPath, onClick, status, ...rest }: Props) {
|
||||
const [editingImg, editingImgSet] = useState(0);
|
||||
const transitions = useTransition(editingImg, basicTransition({}));
|
||||
|
||||
return transitions((style, i) => !currentPath || i ? <a.div {...{ style }}>
|
||||
<ImageInput {...{ ...rest, disabled: status > 1, placeholderIcon: faImage }} >
|
||||
<div {...{ className: "fy-cover-control" }}>
|
||||
<UploadControl {...{ onClick, status }} />
|
||||
</div>
|
||||
</ImageInput>
|
||||
</a.div> : <a.div {...{
|
||||
className: "weight-initial-cover-img",
|
||||
style: { ...style, backgroundImage: `url(${ currentPath })` }
|
||||
}}>
|
||||
<div>
|
||||
<Button {...{
|
||||
className: "upload-control-btn",
|
||||
label: "Change cover image",
|
||||
onClick: () => editingImgSet(1),
|
||||
variant: "secondary"
|
||||
}} />
|
||||
</div>
|
||||
</a.div>);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CoverImageInput";
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { TintSpinner } from "components/common";
|
||||
|
||||
// Header, Body, and Footer Interfaces and Components
|
||||
interface HeaderProps {
|
||||
@@ -30,6 +31,7 @@ const Footer: React.FC<FooterProps> = ({ children, padding }) => {
|
||||
|
||||
// Panel Interface
|
||||
interface PanelProps {
|
||||
busy?: boolean;
|
||||
children: ReactNode;
|
||||
dividerHeader?: boolean;
|
||||
dividerFooter?: boolean;
|
||||
@@ -42,7 +44,7 @@ const SplitPanel: React.FC<PanelProps> & {
|
||||
Header: typeof Header;
|
||||
Body: typeof Body;
|
||||
Footer: typeof Footer;
|
||||
} = ({ children, dividerHeader, dividerFooter, clear, className }) => {
|
||||
} = ({ busy, children, dividerHeader, dividerFooter, clear, className }) => {
|
||||
let header: ReactNode, body: ReactNode, footer: ReactNode;
|
||||
|
||||
const panelClassName = `${clear ? "panel-clear" : "panel"} ${
|
||||
@@ -72,6 +74,7 @@ const SplitPanel: React.FC<PanelProps> & {
|
||||
{body}
|
||||
{dividerFooter && <hr className="m-0" />}
|
||||
{footer}
|
||||
{ <TintSpinner {...{ busy }} /> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import { basicTransition } from "resources";
|
||||
import { Spinner } from "components/common";
|
||||
|
||||
interface Props {
|
||||
busy?: boolean;
|
||||
}
|
||||
|
||||
export default function TintSpinner({ busy }: Props) {
|
||||
const transitions = useTransition(busy, basicTransition({}));
|
||||
|
||||
return transitions((style, i) => busy && <a.div {...{ className: "fy-tint-spinner", style }}><Spinner /></a.div>);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TintSpinner";
|
||||
@@ -4,6 +4,7 @@ import BasicVideo from "./BasicVideo";
|
||||
import Button from "./Button";
|
||||
import Checkbox from "./Checkbox";
|
||||
import Container from "./Container";
|
||||
import CoverImageInput from "./CoverImageInput";
|
||||
import FileActions from "./FileActions";
|
||||
import FileDetails from "./FileDetails";
|
||||
import FileWrapper from "./FileWrapper";
|
||||
@@ -23,6 +24,7 @@ import TempAudioPlayer from "./TempAudioPlayer";
|
||||
import TempInput from "./TempInput";
|
||||
import TempSelect from "./TempSelect";
|
||||
import TempTextArea from "./TempTextArea";
|
||||
import TintSpinner from "./TintSpinner";
|
||||
|
||||
export {
|
||||
AudioBlobPreview,
|
||||
@@ -31,6 +33,7 @@ export {
|
||||
Button,
|
||||
Checkbox,
|
||||
Container,
|
||||
CoverImageInput,
|
||||
FileActions,
|
||||
FileDetails,
|
||||
FileWrapper,
|
||||
@@ -49,5 +52,6 @@ export {
|
||||
TempAudioPlayer,
|
||||
TempInput,
|
||||
TempSelect,
|
||||
TempTextArea
|
||||
TempTextArea,
|
||||
TintSpinner
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export default function useWeightFetch({ onRemove = () => {}, token }: Props) {
|
||||
const update = () => {
|
||||
writeStatusSet(FetchStatus.in_progress);
|
||||
UpdateWeight(token,{
|
||||
cover_image_media_file_token: imgMediaFile,
|
||||
...imgMediaFile ? { cover_image_media_file_token: imgMediaFile } : {},
|
||||
description_markdown: descriptionMD,
|
||||
description_rendered_html: data?.description_rendered_html || "",
|
||||
title,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { config } from "@react-spring/web";
|
||||
|
||||
const basicTransition = ({ ...overwrite }) => ({
|
||||
config: config.gentle,
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
...overwrite
|
||||
});
|
||||
|
||||
export default basicTransition;
|
||||
@@ -1,4 +1,5 @@
|
||||
import basicTransition from './basicTransition';
|
||||
import baseColors from './baseColors';
|
||||
import springs from './springs';
|
||||
|
||||
export { baseColors, springs }
|
||||
export { basicTransition, baseColors, springs }
|
||||
@@ -85,3 +85,17 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-tint-spinner {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba($panel, .6);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: .5rem;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { a, useTransition } from "@react-spring/web";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faEye,
|
||||
faImage,
|
||||
faWaveform,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { usePrefixedDocumentTitle } from "common/UsePrefixedDocumentTitle";
|
||||
@@ -12,18 +10,16 @@ import PageHeader from "components/layout/PageHeader";
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
CoverImageInput,
|
||||
Panel,
|
||||
Skeleton,
|
||||
SplitPanel,
|
||||
ImageInput,
|
||||
TempInput,
|
||||
TempSelect,
|
||||
TempTextArea,
|
||||
} from "components/common";
|
||||
import { WorkIndicator } from "components/svg";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
|
||||
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
|
||||
import { useWeightFetch } from "hooks";
|
||||
import "./WeightEditPage.scss";
|
||||
|
||||
@@ -31,40 +27,6 @@ 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) {
|
||||
@@ -87,20 +49,11 @@ export default function WeightEditPage({
|
||||
uploadCoverImg,
|
||||
update,
|
||||
visibility,
|
||||
writeStatus
|
||||
} = useWeightFetch({ token: weight_token });
|
||||
|
||||
const src = new BucketConfig().getGcsUrl(
|
||||
weight?.maybe_cover_image_public_bucket_path || ""
|
||||
);
|
||||
|
||||
const [editingImg, editingImgSet] = useState(0);
|
||||
|
||||
const transitions = useTransition(editingImg, {
|
||||
config: { tension: 120, friction: 15 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
});
|
||||
const basePath = weight?.maybe_cover_image_public_bucket_path || "";
|
||||
const currentPath = basePath ? new BucketConfig().getGcsUrl(basePath) : "";
|
||||
|
||||
usePrefixedDocumentTitle("Edit Voice");
|
||||
|
||||
@@ -161,57 +114,18 @@ export default function WeightEditPage({
|
||||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<SplitPanel dividerFooter={true}>
|
||||
<SplitPanel {...{ busy: writeStatus > 0, dividerFooter: true }}>
|
||||
<SplitPanel.Body padding={true}>
|
||||
<div {...{ className: "weight-editor row gy-3 gx-4" }}>
|
||||
<div {...{ className: "col-12 col-lg-5" }}>
|
||||
<label className="sub-title">Cover Image</label>
|
||||
<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>
|
||||
)
|
||||
)}{" "}
|
||||
<CoverImageInput {...{
|
||||
currentPath,
|
||||
onClick: uploadCoverImg,
|
||||
status: imgUploadStatus,
|
||||
...imageProps
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<div {...{ className: "col-lg-7 order-first order-lg-last" }}>
|
||||
|
||||
Reference in new issue
Block a user