diff --git a/apps/client/src/features/modals/upload-modal/UploadFile.tsx b/apps/client/src/features/modals/upload-modal/UploadFile.tsx new file mode 100644 index 000000000..958aba9ed --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/UploadFile.tsx @@ -0,0 +1,58 @@ +import { ChangeEvent, useRef, useState } from 'react'; +import { Input } from '@chakra-ui/react'; + +import UploadEntry from './upload-entry/UploadEntry'; +import { useUploadModalContextStore } from './uploadModalContext'; +import { validateFile } from './uploadUtils'; + +import style from './UploadModal.module.scss'; + +export default function UploadFile() { + const fileInputRef = useRef(null); + + const { file, setFile, progress } = useUploadModalContextStore(); + + const [errors, setErrors] = useState(); + const success = false; + + const clearFile = () => { + setFile(null); + }; + + const handleFile = (event: ChangeEvent) => { + const fileSelected = event?.target?.files?.[0]; + if (!fileSelected) return; + + const validate = validateFile(fileSelected); + setErrors(validate.errors?.[0]); + + if (validate.isValid) { + setFile(fileSelected); + } else { + setFile(null); + } + }; + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + return ( + <> + +
+ Click to upload Ontime project or xlsx file +
+ {(file || errors) && ( + + )} + + ); +} diff --git a/apps/client/src/features/modals/upload-modal/UploadModal.module.scss b/apps/client/src/features/modals/upload-modal/UploadModal.module.scss index fdd7d5778..c35c1a31d 100644 --- a/apps/client/src/features/modals/upload-modal/UploadModal.module.scss +++ b/apps/client/src/features/modals/upload-modal/UploadModal.module.scss @@ -5,79 +5,31 @@ .uploadBody { display: flex; flex-direction: column; - gap: 16px; + gap: 1rem; } .uploadArea { + margin: 0 auto; width: 100%; - min-height: 200px; - border: 2px dashed $gray-50; + max-width: 550px; + + min-height: 150px; + border: 2px dashed $gray-200; border-radius: 3px; display: grid; place-content: center; transition-property: background-color; transition-duration: $transition-time-action; + font-size: calc(1rem - 1px); &:hover { border: 2px solid $blue-500; background-color: $blue-50; cursor: pointer; } + &.comment { - color: gray; - } -} - -.uploadedItem { - background-color: $gray-50; - padding: 8px; - display: grid; - grid-template-areas: - "icon title close" - "icon info ." - "progress progress progress"; - grid-template-columns: auto 1fr auto; - column-gap: 16px; - border-radius: 3px; - - .icon { - align-self: center; - grid-area: icon; - font-size: 32px; - color: $gray-700; - } - - .fileTitle { - grid-area: title; - font-size: 14px; - color: $gray-1350; - } - - .fileInfo { - grid-area: info; - font-size: 12px; - color: $gray-1100; - } - - .fileProgress { - grid-area: progress; - } - - .cancelUpload { - grid-area: close; - cursor: pointer; - } - - &.error { - .icon { - color: $error-red; - } - } - - &.success { - .icon { - color: $green-500; - } + color: $modal-note-color; } } @@ -89,3 +41,9 @@ .pad { margin: 8px; } + +.twoColumn { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} diff --git a/apps/client/src/features/modals/upload-modal/UploadModal.tsx b/apps/client/src/features/modals/upload-modal/UploadModal.tsx index c328551f5..a462043ad 100644 --- a/apps/client/src/features/modals/upload-modal/UploadModal.tsx +++ b/apps/client/src/features/modals/upload-modal/UploadModal.tsx @@ -1,7 +1,6 @@ -import { ChangeEvent, useCallback, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Button, - Input, Modal, ModalBody, ModalCloseButton, @@ -9,23 +8,37 @@ import { ModalFooter, ModalHeader, ModalOverlay, - Progress, - Switch, } from '@chakra-ui/react'; -import { IoClose } from '@react-icons/all-files/io5/IoClose'; -import { IoDocumentTextOutline } from '@react-icons/all-files/io5/IoDocumentTextOutline'; -import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline'; import { useQueryClient } from '@tanstack/react-query'; +import { OntimeRundown } from 'ontime-types'; import { RUNDOWN_TABLE } from '../../../common/api/apiConstants'; -import { uploadData } from '../../../common/api/ontimeApi'; -import { useEmitLog } from '../../../common/stores/logger'; -import ModalSplitInput from '../ModalSplitInput'; +import { maybeAxiosError } from '../../../common/api/apiUtils'; +import { postPreviewExcel, uploadData } from '../../../common/api/ontimeApi'; +import { projectDataPlaceholder } from '../../../common/models/ProjectData'; +import { userFieldsPlaceholder } from '../../../common/models/UserFields'; +import { cx } from '../../../common/utils/styleUtils'; -import { validateFile } from './utils'; +import PreviewExcel from './preview/PreviewExcel'; +import ExcelFileOptions from './upload-options/ExcelFileOptions'; +import OntimeFileOptions from './upload-options/OntimeFileOptions'; +import UploadStepTracker from './upload-step/UploadStep'; +import UploadFile from './UploadFile'; +import { useUploadModalContextStore } from './uploadModalContext'; +import { defaultExcelImportMap, ExcelImportMapKeys, isExcelFile, isOntimeFile } from './uploadUtils'; import style from './UploadModal.module.scss'; +export type UploadStep = 'upload' | 'review'; + +export interface OntimeInputOptions { + onlyImportRundown?: boolean; +} + +export type ExcelInputOptions = { + [K in ExcelImportMapKeys]: string; +}; + interface UploadModalProps { onClose: () => void; isOpen: boolean; @@ -33,65 +46,111 @@ interface UploadModalProps { export default function UploadModal({ onClose, isOpen }: UploadModalProps) { const queryClient = useQueryClient(); - const { emitError } = useEmitLog(); - const [errors, setErrors] = useState(); - const [isSubmitting, setSubmitting] = useState(false); - const [file, setFile] = useState(null); - const [progress, setProgress] = useState(0); - const overrideOptionRef = useRef(null); - const fileInputRef = useRef(null); - const [success, setSuccess] = useState(false); - const handleFile = useCallback((event: ChangeEvent) => { - const fileUploaded = event?.target?.files?.[0]; - if (!fileUploaded) return; + const { file, setProgress, clear } = useUploadModalContextStore(); - const validate = validateFile(fileUploaded); - setErrors(validate.errors?.[0]); + const [uploadStep, setUploadStep] = useState('upload'); + const [submitting, setSubmitting] = useState(false); + const [rundown, setRundown] = useState([]); + const [userFields, setUserFields] = useState(userFieldsPlaceholder); + const [project, setProject] = useState(projectDataPlaceholder); - if (validate.isValid) { - setFile(fileUploaded); - } else { - setFile(null); - } - }, []); + const [errors, setErrors] = useState(''); - const handleSubmit = useCallback(async () => { - setSubmitting(true); + const ontimeFileOptions = useRef>({}); + const excelFileOptions = useRef>(defaultExcelImportMap); + + useEffect(() => { + clear(); + setUploadStep('upload'); + setSubmitting(false); + setRundown([]); + setUserFields(userFieldsPlaceholder); + setProject(projectDataPlaceholder); + setErrors(''); + }, [clear, isOpen]); + + const handleParse = async () => { if (file) { + setSubmitting(true); try { - const options = { - onlyRundown: overrideOptionRef.current?.checked || false, - }; - await uploadData(file, setProgress, options); + if (isOntimeFile(file)) { + await handleOntimeFile(file); + await queryClient.invalidateQueries(RUNDOWN_TABLE); + } else if (isExcelFile(file)) { + await handleExcelFile(file); + } } catch (error) { - emitError(`Failed uploading file: ${error}`); + setErrors(`Failed uploading file: ${error}`); } finally { - await queryClient.invalidateQueries(RUNDOWN_TABLE); - setSuccess(true); + setSubmitting(false); } } - setSubmitting(false); - }, [emitError, file, queryClient]); - const handleClick = () => { - fileInputRef.current?.click(); - }; + async function handleExcelFile(file: File) { + const options = excelFileOptions.current; + // TODO: option type should be central, to also be used by backend + try { + const response = await postPreviewExcel(file, setProgress, options); + if (response.status === 200) { + setRundown(response.data.rundown); + setUserFields(response.data.userFields); + setProject(response.data.project); + setUploadStep('review'); + } + } catch (error) { + const message = maybeAxiosError(error); + setErrors(`Error importing excel ${message}`); + } + } - const clearFile = () => { - setFile(null); + async function handleOntimeFile(file: File) { + const options = { + onlyRundown: Boolean(ontimeFileOptions.current.onlyImportRundown), + }; + await uploadData(file, setProgress, options); + } }; const handleClose = () => { - clearFile(); - setSuccess(false); - setErrors(undefined); - setProgress(0); + clear(); + setRundown([]); + setUserFields(userFieldsPlaceholder); + setProject(projectDataPlaceholder); onClose(); }; - const disableSubmit = !file || isSubmitting; + const handleFinalise = async () => { + if (file) { + setSubmitting(true); + try { + const options = { + //onlyRundown: overrideOptionRef.current?.checked || false, + }; + await uploadData(file, setProgress, options); + handleClose(); + } catch (error) { + console.error(error); + } finally { + await queryClient.invalidateQueries(RUNDOWN_TABLE); + setSubmitting(false); + } + } + }; + const isUpload = uploadStep === 'upload'; + const isExcel = isExcelFile(file); + const isOntime = isOntimeFile(file); + + const handleGoBack = isUpload ? undefined : () => setUploadStep('upload'); + const handleSubmit = isUpload ? handleParse : handleFinalise; + const disableSubmit = isUpload && !file; + const disableGoBack = isUpload; + const submitText = isUpload ? 'Upload' : 'Finish'; + + const modalClasses = cx([style.modalWidthOverride, isExcel ? style.doExtend : null]); + + console.log('debug', isExcel, modalClasses); return ( File import - -
- Click to upload Ontime project file -
- {file && ( -
- - - {file.name} - {`${(file.size / 1024).toFixed(2)}kb - ${file.type}`} - -
+ {isExcel && } + {uploadStep === 'upload' ? ( + <> + + {errors &&
{errors}
} + {isOntime && } + {isExcel && } + + ) : ( + )} - {errors && ( -
- - {errors} - Please try again - -
- )} -
- Import options - - - -
-
diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewColumn.module.scss b/apps/client/src/features/modals/upload-modal/preview/PreviewColumn.module.scss new file mode 100644 index 000000000..72a783ee4 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/PreviewColumn.module.scss @@ -0,0 +1,22 @@ +@use "../../../../theme/_ontimeColours" as *; + +@mixin pad-item { + padding-left: 0.5rem; + padding-right: 1rem; +} + +.previewTable { + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: repeat(6, auto); + font-size: calc(1rem - 2px); +} + +.field { + font-weight: 200; + @include pad-item; +} + +.value { + @include pad-item; +} diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewColumn.tsx b/apps/client/src/features/modals/upload-modal/preview/PreviewColumn.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewExcel.tsx b/apps/client/src/features/modals/upload-modal/preview/PreviewExcel.tsx new file mode 100644 index 000000000..f3b273e9e --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/PreviewExcel.tsx @@ -0,0 +1,25 @@ +import { OntimeRundown, ProjectData, UserFields } from 'ontime-types'; + +import PreviewProjectData from './PreviewProjectData'; +import PreviewRundown from './PreviewRundown'; + +import style from '../../Modal.module.scss'; + +interface PreviewExcelProps { + rundown: OntimeRundown; + project: ProjectData; + userFields: UserFields; +} + +export default function PreviewExcel(props: PreviewExcelProps) { + const { rundown, project, userFields } = props; + + return ( +
+
Review Project Data
+ +
Review Rundown
+ +
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewProjectData.tsx b/apps/client/src/features/modals/upload-modal/preview/PreviewProjectData.tsx new file mode 100644 index 000000000..46eb00d47 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/PreviewProjectData.tsx @@ -0,0 +1,26 @@ +import { ProjectData } from 'ontime-types'; + +import style from './PreviewColumn.module.scss'; + +interface PreviewProjectDataProps { + project: ProjectData; +} + +export default function PreviewProjectData({ project }: PreviewProjectDataProps) { + return ( +
+ Title + {project.title} + Description + {project.description} + Public URL + {project.publicUrl} + Public info + {project.publicInfo} + Backstage URL + {project.backstageUrl} + Backstage info + {project.backstageInfo} +
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewRundown.tsx b/apps/client/src/features/modals/upload-modal/preview/PreviewRundown.tsx new file mode 100644 index 000000000..80ce53454 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/PreviewRundown.tsx @@ -0,0 +1,123 @@ +import { isOntimeEvent, OntimeRundown, UserFields } from 'ontime-types'; +import { millisToString } from 'ontime-utils'; + +import { getAccessibleColour } from '../../../../common/utils/styleUtils'; + +import Tag from './Tag'; + +import style from './PreviewTable.module.scss'; + +interface PreviewRundownProps { + rundown: OntimeRundown; + userFields: UserFields; +} + +function booleanToText(value?: boolean) { + return value ? 'Yes' : undefined; +} + +export default function PreviewRundown({ rundown, userFields }: PreviewRundownProps) { + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {rundown.map((event, index) => { + const key = event.id; + if (isOntimeEvent(event)) { + const colour = event.colour ? getAccessibleColour(event.colour) : {}; + const isPublic = booleanToText(event.isPublic); + const skip = booleanToText(event.skip); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + return null; + })} + +
#TypeCueTitleSubtitlePresenterNoteTime StartTime EndDurationIs PublicSkipColourTimer TypeEnd Action + user0 {userFields.user0} + + user1 {userFields.user1} + + user2 {userFields.user2} + + user3 {userFields.user3} + + user4 {userFields.user4} + + user5 {userFields.user5} + + user6 {userFields.user6} + + user7 {userFields.user7} + + user8 {userFields.user8} + + user9 {userFields.user9} +
+ {index + 1} + Event{event.cue}{event.title}{event.subtitle}{event.presenter}{event.note}{millisToString(event.timeStart)}{millisToString(event.timeEnd)}{millisToString(event.duration)}{isPublic && {isPublic}}{skip && {skip}}{event.colour} + {event.timerType} + + {event.endAction} + {event.user0}{event.user1}{event.user2}{event.user3}{event.user4}{event.user5}{event.user6}{event.user7}{event.user8}{event.user9}
+
+
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/preview/PreviewTable.module.scss b/apps/client/src/features/modals/upload-modal/preview/PreviewTable.module.scss new file mode 100644 index 000000000..763139d13 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/PreviewTable.module.scss @@ -0,0 +1,45 @@ +@use "../../../../theme/_ontimeColours" as *; + +.container { + max-width: 100%; + max-height: max(300px, 30vh); + overflow: scroll; +} + +.scrollContainer { + overflow-x: scroll; +} + +.rundownPreview { + font-size: calc(1rem - 2px); + border-collapse: separate; +} + +.header, +.body { + th { + font-weight: 400; + height: unset; + line-height: calc(1rem - 2px); + white-space: nowrap; + padding-left: 0.25rem; + padding-right: 1rem; + } +} + +.header { + th { + font-weight: 200; + text-align: left; + } + + tr { + word-wrap: unset; + } +} + +.body { + tr:nth-child(odd) { + background-color: $gray-50; + } +} diff --git a/apps/client/src/features/modals/upload-modal/preview/Tag.module.scss b/apps/client/src/features/modals/upload-modal/preview/Tag.module.scss new file mode 100644 index 000000000..b55a3a9ce --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/Tag.module.scss @@ -0,0 +1,10 @@ +@use "../../../../theme/_ontimeColours" as *; + +.tag { + font-size: 10px; + background-color: $blue-500; + color: $pure-white; + border-radius: 2px; + padding: 0 0.25rem; + white-space: nowrap; +} diff --git a/apps/client/src/features/modals/upload-modal/preview/Tag.tsx b/apps/client/src/features/modals/upload-modal/preview/Tag.tsx new file mode 100644 index 000000000..de1ce550c --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/preview/Tag.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +import style from './Tag.module.scss'; + +export default function Tag({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.module.scss b/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.module.scss new file mode 100644 index 000000000..8adb54112 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.module.scss @@ -0,0 +1,59 @@ +@use '../../../../theme/ontimeColours' as *; +@use '../../../../theme/v2Styles' as *; + +.uploadedItem { + margin: 0 auto; + width: 100%; + max-width: 550px; + + border: 1px solid $gray-200; + padding: 0.5rem; + display: grid; + grid-template-areas: + "icon title close" + "icon info ." + "progress progress progress"; + grid-template-columns: auto 1fr auto; + column-gap: 1rem; + border-radius: 3px; + + .icon { + align-self: center; + grid-area: icon; + font-size: 2rem; + color: $gray-700; + } + + .fileTitle { + grid-area: title; + font-size: calc(1rem - 2px); + color: $ui-black; + } + + .fileInfo { + grid-area: info; + font-size: calc(1rem - 4px); + color: $gray-1100; + } + + .fileProgress { + grid-area: progress; + } + + .cancelUpload { + grid-area: close; + cursor: pointer; + } + + &.error { + .icon { + color: $error-red; + } + } + + &.success { + .icon { + color: $green-500; + } + } +} diff --git a/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.tsx b/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.tsx new file mode 100644 index 000000000..fa8da716c --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-entry/UploadEntry.tsx @@ -0,0 +1,53 @@ +import { Progress } from '@chakra-ui/react'; +import { IoClose } from '@react-icons/all-files/io5/IoClose'; +import { IoDocumentTextOutline } from '@react-icons/all-files/io5/IoDocumentTextOutline'; +import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline'; + +import { isExcelFile, isOntimeFile } from '../uploadUtils'; + +import style from './UploadEntry.module.scss'; + +interface UploadEntryProps { + file: File | null; + errors?: string; + progress: number; + success: boolean; + handleClear: () => void; +} + +export default function UploadEntry(props: UploadEntryProps) { + const { file, errors, progress, success, handleClear } = props; + + if (errors) { + return ( +
+ + {errors} + Please try again + +
+ ); + } + + if (file) { + const fileSize = `${(file.size / 1024).toFixed(2)}kb`; + let fileType = ''; + if (isOntimeFile(file)) { + fileType = 'Ontime Project File'; + } else if (isExcelFile(file)) { + fileType = 'Excel Rundown'; + } + + return ( +
+ + + {file.name} + {`${fileSize} - ${fileType}`} + +
+ ); + } + + return null; +} diff --git a/apps/client/src/features/modals/upload-modal/upload-options/ExcelFileOptions.tsx b/apps/client/src/features/modals/upload-modal/upload-options/ExcelFileOptions.tsx new file mode 100644 index 000000000..ba090bcfa --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-options/ExcelFileOptions.tsx @@ -0,0 +1,78 @@ +import { MutableRefObject } from 'react'; +import { Input } from '@chakra-ui/react'; + +import ModalSplitInput from '../../ModalSplitInput'; + +import ImportMapTable, { type TableEntry } from './ImportMapTable'; +import { ExcelInputOptions } from '../UploadModal'; + +import style from '../UploadModal.module.scss'; + +interface ExcelFileOptionsProps { + optionsRef: MutableRefObject; +} + +export default function ExcelFileOptions(props: ExcelFileOptionsProps) { + const { optionsRef } = props; + + const updateRef = (field: T, value: ExcelInputOptions[T]) => { + // avoid unnecessary changes + if (optionsRef.current[field] !== value) { + optionsRef.current = { ...optionsRef.current, [field]: value }; + } + }; + + const worksheet: TableEntry[] = [{ label: 'Worksheet', title: 'worksheet', value: optionsRef.current.worksheet }]; + + const timings: TableEntry[] = [ + { label: 'Start time', title: 'timeStart', value: optionsRef.current.timeStart }, + { label: 'End Time', title: 'timeEnd', value: optionsRef.current.timeEnd }, + { label: 'Duration', title: 'duration', value: optionsRef.current.duration }, + ]; + + const titles: TableEntry[] = [ + { label: 'Cue', title: 'cue', value: optionsRef.current.cue }, + { label: 'Colour', title: 'colour', value: optionsRef.current.colour }, + { label: 'Title', title: 'title', value: optionsRef.current.title }, + { label: 'Presenter', title: 'presenter', value: optionsRef.current.presenter }, + { label: 'Subtitle', title: 'subtitle', value: optionsRef.current.subtitle }, + { label: 'Note', title: 'note', value: optionsRef.current.note }, + ]; + + const options: TableEntry[] = [ + { label: 'Is Public', title: 'isPublic', value: optionsRef.current.isPublic }, + { label: 'Timer Type', title: 'timerType', value: optionsRef.current.timerType }, + { label: 'End Action', title: 'endAction', value: optionsRef.current.endAction }, + ]; + + const userFields: TableEntry[] = [ + { label: 'User 0', title: 'user0', value: optionsRef.current.user0 }, + { label: 'User 1', title: 'user1', value: optionsRef.current.user1 }, + { label: 'User 2', title: 'user2', value: optionsRef.current.user2 }, + { label: 'User 3', title: 'user3', value: optionsRef.current.user3 }, + { label: 'User 4', title: 'user4', value: optionsRef.current.user4 }, + { label: 'User 5', title: 'user5', value: optionsRef.current.user5 }, + { label: 'User 6', title: 'user6', value: optionsRef.current.user6 }, + { label: 'User 7', title: 'user7', value: optionsRef.current.user7 }, + { label: 'User 8', title: 'user8', value: optionsRef.current.user8 }, + { label: 'User 9', title: 'user9', value: optionsRef.current.user9 }, + ]; + + return ( +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.module.scss b/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.module.scss new file mode 100644 index 000000000..de49d00d5 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.module.scss @@ -0,0 +1,22 @@ +@use '../../../../theme/v2Styles' as *; +@use '../../../../theme/ontimeColours' as *; + +.importTable { + margin: 0.5rem; + font-size: $inner-section-text-size; + height: fit-content; + + thead { + color: $gray-500; + text-transform: uppercase; + } + + tr:hover { + background-color: $gray-50; + } +} + +.label { + display: inline-block; + width: 100%; +} diff --git a/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.tsx b/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.tsx new file mode 100644 index 000000000..5593ba632 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-options/ImportMapTable.tsx @@ -0,0 +1,52 @@ +import { Input } from '@chakra-ui/react'; + +import { ExcelInputOptions } from '../UploadModal'; + +import style from './ImportMapTable.module.scss'; + +// TODO: make this generic +export type TableEntry = { label: string; title: keyof ExcelInputOptions; value: string }; + +interface ImportMapTableProps { + title: string; + fields: TableEntry[]; + handleOnChange: (field: keyof ExcelInputOptions, value: string) => void; +} + +export default function ImportMapTable(props: ImportMapTableProps) { + const { title, fields, handleOnChange } = props; + + return ( + + + {title} + + + {fields.map((field) => { + return ( + + + + + ); + })} + +
+ + + { + handleOnChange(field.title, event.target.value); + }} + /> +
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx b/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx new file mode 100644 index 000000000..21e37c35c --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-options/OntimeFileOptions.tsx @@ -0,0 +1,37 @@ +import { MutableRefObject } from 'react'; +import { Switch } from '@chakra-ui/react'; + +import ModalSplitInput from '../../ModalSplitInput'; +import { OntimeInputOptions } from '../UploadModal'; + +import style from '../UploadModal.module.scss'; + +interface OntimeFileOptionsProps { + optionsRef: MutableRefObject; +} + +export default function OntimeFileOptions(props: OntimeFileOptionsProps) { + const { optionsRef } = props; + + const updateRef = (field: T, value: OntimeInputOptions[T]) => { + optionsRef.current = { ...optionsRef.current, [field]: value }; + }; + + return ( +
+ Import options + + { + updateRef('onlyImportRundown', e.target.checked); + }} + /> + +
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.module.scss b/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.module.scss new file mode 100644 index 000000000..f2f59fc0f --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.module.scss @@ -0,0 +1,40 @@ +@use '../../../../theme/ontimeColours' as *; + +@mixin row { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0 0.25rem; +} + +.stepRow { + display: flex; + gap: 2rem; + align-items: center; + margin: 0 auto; + font-size: 1rem; +} + +.idle { + @include row; + color: $blue-500; +} + +.inactive { + @include row; + color: $gray-700; +} + +.active { + @include row; + color: $blue-700; +} + + +.inactiveIcon { + color: $gray-700; +} + +.activeIcon { + color: $blue-500; +} diff --git a/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.tsx b/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.tsx new file mode 100644 index 000000000..5c15c164f --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/upload-step/UploadStep.tsx @@ -0,0 +1,26 @@ +import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle'; +import { IoChevronForward } from '@react-icons/all-files/io5/IoChevronForward'; +import { IoEllipseOutline } from '@react-icons/all-files/io5/IoEllipseOutline'; + +import type { UploadStep } from '../UploadModal'; + +import style from './UploadStep.module.scss'; + +export default function UploadStepTracker({ uploadStep }: { uploadStep: UploadStep }) { + const isUpload = uploadStep === 'upload'; + const isReview = uploadStep === 'review'; + + return ( +
+
+ + Upload +
+ +
+ {isReview ? : } + Review +
+
+ ); +} diff --git a/apps/client/src/features/modals/upload-modal/uploadModalContext.tsx b/apps/client/src/features/modals/upload-modal/uploadModalContext.tsx new file mode 100644 index 000000000..e17bc0869 --- /dev/null +++ b/apps/client/src/features/modals/upload-modal/uploadModalContext.tsx @@ -0,0 +1,21 @@ +import { create } from 'zustand'; + +type UploadModalContext = { + file: File | null; + setFile: (file: File | null) => void; + + progress: number; + setProgress: (progress: number) => void; + + clear: () => void; +}; + +export const useUploadModalContextStore = create((set) => ({ + file: null, + setFile: (file: File | null) => set({ file }), + + progress: 0, + setProgress: (progress: number) => set({ progress }), + + clear: () => set({ file: null, progress: 0 }), +})); diff --git a/apps/client/src/features/modals/upload-modal/uploadUtils.ts b/apps/client/src/features/modals/upload-modal/uploadUtils.ts index a30c11fbf..eea99d93c 100644 --- a/apps/client/src/features/modals/upload-modal/uploadUtils.ts +++ b/apps/client/src/features/modals/upload-modal/uploadUtils.ts @@ -33,39 +33,3 @@ export function isExcelFile(file: File | null) { export function isOntimeFile(file: File | null) { return file?.name.endsWith('.json'); } - -export type ExcelImportMapKeys = keyof typeof defaultExcelImportMap; - -// TODO: extract to shared code -export const defaultExcelImportMap = { - worksheet: 'ontime', - projectName: 'project name', - projectDescription: 'project description', - publicUrl: 'public url', - publicInfo: 'public info', - backstageUrl: 'backstage url', - backstageInfo: 'backstage info', - timeStart: 'start', - timeEnd: 'end', - duration: 'duration', - cue: 'cue', - title: 'title', - presenter: 'presenter', - subtitle: 'subtitle', - isPublic: 'public', - skip: 'skip', - note: 'note', - colour: 'colour', - endAction: 'end action', - timerType: 'timer type', - user0: 'user0', - user1: 'user1', - user2: 'user2', - user3: 'user3', - user4: 'user4', - user5: 'user5', - user6: 'user6', - user7: 'user7', - user8: 'user8', - user9: 'user9', -};