Merge branch 'main' of github.com:storytold/storyteller-board into main

This commit is contained in:
Michael Chung committed 2024-09-18 19:04:15 -04:00
commit 7e3c6efe59
19 files changed
+407 -123

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/*
Access-Control-Allow-Origin: *
+3
View File
@@ -8,5 +8,8 @@ yarn run build2
echo "Copy test files into dist"
cp -r test/* dist/
echo "Copy netlify headers config into dist"
cp _headers dist/
echo "List files after build"
find dist/
+1 -1
View File
@@ -1,7 +1,7 @@
// to do fix https://codesandbox.io/p/sandbox/react-konva-infinite-grid-kkndq?file=%2Fsrc%2Findex.js the dotted background doesn't move when draggable.
import { Engine } from "./Engine";
export { Engine as EngineType };
export const KonvaApp = async (element: HTMLDivElement) => {
export const KonvaApp = (element: HTMLDivElement) => {
const engine = new Engine(element);
engine.initializeStage("");
return engine;
+27 -18
View File
@@ -1,26 +1,33 @@
import { useCallback, useRef } from "react";
import { useRenderCounter } from "~/hooks/useRenderCounter";
// Components
import { KonvaCanvasContainer } from "./KonvaCanvasContainer";
import { ContextualButtonRetry } from "./ContextualButtonRetry";
import { ContextualToolbarImage } from "./ContextualToolbarImage";
import { ContextualLoadingBar } from "./ContextualLoadingBar";
import { SignaledToolbarMain } from "./SignaledToolbarMain";
import { SignaledDialogError } from "./SignaledComponents";
import { EngineType } from "~/KonvaApp";
import { SignaledDialogs } from "./SignaledDialogs";
import {
SignaledDialogError,
SignaledCanvasDragDropFiles,
} from "./OtherSignaledComponents";
// The KonvaApp is the root of the Konva stage
// and only entry point for anything in Konva JS
import { EngineType } from "~/KonvaApp";
import { KonvaApp } from "~/KonvaApp";
import { Button } from "~/components/ui";
import { uiAccess } from "~/signals";
// all the signal-contexts are wrapped in hooks
import { useAppUiContext } from "./contextSignals/appUi";
// common hooks
import { useRenderCounter } from "~/hooks/useRenderCounter";
export const KonvaRootComponent = ({ className }: { className: string }) => {
// This is a hook that will log the number of times the component has rerendered
// Let's make sure we only log once
useRenderCounter("KonvaRootComponent");
const appUiContext = useAppUiContext();
const engineRef = useRef<EngineType | null>(null);
const konvaContainerCallbackRef = useCallback((node: HTMLDivElement) => {
@@ -31,23 +38,25 @@ export const KonvaRootComponent = ({ className }: { className: string }) => {
return (
<>
<Button
className="absolute bottom-0 left-0"
onClick={() => {
uiAccess.dialogueError.show({
title: "Test Error",
message: "This is a test error",
});
}}
>
Error Dialogue Test
</Button>
<KonvaCanvasContainer
ref={konvaContainerCallbackRef}
className={className}
// retreive the classNames from the parent for sizing/styling
/>
<SignaledToolbarMain />
<SignaledCanvasDragDropFiles
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
/>
<SignaledToolbarMain
openAddImage={appUiContext.openAddImage}
openAddVideo={appUiContext.openAddVideo}
openAIStylize={appUiContext.openAIStylize}
/>
<SignaledDialogs
appUiSignal={appUiContext.signal}
resetAll={appUiContext.resetAll}
/>
<ContextualToolbarImage />
<ContextualButtonRetry />
<ContextualLoadingBar />
@@ -0,0 +1,75 @@
import { useCallback } from "react";
import { DialogError, CanvasDragDropFiles } from "~/components/features";
import { uiAccess } from "~/signals";
// global signals
import { dialogueError } from "~/signals/uiAccess/dialogueError";
import { getFileExtension } from "~/utilities";
// enums
import { IMAGE_FILE_TYPE, VIDEO_FILE_TYPE } from "~/constants/fileTypeEnums";
export const SignaledDialogError = () => {
const props = dialogueError.signal.value;
const { isShowing, title, message } = props;
const onClose = useCallback(() => {
dialogueError.hide();
}, []);
return (
<DialogError
isShowing={isShowing}
title={title}
message={message}
onClose={onClose}
/>
);
};
export const SignaledCanvasDragDropFiles = ({
openAddImage,
openAddVideo,
}: {
openAddImage: (file?: File) => void;
openAddVideo: (file?: File) => void;
}) => {
// const [files, setFiles] = useState<File[]>([]);
const imageFileTypes = Object.values(IMAGE_FILE_TYPE) as string[];
const videoFileTypes = Object.values(VIDEO_FILE_TYPE) as string[];
const fileTypes = [...imageFileTypes, ...videoFileTypes];
const onSetFiles = useCallback((files: File[]) => {
if (files.length < 0) {
return;
}
if (files.length > 1) {
uiAccess.dialogueError.show({
title: "Error in Adding files",
message:
"Sorry, we don't support adding multiple files at once, yet! it's coming soon.",
});
return;
}
const file = files[0];
const fileExt = getFileExtension(file);
if (imageFileTypes.includes(fileExt)) {
openAddImage(file);
return;
}
if (videoFileTypes.includes(fileExt)) {
openAddVideo(file);
return;
}
uiAccess.dialogueError.show({
title: "Error in Adding files",
message: `Sorry, we do not support this file type: .${fileExt.toUpperCase()}`,
});
return;
}, []);
return (
<CanvasDragDropFiles
className="col-span-12 col-start-1 row-span-11 row-start-1"
onSetFiles={onSetFiles}
fileTypes={fileTypes}
/>
);
};
@@ -1,21 +0,0 @@
import { useCallback } from "react";
import { dialogueError } from "~/signals/uiAccess/dialogueError";
import { DialogError } from "~/components/features";
export const SignaledDialogError = () => {
const props = dialogueError.signal.value;
const { isShowing, title, message } = props;
const onClose = useCallback(() => {
dialogueError.hide();
}, []);
return (
<DialogError
isShowing={isShowing}
title={title}
message={message}
onClose={onClose}
/>
);
};
@@ -0,0 +1,51 @@
import { Signal } from "@preact/signals-react";
import { dispatchUiEvents } from "~/signals";
import {
DialogAddImage,
DialogAddVideo,
DialogAiStylize,
} from "~/components/features";
import { AppUiSignalType } from "./contextSignals/appUi";
export const SignaledDialogs = ({
appUiSignal,
resetAll,
}: {
appUiSignal: Signal<AppUiSignalType>;
resetAll: () => void;
}) => {
return (
<>
<DialogAddImage
isOpen={appUiSignal.value.isAddImageOpen ?? false}
stagedImage={appUiSignal.value.stagedImage}
closeCallback={resetAll}
/>
<DialogAddVideo
isOpen={appUiSignal.value.isAddVideoOpen ?? false}
stagedVideo={appUiSignal.value.stagedVideo}
closeCallback={resetAll}
onUploadedVideo={(response) => {
if (!response.data) {
return;
}
dispatchUiEvents.addVideoToEngine({
url: response.data.public_bucket_url,
});
}}
/>
<DialogAiStylize
isOpen={appUiSignal.value.isAiStylizeOpen ?? false}
onRequestAIStylize={(data) => {
const { selectedArtStyle: artstyle, ...rest } = data;
dispatchUiEvents.aiStylize.dispatchRequest({
artstyle,
...rest,
});
}}
closeCallback={resetAll}
/>
</>
);
};
+14 -65
View File
@@ -1,30 +1,21 @@
import { MouseEventHandler, useCallback, useState } from "react";
import { MouseEventHandler } from "react";
import { toolbarMain } from "~/signals/uiAccess/toolbarMain";
import { dispatchers } from "~/signals/uiEvents/toolbarMain";
import { dispatchUiEvents } from "~/signals";
import {
ToolbarMain,
DialogAddImage,
DialogAddVideo,
DialogAiStylize,
} from "~/components/features";
import { ToolbarMain } from "~/components/features";
import { LoadingBar } from "~/components/ui";
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
const initialState = {
isAddVideoOpen: false,
isAddImageOpen: false,
isAiStylizeOpen: false,
};
export const SignaledToolbarMain = () => {
const [state, setState] = useState(initialState);
const closeAll = useCallback(() => {
setState(initialState);
}, []);
export const SignaledToolbarMain = ({
openAddImage,
openAddVideo,
openAIStylize,
}: {
openAddImage: () => void;
openAddVideo: () => void;
openAIStylize: () => void;
}) => {
const loadingBar = toolbarMain.loadingBar.signal.value;
const buttonProps = Object.values(ToolbarMainButtonNames).reduce(
(acc, buttonName) => {
@@ -62,51 +53,9 @@ export const SignaledToolbarMain = () => {
<ToolbarMain
disabled={toolbarMain.signal.value.disabled}
buttonProps={buttonProps}
openAddImage={() => {
setState({
...initialState, //this closes all other opened things
isAddImageOpen: true,
});
}}
openAddVideo={() => {
setState({
...initialState, //this closes all other opened things
isAddVideoOpen: true,
});
}}
openAIStylize={() => {
setState({
...initialState, //this closes all other opened things
isAiStylizeOpen: true,
});
}}
/>
<DialogAddImage
isOpen={state.isAddImageOpen ?? false}
closeCallback={closeAll}
/>
<DialogAddVideo
isOpen={state.isAddVideoOpen ?? false}
closeCallback={closeAll}
onUploadedVideo={(response) => {
if (!response.data) {
return;
}
dispatchUiEvents.addVideoToEngine({
url: response.data.public_bucket_url,
});
}}
/>
<DialogAiStylize
isOpen={state.isAiStylizeOpen ?? false}
onRequestAIStylize={(data) => {
const { selectedArtStyle: artstyle, ...rest } = data;
dispatchUiEvents.aiStylize.dispatchRequest({
artstyle,
...rest,
});
}}
closeCallback={closeAll}
openAddImage={() => openAddImage()}
openAddVideo={() => openAddVideo()}
openAIStylize={() => openAIStylize()}
/>
</div>
);
@@ -0,0 +1,50 @@
import { useCallback, useRef } from "react";
import { signal } from "@preact/signals-react";
export type AppUiSignalType = {
isAddVideoOpen: boolean;
stagedVideo: File | null;
isAddImageOpen: boolean;
stagedImage: File | null;
isAiStylizeOpen: boolean;
};
const appUiInitialState = {
isAddVideoOpen: false,
stagedVideo: null,
isAddImageOpen: false,
stagedImage: null,
isAiStylizeOpen: false,
};
export const useAppUiContext = () => {
const appUiRef = useRef(signal<AppUiSignalType>(appUiInitialState));
const appUiSignal = appUiRef.current;
const resetAll = useCallback(() => {
appUiSignal.value = appUiInitialState;
}, []);
const openAddImage = useCallback((file?: File) => {
appUiSignal.value = {
...appUiInitialState,
isAddImageOpen: true,
stagedImage: file ? file : null,
};
}, []);
const openAddVideo = useCallback((file?: File) => {
appUiSignal.value = {
...appUiInitialState,
isAddVideoOpen: true,
stagedVideo: file ? file : null,
};
}, []);
const openAIStylize = useCallback(() => {
appUiSignal.value = {
...appUiInitialState,
isAiStylizeOpen: true,
};
}, []);
return {
signal: appUiSignal,
resetAll,
openAddImage,
openAddVideo,
openAIStylize,
};
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { ContextualToolbarImage } from "~/KonvaRootComponent/ContextualToolbarImage";
import { ContextualLoadingBar } from "~/KonvaRootComponent/ContextualLoadingBar";
import { ContextualButtonRetry } from "~/KonvaRootComponent/ContextualButtonRetry";
import { SignaledDialogError } from "~/KonvaRootComponent/SignaledComponents";
import { SignaledDialogError } from "~/KonvaRootComponent/OtherSignaledComponents";
import { useRenderCounter } from "~/hooks/useRenderCounter";
@@ -0,0 +1,127 @@
import {
DragEventHandler,
HTMLAttributes,
useEffect,
useRef,
useState,
} from "react";
import { faFilePlus } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
interface CanvasDragDropFilesInterface extends HTMLAttributes<HTMLDivElement> {
fileTypes: string[];
onSetFiles: (file: File[]) => void;
}
enum DragStates {
IDLE = "idle",
READY = "ready",
DRAGGING = "dragging",
}
export const CanvasDragDropFiles = ({
fileTypes,
onSetFiles,
className,
children,
}: CanvasDragDropFilesInterface) => {
const [dragging, setDragging] = useState<DragStates>(DragStates.IDLE);
const lastDragging = useRef<DragStates>(DragStates.IDLE);
const handleDrop: DragEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
if (!event.dataTransfer) {
return;
}
const droppedFiles = event.dataTransfer.files;
if (droppedFiles.length > 0) {
const newFiles = Array.from(droppedFiles) as File[];
onSetFiles(newFiles);
setDragging(DragStates.READY);
}
};
const handleDragOver: DragEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
setDragging((curr) => {
if (!event.dataTransfer) {
return curr;
}
if (curr !== DragStates.DRAGGING) {
lastDragging.current = curr;
}
return DragStates.DRAGGING;
});
};
const handleDragExit: DragEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
setDragging(lastDragging.current);
};
useEffect(() => {
const mouseLeaveHandler = (event: MouseEvent) => {
if (
event.clientY <= 0 ||
event.clientX <= 0 ||
event.clientX >= window.innerWidth ||
event.clientY >= window.innerHeight
) {
setDragging(DragStates.READY);
}
};
const mouseEnterHandler = () => {
setDragging((curr) => {
if (curr === DragStates.DRAGGING) {
return curr;
}
return DragStates.IDLE;
});
};
document.addEventListener("mouseleave", mouseLeaveHandler);
document.addEventListener("mouseenter", mouseEnterHandler);
return () => {
document.removeEventListener("mouseleave", mouseLeaveHandler);
document.removeEventListener("mouseenter", mouseEnterHandler);
};
}, []);
return (
<div
className={twMerge(
"h-full w-full border-4 border-dashed border-transparent bg-transparent",
dragging === DragStates.IDLE && "pointer-events-none",
dragging === DragStates.DRAGGING && "border-gray-400 bg-gray-400/50",
"relative",
className,
)}
>
{dragging === DragStates.DRAGGING && (
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
<FontAwesomeIcon icon={faFilePlus} className="text-5xl" />
<p className="mt-1 text-lg">
Drag and drop files here to add them to the board!
</p>
<p className="text-lg">
Acceptable file types:{" "}
<b>
{fileTypes.reduce((acc, curr, idx) => {
const prefix = curr[0] !== "." ? "." : "";
const suffix = idx === fileTypes.length - 1 ? "" : ", ";
return acc + prefix + curr.toUpperCase() + suffix;
}, "")}
</b>
</p>
</div>
)}
{children}
<div
className="absolute left-0 top-0 h-full w-full"
onDrop={handleDrop}
onDragEnter={handleDragOver}
onDragOver={handleDragOver}
onDragExit={handleDragExit}
onDragLeave={handleDragExit}
/>
</div>
);
};
@@ -1,10 +1,12 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { twMerge } from "tailwind-merge";
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
import { FileUploader, IMAGE_FILE_TYPE } from "../FileUploader";
import { FileUploader } from "../FileUploader";
import { Button } from "~/components/ui";
import { IMAGE_FILE_TYPE } from "~/constants/fileTypeEnums";
import {
paperWrapperStyles,
dialogBackgroundStyles,
@@ -12,13 +14,26 @@ import {
import { dispatchUiEvents } from "~/signals/uiEvents/";
export const DialogAddImage = ({
stagedImage = null,
isOpen,
closeCallback,
}: {
stagedImage?: File | null;
isOpen: boolean;
closeCallback: () => void;
}) => {
const [assetFile, setAssetFile] = useState<File | null>(null);
const previouslyStagedImageRef = useRef<File | null>(null);
const currFile =
stagedImage &&
stagedImage !== assetFile &&
stagedImage !== previouslyStagedImageRef.current
? stagedImage
: assetFile;
if (previouslyStagedImageRef.current !== stagedImage) {
previouslyStagedImageRef.current = stagedImage;
}
function handleClose() {
setAssetFile(null);
@@ -26,8 +41,8 @@ export const DialogAddImage = ({
}
function handleEnter() {
if (assetFile) {
dispatchUiEvents.addImageToEngine(assetFile);
if (currFile) {
dispatchUiEvents.addImageToEngine(currFile);
}
handleClose();
}
@@ -44,18 +59,18 @@ export const DialogAddImage = ({
<FileUploader
title=""
fileTypes={Object.values(IMAGE_FILE_TYPE)}
file={assetFile}
file={currFile}
setFile={(file: File | null) => {
setAssetFile(file);
}}
/>
{assetFile && (
{currFile && (
<div className="relative flex items-center justify-center rounded-xl bg-ui-border">
<label className="absolute left-0 top-0 rounded-br-xl rounded-tl-xl border border-ui-border bg-white p-2 shadow-md">
Preview
</label>
<img
src={URL.createObjectURL(assetFile)}
src={URL.createObjectURL(currFile)}
className="border border-dashed border-white"
/>
</div>
@@ -64,7 +79,7 @@ export const DialogAddImage = ({
<Button onClick={handleClose} variant="secondary">
Cancel
</Button>
<Button onClick={handleEnter} disabled={assetFile === null}>
<Button onClick={handleEnter} disabled={currFile === null}>
Enter
</Button>
</div>
@@ -24,20 +24,32 @@ const initialState = {
dialogStatus: DialogAddMediaStatuses.STAGING_FILE,
};
export const DialogAddVideo = ({
stagedVideo = null,
isOpen,
closeCallback,
onUploadedVideo,
}: {
stagedVideo?: File | null;
isOpen: boolean;
closeCallback: () => void;
onUploadedVideo: (response: ApiResponse<MediaFile>) => void;
}) => {
// useRenderCounter("DialogAddVideo");
const [{ file, dialogStatus }, setStates] = useState<{
file: File | null;
dialogStatus: DialogAddMediaStatuses;
}>(initialState);
const previouslyStagedVideoRef = useRef<File | null>(null);
const currFile =
stagedVideo &&
stagedVideo !== file &&
stagedVideo !== previouslyStagedVideoRef.current
? stagedVideo
: file;
if (previouslyStagedVideoRef.current !== stagedVideo) {
previouslyStagedVideoRef.current = stagedVideo;
}
console.log(currFile);
const trimDataRef = useRef(signal<TrimData | undefined>(undefined));
const trimData = trimDataRef.current;
@@ -81,7 +93,7 @@ export const DialogAddVideo = ({
{dialogStatus === DialogAddMediaStatuses.STAGING_FILE && (
<>
<QuickTrimVideoUploader
file={file}
file={currFile}
onFileStaged={(file) => {
setStates((curr) => ({ ...curr, file }));
}}
@@ -1,10 +1,12 @@
import { useCallback, useState } from "react";
import { Signal } from "@preact/signals-react";
import { FileUploader, VIDEO_FILE_TYPE } from "../FileUploader";
import { FileUploader } from "../FileUploader";
import { VideoControls } from "./VideoControls";
import { TrimmerPlaybar, TrimData } from "./TrimmerPlaybar";
import { VIDEO_FILE_TYPE } from "~/constants/fileTypeEnums";
export type { TrimData };
export const QuickTrimVideoUploader = ({
@@ -5,9 +5,9 @@ import {
faFileImage,
faFileVideo,
} from "@fortawesome/pro-solid-svg-icons";
import { getFileName } from "~/utilities";
import { VIDEO_FILE_TYPE } from "./enums";
import { VIDEO_FILE_TYPE } from "~/constants/fileTypeEnums";
// import { getFileName } from "~/utilities";
interface Props {
file: File | null;
@@ -25,5 +25,3 @@ export const FileUploader = ({ fileTypes, title, file, setFile }: Props) => {
</div>
);
};
export { IMAGE_FILE_TYPE, VIDEO_FILE_TYPE } from "./enums";
+1
View File
@@ -1,3 +1,4 @@
export { CanvasDragDropFiles } from "./CanvasDragDropFiles";
export { DialogAddImage } from "./DialogAddImage";
export { DialogAddVideo } from "./DialogAddVideo";
export { DialogAiStylize } from "./DialogAiStylize";
+10
View File
@@ -0,0 +1,10 @@
export enum IMAGE_FILE_TYPE {
PNG = "png",
JPG = "jpg",
JPEG = "jpeg",
WEBP = "webp",
}
export enum VIDEO_FILE_TYPE {
MP4 = "mp4",
}
+1 -1
View File
@@ -8,5 +8,5 @@ export const getFileName = (file: File) => {
return file.name.substring(0, file.name.lastIndexOf("."));
};
export const getFileExtension = (file: File) => {
return file.name.substring(file.name.lastIndexOf("."));
return file.name.substring(file.name.lastIndexOf(".") + 1);
};