diff --git a/apps/client/src/common/components/loader-overlay/LoaderOverlay.module.scss b/apps/client/src/common/components/loader-overlay/LoaderOverlay.module.scss new file mode 100644 index 000000000..074e32f2f --- /dev/null +++ b/apps/client/src/common/components/loader-overlay/LoaderOverlay.module.scss @@ -0,0 +1,45 @@ +$loader-size: 4rem; + +.overlay { + position: absolute; + z-index: 10; + width: 100%; + height: 100%; + display: grid; + place-content: center; + background-color: $black-10; + backdrop-filter: blur(5px); +} + + +.loader { + width: $loader-size; + height: $loader-size; + background: $blue-500; + display: inline-block; + border-radius: 50%; + box-sizing: border-box; + animation: animloader 1s ease-in infinite; +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} diff --git a/apps/client/src/common/components/loader-overlay/LoaderOverlay.tsx b/apps/client/src/common/components/loader-overlay/LoaderOverlay.tsx new file mode 100644 index 000000000..bc77a2814 --- /dev/null +++ b/apps/client/src/common/components/loader-overlay/LoaderOverlay.tsx @@ -0,0 +1,9 @@ +import style from './LoaderOverlay.module.scss'; + +export default function LoaderOverlay() { + return ( +
+ +
+ ); +} diff --git a/apps/client/src/common/hooks-query/useInfo.ts b/apps/client/src/common/hooks-query/useInfo.ts index 680988500..6601f11ea 100644 --- a/apps/client/src/common/hooks-query/useInfo.ts +++ b/apps/client/src/common/hooks-query/useInfo.ts @@ -17,5 +17,5 @@ export default function useInfo() { networkMode: 'always', }); - return { data, status, isError, refetch, isFetching }; + return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching }; } diff --git a/apps/client/src/features/app-settings/panel/Panel.module.scss b/apps/client/src/features/app-settings/panel/Panel.module.scss index 22030e8a3..84314ebf4 100644 --- a/apps/client/src/features/app-settings/panel/Panel.module.scss +++ b/apps/client/src/features/app-settings/panel/Panel.module.scss @@ -27,6 +27,7 @@ $inner-padding: 1rem; } .section { + position: relative; margin-top: 2rem; font-size: calc(1rem - 1px); max-width: 800px; @@ -128,3 +129,49 @@ $inner-padding: 1rem; border-top: 1px solid $white-10; margin: 1rem -2rem; } + +$loader-size: 4rem; + +.overlay { + position: absolute; + z-index: 10; + width: calc(100% - 2rem); + left: 1rem; + height: calc(100% - 1rem); + display: grid; + place-content: center; + backdrop-filter: blur(5px); + background-color: rgba(0, 0, 0, 0.01); +} + +.loader { + width: $loader-size; + height: $loader-size; + background: $blue-500; + display: inline-block; + border-radius: 50%; + box-sizing: border-box; + animation: animloader 1s ease-in infinite; +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} + +@keyframes animloader { + 0% { + transform: scale(0); + opacity: 0.6; + } + 100% { + transform: scale(1); + opacity: 0; + } +} diff --git a/apps/client/src/features/app-settings/panel/PanelUtils.tsx b/apps/client/src/features/app-settings/panel/PanelUtils.tsx index df449b165..2725facd1 100644 --- a/apps/client/src/features/app-settings/panel/PanelUtils.tsx +++ b/apps/client/src/features/app-settings/panel/PanelUtils.tsx @@ -81,3 +81,11 @@ export function Error({ children }: { children: ReactNode }) { export function Divider() { return
; } + +export function Loader() { + return ( +
+ +
+ ); +} diff --git a/apps/client/src/features/app-settings/panel/general-panel/EditorSettingsForm.tsx b/apps/client/src/features/app-settings/panel/general-panel/EditorSettingsForm.tsx new file mode 100644 index 000000000..26ffb0f3d --- /dev/null +++ b/apps/client/src/features/app-settings/panel/general-panel/EditorSettingsForm.tsx @@ -0,0 +1,55 @@ +import { Switch } from '@chakra-ui/react'; + +import { useEditorSettings } from '../../../../common/stores/editorSettings'; +import * as Panel from '../PanelUtils'; + +export default function EditorSettingsForm() { + const eventSettings = useEditorSettings((state) => state.eventSettings); + const setShowQuickEntry = useEditorSettings((state) => state.setShowQuickEntry); + const setStartTimeIsLastEnd = useEditorSettings((state) => state.setStartTimeIsLastEnd); + const setDefaultPublic = useEditorSettings((state) => state.setDefaultPublic); + + return ( + + + Editor settings + + + + + setShowQuickEntry(event.target.checked)} + /> + + + + setStartTimeIsLastEnd(event.target.checked)} + /> + + + + setDefaultPublic(event.target.checked)} + /> + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx index 5071a87a9..531a916d9 100644 --- a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx +++ b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx @@ -1,12 +1,16 @@ import * as Panel from '../PanelUtils'; +import EditorSettingsForm from './EditorSettingsForm'; import GeneralPanelForm from './GeneralPanelForm'; +import ViewSettingsForm from './ViewSettingsForm'; export default function GeneralPanel() { return ( <> Settings + + ); } diff --git a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanelForm.tsx b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanelForm.tsx index e2fcbe408..5226f5d19 100644 --- a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanelForm.tsx +++ b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanelForm.tsx @@ -65,7 +65,7 @@ export default function GeneralPanelForm() { General settings
- + +
+
+ + {isFetching || isFetchingInfo ? : null} + + + + + You can override the styles of the viewers with a custom CSS file.
+ {info?.cssOverride && `In your installation the file is at ${info?.cssOverride}`} +
+
+ See the docs +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/client/src/features/app-settings/panel/project-panel/ManageProjects.tsx b/apps/client/src/features/app-settings/panel/project-panel/ManageProjects.tsx new file mode 100644 index 000000000..1ed0af305 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/project-panel/ManageProjects.tsx @@ -0,0 +1,95 @@ +import { ChangeEvent, useRef, useState } from 'react'; +import { Button, Input } from '@chakra-ui/react'; +import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; + +import { uploadProjectFile } from '../../../../common/api/db'; +import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils'; +import { validateProjectFile } from '../../../../common/utils/uploadUtils'; +import * as Panel from '../PanelUtils'; + +import ProjectCreateForm from './ProjectCreateForm'; +import ProjectList from './ProjectList'; + +import style from './ProjectPanel.module.scss'; + +export default function ManageProjects() { + const [isCreatingProject, setIsCreatingProject] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState<'import' | null>(null); + + const fileInputRef = useRef(null); + + const handleToggleCreate = () => { + setIsCreatingProject((prev) => !prev); + }; + + const handleSelectFile = () => { + fileInputRef.current?.click(); + }; + + const handleImport = async (event: ChangeEvent) => { + const selectedFile = event.target?.files?.[0]; + if (!selectedFile) { + return; + } + + setLoading('import'); + + try { + validateProjectFile(selectedFile); + await uploadProjectFile(selectedFile); + } catch (error) { + const errorMessage = maybeAxiosError(error); + setError(`Error uploading file: ${errorMessage}`); + } finally { + invalidateAllCaches(); + } + + setLoading(null); + }; + + const handleCloseForm = () => { + setIsCreatingProject(false); + }; + return ( + + + + + Manage projects +
+ + +
+
+ {error && {error}} + + {isCreatingProject && } + +
+
+ ); +} diff --git a/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx b/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx new file mode 100644 index 000000000..b2b770f9e --- /dev/null +++ b/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx @@ -0,0 +1,155 @@ +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { Button, Input, Textarea } from '@chakra-ui/react'; +import { type ProjectData } from 'ontime-types'; + +import { postProjectData } from '../../../../common/api/project'; +import { maybeAxiosError } from '../../../../common/api/utils'; +import useProjectData from '../../../../common/hooks-query/useProjectData'; +import * as Panel from '../PanelUtils'; + +import style from './ProjectPanel.module.scss'; + +export default function ProjectData() { + const { data, refetch } = useProjectData(); + + const { + handleSubmit, + register, + reset, + formState: { isSubmitting, isValid, isDirty }, + setError, + } = useForm({ + defaultValues: data, + values: data, + resetOptions: { + keepDirtyValues: true, + }, + }); + + // reset form values if data changes + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + + const onSubmit = async (formData: ProjectData) => { + try { + await postProjectData(formData); + } catch (error) { + const message = maybeAxiosError(error); + setError('root', { message }); + } finally { + await refetch(); + } + }; + + const onReset = () => { + reset(data); + }; + return ( + + + + Project Data +
+ + +
+
+ + +
+ +
+
+ +
+
+