From a6e1cbbbce6c0c35e45fa2ce6539517a838f1c40 Mon Sep 17 00:00:00 2001 From: Ary Date: Sun, 17 Nov 2024 14:55:20 -0700 Subject: [PATCH] Upload corporate logo (#1314) * feat: upload logo to project data --------- Co-authored-by: Carlos Valente --- apps/client/src/common/api/constants.ts | 1 + apps/client/src/common/api/project.ts | 17 +++- apps/client/src/common/models/ProjectData.ts | 1 + apps/client/src/common/utils/uploadUtils.ts | 18 +++- .../app-settings/panel-utils/PanelUtils.tsx | 4 +- .../panel/project-panel/ProjectData.tsx | 93 ++++++++++++++++++- .../project-panel/ProjectPanel.module.scss | 23 +++++ .../src/features/viewers/timer/Timer.scss | 8 ++ .../src/features/viewers/timer/Timer.tsx | 8 +- apps/server/src/api-data/db/db.controller.ts | 39 +++++++- apps/server/src/api-data/db/db.middleware.ts | 14 +++ apps/server/src/api-data/db/db.validation.ts | 1 + .../api-data/project/project.controller.ts | 20 +++- .../src/api-data/project/project.router.ts | 3 + .../api-data/project/project.validation.ts | 1 + .../__tests__/DataProvider.utils.test.ts | 2 + apps/server/src/models/dataModel.ts | 1 + apps/server/src/models/demoProject.ts | 1 + .../project-service/projectServiceUtils.ts | 10 +- apps/server/src/setup/config.ts | 1 + apps/server/src/setup/index.ts | 1 + .../utils/__tests__/parserFunctions.test.ts | 28 ++++++ apps/server/src/utils/parserFunctions.ts | 1 + .../ontime-controller/BackendResponse.type.ts | 4 + .../src/definitions/core/ProjectData.type.ts | 1 + packages/types/src/index.ts | 1 + 26 files changed, 289 insertions(+), 13 deletions(-) diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index a75d75385..789e1ce87 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -26,3 +26,4 @@ export const ontimeURL = `${serverURL}/ontime`; export const userAssetsPath = 'user'; export const cssOverridePath = 'styles/override.css'; export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`; +export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`; diff --git a/apps/client/src/common/api/project.ts b/apps/client/src/common/api/project.ts index 11b14d267..50e7689d6 100644 --- a/apps/client/src/common/api/project.ts +++ b/apps/client/src/common/api/project.ts @@ -1,5 +1,5 @@ import axios, { AxiosResponse } from 'axios'; -import { ProjectData } from 'ontime-types'; +import { ProjectData, ProjectLogoResponse } from 'ontime-types'; import { apiEntryUrl } from './constants'; @@ -19,3 +19,18 @@ export async function getProjectData(): Promise { export async function postProjectData(data: ProjectData): Promise> { return axios.post(projectPath, data); } + +/** + * HTTP request to upload a project logo + */ +export async function uploadProjectLogo(file: File): Promise> { + const formData = new FormData(); + formData.append('image', file); + const response = await axios.post(`${projectPath}/upload`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response; +} diff --git a/apps/client/src/common/models/ProjectData.ts b/apps/client/src/common/models/ProjectData.ts index 142748e82..4d7a0e57c 100644 --- a/apps/client/src/common/models/ProjectData.ts +++ b/apps/client/src/common/models/ProjectData.ts @@ -7,4 +7,5 @@ export const projectDataPlaceholder: ProjectData = { publicInfo: '', backstageUrl: '', backstageInfo: '', + projectLogo: null, }; diff --git a/apps/client/src/common/utils/uploadUtils.ts b/apps/client/src/common/utils/uploadUtils.ts index 83098e876..95a67365a 100644 --- a/apps/client/src/common/utils/uploadUtils.ts +++ b/apps/client/src/common/utils/uploadUtils.ts @@ -34,7 +34,7 @@ export function validateProjectFile(file: File) { // Limit file size of a project file to around 1MB if (file.size > 1_000_000) { - throw new Error('File size limit (10MB) exceeded'); + throw new Error('File size limit (1MB) exceeded'); } } @@ -45,3 +45,19 @@ export function isExcelFile(file: File | null) { export function isOntimeFile(file: File | null) { return file?.name.endsWith('.json'); } + +/** + * Collection of rules for pre-validating a project file + * @param file + */ +export function validateLogo(file: File) { + // Check if file is empty + if (file.size === 0) { + throw new Error('File is empty'); + } + + // Limit file size of a project file to around 1MB + if (file.size > 1_000_000) { + throw new Error('File size limit (1MB) exceeded'); + } +} diff --git a/apps/client/src/features/app-settings/panel-utils/PanelUtils.tsx b/apps/client/src/features/app-settings/panel-utils/PanelUtils.tsx index 67e4a4422..4020d73d6 100644 --- a/apps/client/src/features/app-settings/panel-utils/PanelUtils.tsx +++ b/apps/client/src/features/app-settings/panel-utils/PanelUtils.tsx @@ -35,9 +35,9 @@ export function Paragraph({ children }: { children: ReactNode }) { return

{children}

; } -export function Card({ children, ...props }: { children: ReactNode } & JSX.IntrinsicElements['div']) { +export function Card({ children, className, ...props }: { children: ReactNode } & JSX.IntrinsicElements['div']) { return ( -
+
{children}
); 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 index 2bcede449..d3a0e2baf 100644 --- a/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx +++ b/apps/client/src/features/app-settings/panel/project-panel/ProjectData.tsx @@ -1,11 +1,15 @@ -import { useEffect } from 'react'; +import { ChangeEvent, useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { Button, Input, Textarea } from '@chakra-ui/react'; +import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline'; +import { IoTrash } from '@react-icons/all-files/io5/IoTrash'; import { type ProjectData } from 'ontime-types'; -import { postProjectData } from '../../../../common/api/project'; +import { projectLogoPath } from '../../../../common/api/constants'; +import { postProjectData, uploadProjectLogo } from '../../../../common/api/project'; import { maybeAxiosError } from '../../../../common/api/utils'; import useProjectData from '../../../../common/hooks-query/useProjectData'; +import { validateLogo } from '../../../../common/utils/uploadUtils'; import * as Panel from '../../panel-utils/PanelUtils'; import style from './ProjectPanel.module.scss'; @@ -17,8 +21,10 @@ export default function ProjectData() { handleSubmit, register, reset, - formState: { isSubmitting, isValid, isDirty }, + formState: { isSubmitting, isValid, isDirty, errors }, setError, + watch, + setValue, } = useForm({ defaultValues: data, values: data, @@ -34,6 +40,40 @@ export default function ProjectData() { } }, [data, reset]); + const handleUploadProjectLogo = async (event: ChangeEvent) => { + const file = event.target.files?.[0]; + + if (!file) { + return; + } + + try { + validateLogo(file); + const response = await uploadProjectLogo(file); + + setValue('projectLogo', response.data.logoFilename, { + shouldDirty: true, + }); + } catch (error) { + const message = maybeAxiosError(error); + setError('projectLogo', { message }); + } + }; + + const { ref, ...projectLogoRest } = register('projectLogo'); + + const uploadInputRef = useRef(null); + + const handleClickUpload = () => { + uploadInputRef.current?.click(); + }; + + const handleDeleteLogo = () => { + setValue('projectLogo', null, { + shouldDirty: true, + }); + }; + const onSubmit = async (formData: ProjectData) => { try { await postProjectData(formData); @@ -86,6 +126,53 @@ export default function ProjectData() { {...register('title')} /> + + +