From 564ce290d5864667d06650884b01ec02d2566450 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 4 Jun 2024 09:33:03 -0400 Subject: [PATCH 01/54] display download or share depending on if navigator can share --- .../templates/canvas/artwork-canvas.tsx | 18 +----- .../templates/canvas/canvas-download.tsx | 57 +++++++++++++++++ .../templates/canvas/canvas-share.tsx | 64 +++++++++++++++++++ app/components/templates/canvas/index.ts | 2 + app/components/ui/icons/name.d.ts | 2 + app/components/ui/icons/sprite.svg | 14 ++++ app/utils/download.ts | 2 +- other/svg-icons/share-1.svg | 13 ++++ other/svg-icons/share-2.svg | 13 ++++ 9 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 app/components/templates/canvas/canvas-download.tsx create mode 100644 app/components/templates/canvas/canvas-share.tsx create mode 100644 other/svg-icons/share-1.svg create mode 100644 other/svg-icons/share-2.svg diff --git a/app/components/templates/canvas/artwork-canvas.tsx b/app/components/templates/canvas/artwork-canvas.tsx index 0567c33d..fb119be1 100644 --- a/app/components/templates/canvas/artwork-canvas.tsx +++ b/app/components/templates/canvas/artwork-canvas.tsx @@ -8,9 +8,9 @@ import { type IArtworkVersionGenerator, } from '#app/definitions/artwork-generator' import { canvasDrawService } from '#app/services/canvas/draw.service' -import { downloadCanvasToImg } from '#app/utils/download' import { useOptionalUser } from '#app/utils/user' import { TooltipHydrated } from '../tooltip' +import { DownloadCanvas, ShareCanvas } from '.' const LinkToEditor = memo( ({ @@ -73,13 +73,6 @@ export const ArtworkCanvas = memo( setRefresh(prev => prev + 1) } - const handleDownload = () => { - const canvas = canvasRef.current - - if (!canvas) return - downloadCanvasToImg({ canvas }) - } - return ( - - - + + {generator.metadata && linkToEditor()} diff --git a/app/components/templates/canvas/canvas-download.tsx b/app/components/templates/canvas/canvas-download.tsx new file mode 100644 index 00000000..78a9763d --- /dev/null +++ b/app/components/templates/canvas/canvas-download.tsx @@ -0,0 +1,57 @@ +import { memo, useEffect, useState } from 'react' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { downloadCanvasToImg, downloadImageFileName } from '#app/utils/download' +import { TooltipHydrated } from '../tooltip' + +export const DownloadCanvas = memo( + ({ + canvasRef, + isHydrated, + }: { + canvasRef: React.RefObject + isHydrated: boolean + }) => { + const [canDownload, setCanDownload] = useState(false) + + useEffect(() => { + const checkShareCapability = () => { + const canvas = canvasRef.current + if (!canvas) return false + + canvas.toBlob(blob => { + if (!blob) return + const file = new File([blob], downloadImageFileName(), { + type: 'image/png', + }) + + // if you can share then don't display download button + const canShare = + navigator.canShare && navigator.canShare({ files: [file] }) + setCanDownload(!canShare) + }, 'image/png') + } + + checkShareCapability() + }, [canvasRef]) + + const handleDownload = () => { + const canvas = canvasRef.current + + if (!canvas) return + downloadCanvasToImg({ canvas }) + } + + if (!canDownload) return null + + return ( + + + + ) + }, +) +DownloadCanvas.displayName = 'DownloadCanvas' diff --git a/app/components/templates/canvas/canvas-share.tsx b/app/components/templates/canvas/canvas-share.tsx new file mode 100644 index 00000000..db6ad2d0 --- /dev/null +++ b/app/components/templates/canvas/canvas-share.tsx @@ -0,0 +1,64 @@ +import { memo, useEffect, useState } from 'react' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { downloadImageFileName } from '#app/utils/download' +import { TooltipHydrated } from '../tooltip' + +export const ShareCanvas = memo( + ({ + canvasRef, + isHydrated, + }: { + canvasRef: React.RefObject + isHydrated: boolean + }) => { + const [canShare, setCanShare] = useState(false) + const [fileToShare, setFileToShare] = useState(null) + + useEffect(() => { + const checkShareCapability = () => { + const canvas = canvasRef.current + if (!canvas) return false + + canvas.toBlob(blob => { + if (!blob) return + const file = new File([blob], downloadImageFileName(), { + type: 'image/png', + }) + + if (navigator.canShare && navigator.canShare({ files: [file] })) { + setCanShare(true) + setFileToShare(file) + } else { + setCanShare(false) + } + }, 'image/png') + } + + checkShareCapability() + }, [canvasRef]) + + const handleShare = () => { + if (!fileToShare) return + + navigator + .share({ + files: [fileToShare], + title: 'Share this artwork from PPPAAATTTT', + }) + .catch(error => console.error('Error sharing', error)) + } + + if (!canShare) return null + + return ( + + + + ) + }, +) +ShareCanvas.displayName = 'ShareCanvas' diff --git a/app/components/templates/canvas/index.ts b/app/components/templates/canvas/index.ts index f23b7856..ebeb8caf 100644 --- a/app/components/templates/canvas/index.ts +++ b/app/components/templates/canvas/index.ts @@ -1 +1,3 @@ export * from './artwork-canvas' +export * from './canvas-download' +export * from './canvas-share' diff --git a/app/components/ui/icons/name.d.ts b/app/components/ui/icons/name.d.ts index 49cc83cf..da100b7e 100644 --- a/app/components/ui/icons/name.d.ts +++ b/app/components/ui/icons/name.d.ts @@ -62,6 +62,8 @@ export type IconName = | 'reset' | 'rocket' | 'ruler-square' + | 'share-1' + | 'share-2' | 'size' | 'stack' | 'star-filled' diff --git a/app/components/ui/icons/sprite.svg b/app/components/ui/icons/sprite.svg index b9f5af1d..a59990fb 100644 --- a/app/components/ui/icons/sprite.svg +++ b/app/components/ui/icons/sprite.svg @@ -425,6 +425,20 @@ fill="currentColor" > + + + + + + { +export const downloadImageFileName = () => { const dateString = currentDateString() const format = 'png' const filename = `${dateString}_${CONST_FILE_NAME}.${format}` diff --git a/other/svg-icons/share-1.svg b/other/svg-icons/share-1.svg new file mode 100644 index 00000000..c3fd02df --- /dev/null +++ b/other/svg-icons/share-1.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/other/svg-icons/share-2.svg b/other/svg-icons/share-2.svg new file mode 100644 index 00000000..8cb0c8ce --- /dev/null +++ b/other/svg-icons/share-2.svg @@ -0,0 +1,13 @@ + + + + + + + + From f3429721ed3bd05c204632f10e3dec7e06dc9d6d Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 4 Jun 2024 10:58:31 -0400 Subject: [PATCH 02/54] shareable image wait to load --- .../templates/canvas/canvas-share.tsx | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/app/components/templates/canvas/canvas-share.tsx b/app/components/templates/canvas/canvas-share.tsx index db6ad2d0..60eed58b 100644 --- a/app/components/templates/canvas/canvas-share.tsx +++ b/app/components/templates/canvas/canvas-share.tsx @@ -19,22 +19,44 @@ export const ShareCanvas = memo( const canvas = canvasRef.current if (!canvas) return false - canvas.toBlob(blob => { - if (!blob) return - const file = new File([blob], downloadImageFileName(), { - type: 'image/png', - }) + const ctx = canvas.getContext('2d') + if (!ctx) return false - if (navigator.canShare && navigator.canShare({ files: [file] })) { - setCanShare(true) - setFileToShare(file) - } else { - setCanShare(false) - } - }, 'image/png') + // Check if the canvas is not blank + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const isNotBlank = imageData.data.some(value => value !== 0) + + if (isNotBlank) { + canvas.toBlob(blob => { + if (!blob) return + const file = new File([blob], downloadImageFileName(), { + type: 'image/png', + }) + + if (navigator.canShare && navigator.canShare({ files: [file] })) { + setCanShare(true) + setFileToShare(file) + } else { + setCanShare(false) + } + }, 'image/png') + } else { + // Canvas is blank, retry after a short delay + setTimeout(checkShareCapability, 100) + } + } + + const canvas = canvasRef.current + if (!canvas) return + + const handleCanvasLoad = () => { + checkShareCapability() } - checkShareCapability() + canvas.addEventListener('load', handleCanvasLoad) + return () => { + canvas.removeEventListener('load', handleCanvasLoad) + } }, [canvasRef]) const handleShare = () => { From dbc46bf21eed3d5c4bbaef6561d3b9e371c142cd Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 5 Jun 2024 11:19:55 -0400 Subject: [PATCH 03/54] shareable image hack --- .../templates/canvas/canvas-share.tsx | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/app/components/templates/canvas/canvas-share.tsx b/app/components/templates/canvas/canvas-share.tsx index 60eed58b..e1ed7585 100644 --- a/app/components/templates/canvas/canvas-share.tsx +++ b/app/components/templates/canvas/canvas-share.tsx @@ -12,62 +12,60 @@ export const ShareCanvas = memo( isHydrated: boolean }) => { const [canShare, setCanShare] = useState(false) - const [fileToShare, setFileToShare] = useState(null) + // const [fileToShare, setFileToShare] = useState(null) useEffect(() => { const checkShareCapability = () => { const canvas = canvasRef.current if (!canvas) return false - const ctx = canvas.getContext('2d') - if (!ctx) return false + canvas.toBlob(blob => { + if (!blob) return + const file = new File([blob], downloadImageFileName(), { + type: 'image/png', + }) - // Check if the canvas is not blank - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) - const isNotBlank = imageData.data.some(value => value !== 0) - - if (isNotBlank) { - canvas.toBlob(blob => { - if (!blob) return - const file = new File([blob], downloadImageFileName(), { - type: 'image/png', - }) - - if (navigator.canShare && navigator.canShare({ files: [file] })) { - setCanShare(true) - setFileToShare(file) - } else { - setCanShare(false) - } - }, 'image/png') - } else { - // Canvas is blank, retry after a short delay - setTimeout(checkShareCapability, 100) - } + if (navigator.canShare && navigator.canShare({ files: [file] })) { + setCanShare(true) + // setFileToShare(file) + } else { + setCanShare(false) + } + }, 'image/png') } const canvas = canvasRef.current if (!canvas) return - const handleCanvasLoad = () => { - checkShareCapability() - } - - canvas.addEventListener('load', handleCanvasLoad) - return () => { - canvas.removeEventListener('load', handleCanvasLoad) - } + checkShareCapability() }, [canvasRef]) - const handleShare = () => { - if (!fileToShare) return + const handleShare = async () => { + // if (!fileToShare) return + + // navigator + // .share({ + // files: [fileToShare], + // title: 'Share this artwork from PPPAAATTTT', + // }) + // .catch(error => console.error('Error sharing', error)) - navigator - .share({ - files: [fileToShare], - title: 'Share this artwork from PPPAAATTTT', - }) - .catch(error => console.error('Error sharing', error)) + // quick and dirty hack before meetup + // https://github.com/benkaiser/web-share-images/blob/master/src/examples/WebShareCanvas.tsx + const dataUrl = canvasRef.current!.toDataURL() + const blob = await (await fetch(dataUrl)).blob() + const filesArray: File[] = [ + new File([blob], `${downloadImageFileName()}.png`, { + type: blob.type, + lastModified: new Date().getTime(), + }), + ] + const shareData = { + files: filesArray, + } + navigator.share(shareData as any).then(() => { + console.log('Shared successfully') + }) } if (!canShare) return null From 786f703be51d99f259adde65bd6558a57d302b0d Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 5 Jun 2024 23:36:03 -0400 Subject: [PATCH 04/54] shareable image hack pt 1 --- app/components/templates/canvas/canvas-share.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/templates/canvas/canvas-share.tsx b/app/components/templates/canvas/canvas-share.tsx index e1ed7585..e633cceb 100644 --- a/app/components/templates/canvas/canvas-share.tsx +++ b/app/components/templates/canvas/canvas-share.tsx @@ -55,8 +55,8 @@ export const ShareCanvas = memo( const dataUrl = canvasRef.current!.toDataURL() const blob = await (await fetch(dataUrl)).blob() const filesArray: File[] = [ - new File([blob], `${downloadImageFileName()}.png`, { - type: blob.type, + new File([blob], downloadImageFileName(), { + type: 'image/png', lastModified: new Date().getTime(), }), ] From 26e215a412042a68de6326b21c9fad8d3f020b46 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 12:38:30 -0400 Subject: [PATCH 05/54] can upload images for artworks --- .../layout/sidebar/image-sidebar.tsx | 10 + app/components/layout/sidebar/index.ts | 1 + app/models/artwork/artwork.get.server.ts | 16 ++ app/models/artwork/artwork.server.ts | 5 + .../images/artwork-image.create.server.ts | 43 ++++ app/models/images/artwork-image.server.ts | 12 + app/models/images/layer-image.ts | 11 + .../$branchSlug.$versionSlug.tsx | 6 +- .../sidebars.panel.artwork-version.images.tsx | 45 ++++ .../$artworkSlug+/__components/sidebars.tsx | 5 +- .../api.v1+/artwork.image.create.tsx | 229 ++++++++++++++++++ .../resources+/artwork-images.$imageId.tsx | 22 ++ app/schema/artwork-image.ts | 56 +++++ app/services/artwork/image/create.service.ts | 53 ++++ app/utils/conform-utils.ts | 56 +++++ app/utils/misc.tsx | 6 +- app/utils/routes.const.ts | 5 + .../migration.sql | 46 ++++ prisma/schema.prisma | 60 ++--- 19 files changed, 645 insertions(+), 42 deletions(-) create mode 100644 app/components/layout/sidebar/image-sidebar.tsx create mode 100644 app/models/images/artwork-image.create.server.ts create mode 100644 app/models/images/artwork-image.server.ts create mode 100644 app/models/images/layer-image.ts create mode 100644 app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx create mode 100644 app/routes/resources+/api.v1+/artwork.image.create.tsx create mode 100644 app/routes/resources+/artwork-images.$imageId.tsx create mode 100644 app/schema/artwork-image.ts create mode 100644 app/services/artwork/image/create.service.ts create mode 100644 prisma/migrations/20240606043952_add_images_to_artwork_and_layers/migration.sql diff --git a/app/components/layout/sidebar/image-sidebar.tsx b/app/components/layout/sidebar/image-sidebar.tsx new file mode 100644 index 00000000..1d90aa7c --- /dev/null +++ b/app/components/layout/sidebar/image-sidebar.tsx @@ -0,0 +1,10 @@ +import { createContainerComponent } from '../utils' + +const ImageSidebar = createContainerComponent({ + defaultTagName: 'div', + defaultClassName: + 'relative flex-1 flex w-full flex-col overflow-y-scroll p-4 gap-4 ', + displayName: 'ImageSidebar', +}) + +export { ImageSidebar } diff --git a/app/components/layout/sidebar/index.ts b/app/components/layout/sidebar/index.ts index 233af062..3e110469 100644 --- a/app/components/layout/sidebar/index.ts +++ b/app/components/layout/sidebar/index.ts @@ -1,3 +1,4 @@ +export * from './image-sidebar' export * from './sidebar' export * from './tabbed-sidebar' export * from './nav-sidebar' diff --git a/app/models/artwork/artwork.get.server.ts b/app/models/artwork/artwork.get.server.ts index 76a26234..3035ea30 100644 --- a/app/models/artwork/artwork.get.server.ts +++ b/app/models/artwork/artwork.get.server.ts @@ -4,6 +4,7 @@ import { type IArtworkWithProject, type IArtwork, type IArtworkWithBranchesAndVersions, + type IArtworkWithImages, } from '../artwork/artwork.server' export type queryArtworkWhereArgsType = z.infer @@ -73,3 +74,18 @@ export const getArtworkWithProject = async ({ }) return artwork } + +export const getArtworkWithImages = async ({ + where, +}: { + where: queryArtworkWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const artwork = await prisma.artwork.findFirst({ + where, + include: { + images: true, + }, + }) + return artwork +} diff --git a/app/models/artwork/artwork.server.ts b/app/models/artwork/artwork.server.ts index f77208db..614fc670 100644 --- a/app/models/artwork/artwork.server.ts +++ b/app/models/artwork/artwork.server.ts @@ -1,6 +1,7 @@ import { type Artwork } from '@prisma/client' import { type DateOrString } from '#app/definitions/prisma-helper' import { type IArtworkBranchWithVersions } from '../artwork-branch/artwork-branch.server' +import { type IArtworkImage } from '../images/artwork-image.server' import { type IProjectWithArtworks } from '../project/project.server' // Omitting 'createdAt' and 'updatedAt' from the Artwork interface @@ -18,3 +19,7 @@ export interface IArtworkWithProject extends IArtwork { export interface IArtworkWithBranchesAndVersions extends IArtwork { branches: IArtworkBranchWithVersions[] } + +export interface IArtworkWithImages extends IArtwork { + images: IArtworkImage[] +} diff --git a/app/models/images/artwork-image.create.server.ts b/app/models/images/artwork-image.create.server.ts new file mode 100644 index 00000000..9302554a --- /dev/null +++ b/app/models/images/artwork-image.create.server.ts @@ -0,0 +1,43 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { NewArtworkImageSchema } from '#app/schema/artwork-image' +import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IArtwork } from '../artwork/artwork.server' +import { type IArtworkImage } from './artwork-image.server' + +export interface IArtworkImageCreatedResponse { + success: boolean + message?: string + createdArtworkImage?: IArtworkImage +} + +export const validateNewArtworkImageSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkParentSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: NewArtworkImageSchema, + strategy, + }) +} + +export const createArtworkImage = async ({ + data, +}: { + data: { + artworkId: IArtwork['id'] + altText: string | null + contentType: string + blob: Buffer + } +}) => { + const artworkImage = await prisma.artworkImage.create({ + data, + }) + return artworkImage +} diff --git a/app/models/images/artwork-image.server.ts b/app/models/images/artwork-image.server.ts new file mode 100644 index 00000000..260ec1d0 --- /dev/null +++ b/app/models/images/artwork-image.server.ts @@ -0,0 +1,12 @@ +import { type ArtworkImage } from '@prisma/client' +import { type DateOrString } from '#app/definitions/prisma-helper' + +// Omitting 'createdAt' and 'updatedAt' from the ArtworkImage interface +// prisma query returns a string for these fields +// also excluding 'blob' field since this will be served from a resource route +type BaseArtworkImage = Omit + +export interface IArtworkImage extends BaseArtworkImage { + createdAt: DateOrString + updatedAt: DateOrString +} diff --git a/app/models/images/layer-image.ts b/app/models/images/layer-image.ts new file mode 100644 index 00000000..3bdad45c --- /dev/null +++ b/app/models/images/layer-image.ts @@ -0,0 +1,11 @@ +import { type LayerImage } from '@prisma/client' +import { type DateOrString } from '#app/definitions/prisma-helper' + +// Omitting 'createdAt' and 'updatedAt' from the LayerImage interface +// prisma query returns a string for these fields +type BaseLayerImage = Omit + +export interface ILayerImage extends BaseLayerImage { + createdAt: DateOrString + updatedAt: DateOrString +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx index 3d115648..2fb9e48f 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx @@ -12,7 +12,7 @@ import { FlexColumn, FlexRow, } from '#app/components/layout' -import { getArtwork } from '#app/models/artwork/artwork.get.server' +import { getArtworkWithImages } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' import { getArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.get.server' import { getUserBasic } from '#app/models/user/user.get.server' @@ -34,7 +34,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { invariantResponse(owner, 'Owner not found', { status: 404 }) // https://sergiodxa.com/tutorials/avoid-waterfalls-of-queries-in-remix-loaders - const artwork = await getArtwork({ + const artwork = await getArtworkWithImages({ where: { slug: params.artworkSlug, ownerId: owner.id }, }) invariantResponse(artwork, 'Artwork not found', { status: 404 }) @@ -57,7 +57,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const generator = await artworkVersionGeneratorBuildService({ version }) - return json({ version, selectedLayer, generator }) + return json({ artwork, version, selectedLayer, generator }) } export default function EditorProjectArtworkBranchVersionRoute() { diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx new file mode 100644 index 00000000..c0bc09f5 --- /dev/null +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -0,0 +1,45 @@ +import { useMatches } from '@remix-run/react' +import { memo, useCallback } from 'react' +import { ImageSidebar } from '#app/components/layout' +import { + SidebarPanel, + SidebarPanelHeader, + SidebarPanelRowActionsContainer, +} from '#app/components/templates' +import { type IArtworkWithImages } from '#app/models/artwork/artwork.server' +import { ArtworkImageCreate } from '#app/routes/resources+/api.v1+/artwork.image.create' +import { useRouteLoaderMatchData } from '#app/utils/matches' +import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' + +const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithImages }) => { + return +}) +ImageCreate.displayName = 'ImageCreate' + +export const PanelArtworkVersionImages = ({}: {}) => { + const matches = useMatches() + const { artwork } = useRouteLoaderMatchData( + matches, + artworkVersionLoaderRoute, + ) + + const artworkImageCreate = useCallback( + () => , + [artwork], + ) + + return ( +
+ + + + {artworkImageCreate()} + + + + +

{artwork.images.length} image(s)

+
+
+ ) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx index c02c6387..a1a8d850 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx @@ -3,6 +3,7 @@ import { SidebarTabs, SidebarTabsContent } from '#app/components/templates' import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' import { type ILayerWithDesigns } from '#app/models/layer/layer.server' import { PanelArtworkVersion } from './sidebars.panel.artwork-version' +import { PanelArtworkVersionImages } from './sidebars.panel.artwork-version.images' import { PanelArtworkVersionLayers } from './sidebars.panel.artwork-version.layers' import { PanelLayer } from './sidebars.panel.layer' @@ -13,12 +14,12 @@ export const SidebarLeft = ({ }) => { return ( - + - Add assets like images here + diff --git a/app/routes/resources+/api.v1+/artwork.image.create.tsx b/app/routes/resources+/api.v1+/artwork.image.create.tsx new file mode 100644 index 00000000..01218623 --- /dev/null +++ b/app/routes/resources+/api.v1+/artwork.image.create.tsx @@ -0,0 +1,229 @@ +import { conform, useForm } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { useState } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { ErrorList, TextareaField } from '#app/components/forms' +import { + DialogContentGrid, + DialogFormsContainer, +} from '#app/components/layout/dialog' +import { TooltipHydrated } from '#app/components/templates/tooltip' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' +import { Icon } from '#app/components/ui/icon' +import { Label } from '#app/components/ui/label' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { StatusButton } from '#app/components/ui/status-button' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { validateNewArtworkImageSubmission } from '#app/models/images/artwork-image.create.server' +import { + NewArtworkImageSchema, + MAX_UPLOAD_SIZE, +} from '#app/schema/artwork-image' +import { validateNoJS } from '#app/schema/form-data' +import { artworkImageCreateService } from '#app/services/artwork/image/create.service' +import { requireUserId } from '#app/utils/auth.server' +import { cn, useIsPending } from '#app/utils/misc' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.CREATE +const schema = NewArtworkImageSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + console.log('action 🚨') + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateNewArtworkImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + console.log('gonna create 🚨') + const { success, message } = await artworkImageCreateService({ + userId, + ...submission.value, + }) + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const ArtworkImageCreate = ({ artwork }: { artwork: IArtwork }) => { + const artworkId = artwork.id + const [open, setOpen] = useState(false) + const [previewImage, setPreviewImage] = useState(null) + const [altText] = useState('New Image...') + + const fetcher = useFetcher() + const isPending = useIsPending() + let isHydrated = useHydrated() + + const [form, fields] = useForm({ + id: `artwork-image-create-${artworkId}`, + constraint: getFieldsetConstraint(schema), + lastSubmission: fetcher.data?.submission, + onValidate: ({ formData }) => { + return parse(formData, { schema }) + }, + defaultValue: { + image: {}, + altText: '', + }, + }) + + return ( + + + + + + + + + Add a new image + + Add an image to the artwork that can be used on many layers, + branches, and versions. + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + + Submit + + +
+
+ ) +} diff --git a/app/routes/resources+/artwork-images.$imageId.tsx b/app/routes/resources+/artwork-images.$imageId.tsx new file mode 100644 index 00000000..31bee401 --- /dev/null +++ b/app/routes/resources+/artwork-images.$imageId.tsx @@ -0,0 +1,22 @@ +import { invariantResponse } from '@epic-web/invariant' +import { type LoaderFunctionArgs } from '@remix-run/node' +import { prisma } from '#app/utils/db.server.ts' + +export async function loader({ params }: LoaderFunctionArgs) { + invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) + const image = await prisma.artworkImage.findUnique({ + where: { id: params.imageId }, + select: { contentType: true, blob: true }, + }) + + invariantResponse(image, 'Not found', { status: 404 }) + + return new Response(image.blob, { + headers: { + 'Content-Type': image.contentType, + 'Content-Length': Buffer.byteLength(image.blob).toString(), + 'Content-Disposition': `inline; filename="${params.imageId}"`, + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }) +} diff --git a/app/schema/artwork-image.ts b/app/schema/artwork-image.ts new file mode 100644 index 00000000..e8007318 --- /dev/null +++ b/app/schema/artwork-image.ts @@ -0,0 +1,56 @@ +import { z } from 'zod' + +const MAX_ALT_TEXT_LENGTH = 240 +const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH) + +const MAX_MEGABYTES = 6 +export const MAX_UPLOAD_SIZE = 1024 * 1024 * MAX_MEGABYTES +const ACCEPTED_IMAGE_TYPES = [ + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/webp', + 'image/gif', +] + +const FileSchema = z + .instanceof(File) + .refine(file => file.size > 0, 'Image is required') + .refine( + file => file.size <= MAX_UPLOAD_SIZE, + 'Image size must be less than 3MB', + ) + .refine( + file => ACCEPTED_IMAGE_TYPES.includes(file.type), + 'Image must be a JPEG, PNG, WEBP, or GIF', + ) + +export const NewArtworkImageSchema = z.object({ + artworkId: z.string(), + file: FileSchema, + altText: AltTextSchema.optional(), +}) + +// issues with zod and Buffer +// https://github.com/colinhacks/zod/issues/387 +// const BlobSchema = z.custom(data => { +// return typeof window === 'undefined' +// ? data instanceof Buffer +// : data instanceof File +// }, 'Data is not an instance of a Buffer or File') + +export const ArtworkImageDataCreateSchema = z.object({ + artworkId: z.string(), + + // blob: BlobSchema, + // https://github.com/colinhacks/zod/issues/925 + // blob: z.instanceof(Buffer), + // blob: z.unknown().refine(val => val !== undefined, { + // message: 'stream must be defined', + // }), + // blob: z.any().refine(val => val !== undefined, { + // message: 'stream must be defined', + // }), + contentType: z.string(), + altText: AltTextSchema, +}) diff --git a/app/services/artwork/image/create.service.ts b/app/services/artwork/image/create.service.ts new file mode 100644 index 00000000..966b2f9f --- /dev/null +++ b/app/services/artwork/image/create.service.ts @@ -0,0 +1,53 @@ +import { invariant } from '@epic-web/invariant' +import { getArtwork } from '#app/models/artwork/artwork.get.server' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { + createArtworkImage, + type IArtworkImageCreatedResponse, +} from '#app/models/images/artwork-image.create.server' +import { type IUser } from '#app/models/user/user.server' +import { ArtworkImageDataCreateSchema } from '#app/schema/artwork-image' + +export const artworkImageCreateService = async ({ + userId, + artworkId, + blob, + contentType, + altText, +}: { + userId: IUser['id'] + artworkId: IArtwork['id'] + blob: Buffer + contentType: string + altText: string | null +}): Promise => { + try { + // Step 1: find the artwork + const artwork = await getArtwork({ + where: { id: artworkId, ownerId: userId }, + }) + invariant(artwork, 'Artwork not found') + + // Step 2: create image + const imageData = ArtworkImageDataCreateSchema.parse({ + artworkId, + contentType, + altText, + }) + + const createdArtworkImage = await createArtworkImage({ + data: { ...imageData, blob }, + }) + + return { + createdArtworkImage, + success: true, + } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error creating artwork generator.', + } + } +} diff --git a/app/utils/conform-utils.ts b/app/utils/conform-utils.ts index d94adfb9..6f9da3a1 100644 --- a/app/utils/conform-utils.ts +++ b/app/utils/conform-utils.ts @@ -63,3 +63,59 @@ export async function parseEntitySubmission({ async: true, }) } + +export async function validateEntityImageSubmission({ + userId, + formData, + schema, + strategy, +}: { + userId: string + formData: FormData + schema: z.ZodSchema + strategy: IValidateSubmissionStrategy +}) { + const submission = await parseEntityImageSubmission({ + userId, + formData, + schema, + strategy, + }) + if (submission.intent !== 'submit') { + return notSubmissionResponse(submission) + } + if (!submission.value) { + return submissionErrorResponse(submission) + } + return submissionSuccessResponse(submission) +} + +export async function parseEntityImageSubmission({ + userId, + formData, + schema, + strategy, +}: { + userId: string + formData: FormData + schema: z.ZodSchema + strategy: IValidateSubmissionStrategy +}) { + return await parse(formData, { + schema: schema + .superRefine(async (data, ctx) => { + strategy.validateFormDataEntity({ userId, data, ctx }) + }) + + .transform(async data => { + const file: File = data.file + if (file.size <= 0) return z.NEVER + return { + ...data, + contentType: file.type as string, + blob: Buffer.from(await file.arrayBuffer()), + } + }), + async: true, + }) +} diff --git a/app/utils/misc.tsx b/app/utils/misc.tsx index 2a8214af..9ea71cb7 100644 --- a/app/utils/misc.tsx +++ b/app/utils/misc.tsx @@ -13,6 +13,10 @@ export function getNoteImgSrc(imageId: string) { return `/resources/note-images/${imageId}` } +export function getArtworkImgSrc(imageId: string) { + return `/resources/artwork-images/${imageId}` +} + export function getErrorMessage(error: unknown) { if (typeof error === 'string') return error if ( @@ -216,7 +220,7 @@ export function useDoubleCheck() { : e => { e.preventDefault() setDoubleCheck(true) - } + } const onKeyUp: React.ButtonHTMLAttributes['onKeyUp'] = e => { diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 7a3c5493..f35450f8 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -4,6 +4,11 @@ export const Routes = { RESOURCES: { API: { V1: { + ARTWORK: { + IMAGE: { + CREATE: `${pathBase}/artwork/image/create`, + }, + }, ARTWORK_BRANCH: { CREATE: `${pathBase}/artwork-branch/create`, }, diff --git a/prisma/migrations/20240606043952_add_images_to_artwork_and_layers/migration.sql b/prisma/migrations/20240606043952_add_images_to_artwork_and_layers/migration.sql new file mode 100644 index 00000000..adda767d --- /dev/null +++ b/prisma/migrations/20240606043952_add_images_to_artwork_and_layers/migration.sql @@ -0,0 +1,46 @@ +/* + Warnings: + + - You are about to drop the `AssetImage` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `AssetImagesOnLayers` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "AssetImage"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "AssetImagesOnLayers"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "ArtworkImage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "altText" TEXT, + "contentType" TEXT NOT NULL, + "blob" BLOB NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "artworkId" TEXT NOT NULL, + CONSTRAINT "ArtworkImage_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "LayerImage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "altText" TEXT, + "contentType" TEXT NOT NULL, + "blob" BLOB NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "layerId" TEXT NOT NULL, + CONSTRAINT "LayerImage_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE INDEX "ArtworkImage_artworkId_idx" ON "ArtworkImage"("artworkId"); + +-- CreateIndex +CREATE INDEX "LayerImage_layerId_idx" ON "LayerImage"("layerId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f857461..2733a694 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,7 +37,6 @@ model User { artworkBranches ArtworkBranch[] layers Layer[] designs Design[] - assetImages AssetImage[] } model Note { @@ -225,6 +224,8 @@ model Artwork { branches ArtworkBranch[] mergeRequests ArtworkMergeRequest[] + images ArtworkImage[] + // non-unique foreign key @@index([projectId]) @@index([ownerId]) @@ -235,6 +236,22 @@ model Artwork { @@unique([slug, ownerId]) } +model ArtworkImage { + id String @id @default(cuid()) + altText String? + contentType String + blob Bytes + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade, onUpdate: Cascade) + artworkId String + + // non-unique foreign key + @@index([artworkId]) +} + model Layer { id String @id @default(cuid()) name String @@ -263,7 +280,7 @@ model Layer { children Layer[] @relation("ParentChildLayer") designs Design[] - assetImages AssetImagesOnLayers[] + images LayerImage[] // non-unique foreign key @@index([ownerId]) @@ -275,10 +292,10 @@ model Layer { @@unique([slug, ownerId, artworkVersionId]) } -model AssetImage { - id String @id @default(cuid()) - name String - slug String +// refactor image structure later to use a single table for all images +// mayber follow design type architecture +model LayerImage { + id String @id @default(cuid()) altText String? contentType String blob Bytes @@ -286,40 +303,11 @@ model AssetImage { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade) - ownerId String - - layers AssetImagesOnLayers[] - - // non-unique foreign key - @@index([ownerId]) - // This helps our order by in the user search a LOT - @@index([ownerId, updatedAt]) - // Unique constraint for slug scoped to ownerId - @@unique([slug, ownerId]) -} - -model AssetImagesOnLayers { - id String @id @default(cuid()) - isVisible Boolean @default(true) - // order of asset images in the layer - // considered linked lists, but not worth complexity unless I'm adding many asset images to a layer - order Int - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - assetImage AssetImage @relation(fields: [assetImageId], references: [id], onDelete: Cascade, onUpdate: Cascade) - assetImageId String - - layer Layer @relation(fields: [layerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + layer Layer @relation(fields: [layerId], references: [id], onDelete: Cascade, onUpdate: Cascade) layerId String // non-unique foreign key - @@index([assetImageId]) @@index([layerId]) - // This helps our order by in the user search a LOT - @@index([assetImageId, layerId]) } model Design { From e0e8d203b452711aeb442f35538e8fac3c9912d3 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 15:30:20 -0400 Subject: [PATCH 06/54] displaying images in sidebar, moved image upload to reusable fetcher form component --- app/components/image/image-preview.tsx | 52 +++++ app/components/image/index.ts | 1 + .../layout/sidebar/image-sidebar.tsx | 16 +- .../templates/form/fetcher-image-upload.tsx | 219 ++++++++++++++++++ .../images/artwork-image.create.server.ts | 1 + .../sidebars.panel.artwork-version.images.tsx | 20 +- .../api.v1+/artwork.image.create.tsx | 168 ++------------ app/schema/artwork-image.ts | 13 ++ app/services/artwork/image/create.service.ts | 5 + .../migration.sql | 19 ++ prisma/schema.prisma | 1 + 11 files changed, 361 insertions(+), 154 deletions(-) create mode 100644 app/components/image/image-preview.tsx create mode 100644 app/components/image/index.ts create mode 100644 app/components/templates/form/fetcher-image-upload.tsx create mode 100644 prisma/migrations/20240608173759_add_name_to_artwork_image/migration.sql diff --git a/app/components/image/image-preview.tsx b/app/components/image/image-preview.tsx new file mode 100644 index 00000000..aa7dcd84 --- /dev/null +++ b/app/components/image/image-preview.tsx @@ -0,0 +1,52 @@ +import { createContainerComponent } from '../layout/utils' + +const ImagePreviewContainer = createContainerComponent({ + defaultTagName: 'div', + defaultClassName: 'w-32', + displayName: 'ImagePreviewContainer', +}) + +const ImagePreviewWrapper = createContainerComponent({ + defaultTagName: 'div', + defaultClassName: 'relative h-32 w-32', + displayName: 'ImagePreviewWrapper', +}) + +const ImagePreviewLabel = createContainerComponent({ + defaultTagName: 'label', + defaultClassName: 'group absolute h-32 w-32 rounded-lg', + displayName: 'ImagePreviewLabel', +}) + +const ImagePreview = createContainerComponent({ + defaultTagName: 'img', + defaultClassName: 'h-32 w-32 rounded-lg object-cover', + displayName: 'ImagePreview', +}) + +const noImagePreviewClassName = + 'bg-accent opacity-40 focus-within:opacity-100 hover:opacity-100' + +const ImagePreviewSkeleton = createContainerComponent({ + defaultTagName: 'div', + defaultClassName: + 'flex h-32 w-32 items-center justify-center rounded-lg border border-muted-foreground text-4xl text-muted-foreground', + displayName: 'ImagePreviewSkeleton', +}) + +const ImageUploadInput = createContainerComponent({ + defaultTagName: 'input', + defaultClassName: + 'absolute left-0 top-0 z-0 h-32 w-32 cursor-pointer opacity-0', + displayName: 'ImageUploadInput', +}) + +export { + ImagePreviewContainer, + ImagePreviewWrapper, + ImagePreviewLabel, + ImagePreview, + noImagePreviewClassName, + ImagePreviewSkeleton, + ImageUploadInput, +} diff --git a/app/components/image/index.ts b/app/components/image/index.ts new file mode 100644 index 00000000..9b3d472c --- /dev/null +++ b/app/components/image/index.ts @@ -0,0 +1 @@ +export * from './image-preview' diff --git a/app/components/layout/sidebar/image-sidebar.tsx b/app/components/layout/sidebar/image-sidebar.tsx index 1d90aa7c..39f0b20d 100644 --- a/app/components/layout/sidebar/image-sidebar.tsx +++ b/app/components/layout/sidebar/image-sidebar.tsx @@ -3,8 +3,20 @@ import { createContainerComponent } from '../utils' const ImageSidebar = createContainerComponent({ defaultTagName: 'div', defaultClassName: - 'relative flex-1 flex w-full flex-col overflow-y-scroll p-4 gap-4 ', + 'relative flex-1 flex w-full flex-col overflow-y-scroll p-4 gap-4', displayName: 'ImageSidebar', }) -export { ImageSidebar } +const ImageSidebarList = createContainerComponent({ + defaultTagName: 'ul', + defaultClassName: 'flex flex-col gap-4 py-5', + displayName: 'ImageSidebarList', +}) + +const ImageSidebarListItem = createContainerComponent({ + defaultTagName: 'li', + defaultClassName: 'flex flex-col gap-4 px-2', + displayName: 'ImageSidebarListItem', +}) + +export { ImageSidebar, ImageSidebarList, ImageSidebarListItem } diff --git a/app/components/templates/form/fetcher-image-upload.tsx b/app/components/templates/form/fetcher-image-upload.tsx new file mode 100644 index 00000000..4901d976 --- /dev/null +++ b/app/components/templates/form/fetcher-image-upload.tsx @@ -0,0 +1,219 @@ +import { useForm, conform } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { type FetcherWithComponents } from '@remix-run/react' +import { useEffect, useState } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { type z } from 'zod' +import { ErrorList, Field, TextareaField } from '#app/components/forms' +import { + ImagePreview, + ImagePreviewContainer, + ImagePreviewLabel, + ImagePreviewSkeleton, + ImagePreviewWrapper, + ImageUploadInput, + noImagePreviewClassName, +} from '#app/components/image' +import { FlexColumn } from '#app/components/layout' +import { + DialogContentGrid, + DialogFormsContainer, +} from '#app/components/layout/dialog' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' +import { Icon, type IconName } from '#app/components/ui/icon' +import { Label } from '#app/components/ui/label' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { StatusButton } from '#app/components/ui/status-button' +import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { getArtworkImgSrc, useIsPending } from '#app/utils/misc' +import { TooltipHydrated } from '../tooltip' + +export const FetcherImageUpload = ({ + fetcher, + route, + schema, + formId, + image, + icon, + iconText, + tooltipText, + dialogTitle, + dialogDescription, + isHydrated, + children, +}: { + fetcher: FetcherWithComponents + route: string + schema: z.ZodSchema + formId: string + image?: IArtworkImage + icon: IconName + iconText: string + tooltipText: string + dialogTitle: string + dialogDescription: string + isHydrated: boolean + children: JSX.Element +}) => { + const [open, setOpen] = useState(false) + + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + const [form, fields] = useForm({ + id: formId, + constraint: getFieldsetConstraint(schema), + lastSubmission, + shouldValidate: 'onInput', + shouldRevalidate: 'onInput', + onValidate: ({ formData }) => { + return parse(formData, { schema }) + }, + defaultValue: { + id: image?.id ?? '', + name: image?.name ?? '', + altText: image?.altText ?? '', + }, + }) + + const [previewImage, setPreviewImage] = useState( + // TODO: get image to strategy + fields.id.defaultValue ? getArtworkImgSrc(fields.id.defaultValue) : null, + ) + const [altText, setAltText] = useState(fields.altText.defaultValue ?? '') + + // close after successful submission + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data?.status === 'success') { + setOpen(false) + } + }, [fetcher]) + + return ( + + + + + + + + + {dialogTitle} + {dialogDescription} + + + + + + + {children} + + + + + + + + {previewImage ? ( +
+ +
+ ) : ( + + + + )} + {image ? ( + + ) : null} + , + ) => { + const file = event.target.files?.[0] + + if (file) { + const reader = new FileReader() + reader.onloadend = () => { + setPreviewImage(reader.result as string) + } + reader.readAsDataURL(file) + } else { + setPreviewImage(null) + } + }} + accept="image/*" + {...conform.input(fields.file, { + type: 'file', + ariaAttributes: true, + })} + /> +
+
+
+ +
+
+ + setAltText(e.currentTarget.value), + }} + errors={fields.altText.errors} + /> +
+
+
+
+ + + Submit + + +
+
+ ) +} diff --git a/app/models/images/artwork-image.create.server.ts b/app/models/images/artwork-image.create.server.ts index 9302554a..53940b05 100644 --- a/app/models/images/artwork-image.create.server.ts +++ b/app/models/images/artwork-image.create.server.ts @@ -31,6 +31,7 @@ export const createArtworkImage = async ({ }: { data: { artworkId: IArtwork['id'] + name: string altText: string | null contentType: string blob: Buffer diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index c0bc09f5..962574fc 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -1,6 +1,11 @@ import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' -import { ImageSidebar } from '#app/components/layout' +import { ImagePreview } from '#app/components/image' +import { + ImageSidebar, + ImageSidebarList, + ImageSidebarListItem, +} from '#app/components/layout' import { SidebarPanel, SidebarPanelHeader, @@ -9,6 +14,7 @@ import { import { type IArtworkWithImages } from '#app/models/artwork/artwork.server' import { ArtworkImageCreate } from '#app/routes/resources+/api.v1+/artwork.image.create' import { useRouteLoaderMatchData } from '#app/utils/matches' +import { getArtworkImgSrc } from '#app/utils/misc' import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithImages }) => { @@ -38,7 +44,17 @@ export const PanelArtworkVersionImages = ({}: {}) => { -

{artwork.images.length} image(s)

+ + {artwork.images.map(image => ( + + +
{image.name}
+
+ ))} +
) diff --git a/app/routes/resources+/api.v1+/artwork.image.create.tsx b/app/routes/resources+/api.v1+/artwork.image.create.tsx index 01218623..5013b466 100644 --- a/app/routes/resources+/api.v1+/artwork.image.create.tsx +++ b/app/routes/resources+/api.v1+/artwork.image.create.tsx @@ -1,5 +1,3 @@ -import { conform, useForm } from '@conform-to/react' -import { getFieldsetConstraint, parse } from '@conform-to/zod' import { json, type ActionFunctionArgs, @@ -8,29 +6,9 @@ import { unstable_parseMultipartFormData as parseMultipartFormData, } from '@remix-run/node' import { useFetcher } from '@remix-run/react' -import { useState } from 'react' -import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' -import { ErrorList, TextareaField } from '#app/components/forms' -import { - DialogContentGrid, - DialogFormsContainer, -} from '#app/components/layout/dialog' -import { TooltipHydrated } from '#app/components/templates/tooltip' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '#app/components/ui/dialog' -import { Icon } from '#app/components/ui/icon' -import { Label } from '#app/components/ui/label' -import { PanelIconButton } from '#app/components/ui/panel-icon-button' -import { StatusButton } from '#app/components/ui/status-button' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtwork } from '#app/models/artwork/artwork.server' import { validateNewArtworkImageSubmission } from '#app/models/images/artwork-image.create.server' import { @@ -40,7 +18,6 @@ import { import { validateNoJS } from '#app/schema/form-data' import { artworkImageCreateService } from '#app/services/artwork/image/create.service' import { requireUserId } from '#app/utils/auth.server' -import { cn, useIsPending } from '#app/utils/misc' import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components @@ -55,7 +32,6 @@ export async function loader({ request }: LoaderFunctionArgs) { } export async function action({ request }: ActionFunctionArgs) { - console.log('action 🚨') const userId = await requireUserId(request) const formData = await parseMultipartFormData( request, @@ -71,7 +47,6 @@ export async function action({ request }: ActionFunctionArgs) { }) if (status === 'success') { - console.log('gonna create 🚨') const { success, message } = await artworkImageCreateService({ userId, ...submission.value, @@ -96,134 +71,27 @@ export async function action({ request }: ActionFunctionArgs) { export const ArtworkImageCreate = ({ artwork }: { artwork: IArtwork }) => { const artworkId = artwork.id - const [open, setOpen] = useState(false) - const [previewImage, setPreviewImage] = useState(null) - const [altText] = useState('New Image...') + const formId = `artwork-image-create-${artworkId}` const fetcher = useFetcher() - const isPending = useIsPending() let isHydrated = useHydrated() - const [form, fields] = useForm({ - id: `artwork-image-create-${artworkId}`, - constraint: getFieldsetConstraint(schema), - lastSubmission: fetcher.data?.submission, - onValidate: ({ formData }) => { - return parse(formData, { schema }) - }, - defaultValue: { - image: {}, - altText: '', - }, - }) - return ( - - - - - - - - - Add a new image - - Add an image to the artwork that can be used on many layers, - branches, and versions. - - - - - - - - - - - -
-
-
- -
-
- -
-
- -
-
-
-
- - - Submit - - -
-
+ +
+ +
+
) } diff --git a/app/schema/artwork-image.ts b/app/schema/artwork-image.ts index e8007318..138a74cb 100644 --- a/app/schema/artwork-image.ts +++ b/app/schema/artwork-image.ts @@ -1,5 +1,8 @@ import { z } from 'zod' +const MAX_NAME_LENGTH = 240 +const NameSchema = z.string().max(MAX_NAME_LENGTH) + const MAX_ALT_TEXT_LENGTH = 240 const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH) @@ -28,6 +31,15 @@ const FileSchema = z export const NewArtworkImageSchema = z.object({ artworkId: z.string(), file: FileSchema, + name: NameSchema, + altText: AltTextSchema.optional(), +}) + +export const EditArtworkImageSchema = z.object({ + id: z.string(), + artworkId: z.string(), + file: FileSchema, + name: NameSchema, altText: AltTextSchema.optional(), }) @@ -52,5 +64,6 @@ export const ArtworkImageDataCreateSchema = z.object({ // message: 'stream must be defined', // }), contentType: z.string(), + name: NameSchema, altText: AltTextSchema, }) diff --git a/app/services/artwork/image/create.service.ts b/app/services/artwork/image/create.service.ts index 966b2f9f..9e17aab8 100644 --- a/app/services/artwork/image/create.service.ts +++ b/app/services/artwork/image/create.service.ts @@ -13,12 +13,14 @@ export const artworkImageCreateService = async ({ artworkId, blob, contentType, + name, altText, }: { userId: IUser['id'] artworkId: IArtwork['id'] blob: Buffer contentType: string + name: string altText: string | null }): Promise => { try { @@ -29,9 +31,12 @@ export const artworkImageCreateService = async ({ invariant(artwork, 'Artwork not found') // Step 2: create image + // zod schema for blob Buffer/File is not working + // pass in separately from validation const imageData = ArtworkImageDataCreateSchema.parse({ artworkId, contentType, + name, altText, }) diff --git a/prisma/migrations/20240608173759_add_name_to_artwork_image/migration.sql b/prisma/migrations/20240608173759_add_name_to_artwork_image/migration.sql new file mode 100644 index 00000000..4c1b88da --- /dev/null +++ b/prisma/migrations/20240608173759_add_name_to_artwork_image/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ArtworkImage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL DEFAULT 'image', + "altText" TEXT, + "contentType" TEXT NOT NULL, + "blob" BLOB NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "artworkId" TEXT NOT NULL, + CONSTRAINT "ArtworkImage_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_ArtworkImage" ("altText", "artworkId", "blob", "contentType", "createdAt", "id", "updatedAt") SELECT "altText", "artworkId", "blob", "contentType", "createdAt", "id", "updatedAt" FROM "ArtworkImage"; +DROP TABLE "ArtworkImage"; +ALTER TABLE "new_ArtworkImage" RENAME TO "ArtworkImage"; +CREATE INDEX "ArtworkImage_artworkId_idx" ON "ArtworkImage"("artworkId"); +PRAGMA foreign_key_check("ArtworkImage"); +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2733a694..75b63508 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -238,6 +238,7 @@ model Artwork { model ArtworkImage { id String @id @default(cuid()) + name String @default("image") altText String? contentType String blob Bytes From 330875b0e606ca41150be55bab95bd454c6c4ee9 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 20:32:06 -0400 Subject: [PATCH 07/54] can edit image including file itself and delete image --- .../layout/sidebar/image-sidebar.tsx | 2 +- .../templates/form/fetcher-image-upload.tsx | 27 ++-- .../images/artwork-image.create.server.ts | 5 +- .../images/artwork-image.delete.server.ts | 31 +++++ app/models/images/artwork-image.get.server.ts | 32 +++++ .../images/artwork-image.update.server.ts | 68 ++++++++++ .../sidebars.panel.artwork-version.images.tsx | 48 +++++-- .../api.v1+/artwork.image.delete.tsx | 120 ++++++++++++++++++ .../api.v1+/artwork.image.update.tsx | 99 +++++++++++++++ app/schema/artwork-image.ts | 16 ++- app/services/artwork/image/create.service.ts | 15 ++- app/services/artwork/image/delete.service.ts | 37 ++++++ app/services/artwork/image/update.service.ts | 82 ++++++++++++ .../validate-submission.strategy.ts | 30 +++++ app/utils/conform-utils.ts | 60 +++++++-- app/utils/db.server.ts | 14 +- app/utils/prisma-extensions-artwork-image.ts | 57 +++++++++ app/utils/routes.const.ts | 2 + prisma/schema.prisma | 2 + 19 files changed, 696 insertions(+), 51 deletions(-) create mode 100644 app/models/images/artwork-image.delete.server.ts create mode 100644 app/models/images/artwork-image.get.server.ts create mode 100644 app/models/images/artwork-image.update.server.ts create mode 100644 app/routes/resources+/api.v1+/artwork.image.delete.tsx create mode 100644 app/routes/resources+/api.v1+/artwork.image.update.tsx create mode 100644 app/services/artwork/image/delete.service.ts create mode 100644 app/services/artwork/image/update.service.ts create mode 100644 app/utils/prisma-extensions-artwork-image.ts diff --git a/app/components/layout/sidebar/image-sidebar.tsx b/app/components/layout/sidebar/image-sidebar.tsx index 39f0b20d..92e1a89e 100644 --- a/app/components/layout/sidebar/image-sidebar.tsx +++ b/app/components/layout/sidebar/image-sidebar.tsx @@ -15,7 +15,7 @@ const ImageSidebarList = createContainerComponent({ const ImageSidebarListItem = createContainerComponent({ defaultTagName: 'li', - defaultClassName: 'flex flex-col gap-4 px-2', + defaultClassName: 'flex flex-col px-2', displayName: 'ImageSidebarListItem', }) diff --git a/app/components/templates/form/fetcher-image-upload.tsx b/app/components/templates/form/fetcher-image-upload.tsx index 4901d976..46678d45 100644 --- a/app/components/templates/form/fetcher-image-upload.tsx +++ b/app/components/templates/form/fetcher-image-upload.tsx @@ -1,5 +1,5 @@ import { useForm, conform } from '@conform-to/react' -import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { getFieldsetConstraint } from '@conform-to/zod' import { type FetcherWithComponents } from '@remix-run/react' import { useEffect, useState } from 'react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' @@ -64,6 +64,8 @@ export const FetcherImageUpload = ({ children: JSX.Element }) => { const [open, setOpen] = useState(false) + const [name, setName] = useState(image?.name ?? '') + const [altText, setAltText] = useState(image?.altText ?? '') const lastSubmission = fetcher.data?.submission const isPending = useIsPending() @@ -71,23 +73,17 @@ export const FetcherImageUpload = ({ id: formId, constraint: getFieldsetConstraint(schema), lastSubmission, - shouldValidate: 'onInput', - shouldRevalidate: 'onInput', - onValidate: ({ formData }) => { - return parse(formData, { schema }) - }, defaultValue: { id: image?.id ?? '', - name: image?.name ?? '', - altText: image?.altText ?? '', + name, + altText, }, }) const [previewImage, setPreviewImage] = useState( - // TODO: get image to strategy + // TODO: get image to strategy for other image types when needed fields.id.defaultValue ? getArtworkImgSrc(fields.id.defaultValue) : null, ) - const [altText, setAltText] = useState(fields.altText.defaultValue ?? '') // close after successful submission useEffect(() => { @@ -141,14 +137,6 @@ export const FetcherImageUpload = ({ )} - {image ? ( - - ) : null} setName(e.currentTarget.value), }} errors={fields.name.errors} /> diff --git a/app/models/images/artwork-image.create.server.ts b/app/models/images/artwork-image.create.server.ts index 53940b05..811c5fc7 100644 --- a/app/models/images/artwork-image.create.server.ts +++ b/app/models/images/artwork-image.create.server.ts @@ -26,7 +26,7 @@ export const validateNewArtworkImageSubmission = async ({ }) } -export const createArtworkImage = async ({ +export const createArtworkImage = ({ data, }: { data: { @@ -37,8 +37,7 @@ export const createArtworkImage = async ({ blob: Buffer } }) => { - const artworkImage = await prisma.artworkImage.create({ + return prisma.artworkImage.create({ data, }) - return artworkImage } diff --git a/app/models/images/artwork-image.delete.server.ts b/app/models/images/artwork-image.delete.server.ts new file mode 100644 index 00000000..86b3dc02 --- /dev/null +++ b/app/models/images/artwork-image.delete.server.ts @@ -0,0 +1,31 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteArtworkImageSchema } from '#app/schema/artwork-image' +import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IArtworkImage } from './artwork-image.server' + +export interface IArtworkImageDeletedResponse { + success: boolean + message?: string +} + +export const validateArtworkImageDeleteSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkParentSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteArtworkImageSchema, + strategy, + }) +} + +export const deleteArtworkImage = ({ id }: { id: IArtworkImage['id'] }) => { + return prisma.artworkImage.delete({ + where: { id }, + }) +} diff --git a/app/models/images/artwork-image.get.server.ts b/app/models/images/artwork-image.get.server.ts new file mode 100644 index 00000000..d7399f3c --- /dev/null +++ b/app/models/images/artwork-image.get.server.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { prisma } from '#app/utils/db.server' +import { type IArtworkImage } from './artwork-image.server' + +export type queryArtworkWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + artworkId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryArtworkWhereArgsType) => { + if (Object.values(where).some(value => !value)) { + throw new Error( + 'Null or undefined values are not allowed in query parameters for artwork.', + ) + } +} + +export const getArtworkImage = async ({ + where, +}: { + where: queryArtworkWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const artwork = await prisma.artworkImage.findFirst({ + where, + }) + return artwork +} diff --git a/app/models/images/artwork-image.update.server.ts b/app/models/images/artwork-image.update.server.ts new file mode 100644 index 00000000..9ebce2b7 --- /dev/null +++ b/app/models/images/artwork-image.update.server.ts @@ -0,0 +1,68 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { + ArtworkImageDataUpdateSchema, + EditArtworkImageSchema, +} from '#app/schema/artwork-image' +import { ValidateArtworkImageSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { findFirstArtworkImageInstance } from '#app/utils/prisma-extensions-artwork-image' +import { type IArtworkImage } from './artwork-image.server' + +export interface IArtworkImageUpdatedResponse { + success: boolean + message?: string + updatedArtworkImage?: IArtworkImage +} + +export const validateEditArtworkImageSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkImageSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: EditArtworkImageSchema, + strategy, + }) +} + +const getArtworkImageInstance = async ({ id }: { id: IArtworkImage['id'] }) => { + return await findFirstArtworkImageInstance({ + where: { id }, + }) +} + +export const updateArtworkImage = async ({ + id, + name, + altText, +}: { + id: IArtworkImage['id'] + name: string + altText: string | null +}): Promise => { + const artworkImage = await getArtworkImageInstance({ id }) + if (!artworkImage) return { success: false } + + try { + const data = ArtworkImageDataUpdateSchema.parse({ id, name, altText }) + + artworkImage.name = data.name + artworkImage.altText = data.altText + artworkImage.updatedAt = new Date() + await artworkImage.save() + + return { success: true, updatedArtworkImage: artworkImage } + } catch (error) { + // consider how to handle this error where this is called + console.log('updateArtworkImage error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 962574fc..7bec129b 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -2,6 +2,7 @@ import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' import { ImagePreview } from '#app/components/image' import { + FlexRow, ImageSidebar, ImageSidebarList, ImageSidebarListItem, @@ -11,8 +12,14 @@ import { SidebarPanelHeader, SidebarPanelRowActionsContainer, } from '#app/components/templates' -import { type IArtworkWithImages } from '#app/models/artwork/artwork.server' +import { + type IArtwork, + type IArtworkWithImages, +} from '#app/models/artwork/artwork.server' +import { type IArtworkImage } from '#app/models/images/artwork-image.server' import { ArtworkImageCreate } from '#app/routes/resources+/api.v1+/artwork.image.create' +import { ArtworkImageDelete } from '#app/routes/resources+/api.v1+/artwork.image.delete' +import { ArtworkImageUpdate } from '#app/routes/resources+/api.v1+/artwork.image.update' import { useRouteLoaderMatchData } from '#app/utils/matches' import { getArtworkImgSrc } from '#app/utils/misc' import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' @@ -22,6 +29,37 @@ const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithImages }) => { }) ImageCreate.displayName = 'ImageCreate' +const ImageUpdate = memo(({ image }: { image: IArtworkImage }) => { + return +}) +ImageUpdate.displayName = 'ImageUpdate' + +const ImageDelete = memo( + ({ image, artwork }: { image: IArtworkImage; artwork: IArtwork }) => { + return + }, +) +ImageDelete.displayName = 'ImageDelete' + +const ImageListItem = memo( + ({ image, artwork }: { image: IArtworkImage; artwork: IArtwork }) => { + return ( + + +
{image.name}
+ + +
+ +
+ ) + }, +) +ImageListItem.displayName = 'ImageListItem' + export const PanelArtworkVersionImages = ({}: {}) => { const matches = useMatches() const { artwork } = useRouteLoaderMatchData( @@ -46,13 +84,7 @@ export const PanelArtworkVersionImages = ({}: {}) => { {artwork.images.map(image => ( - - -
{image.name}
-
+ ))}
diff --git a/app/routes/resources+/api.v1+/artwork.image.delete.tsx b/app/routes/resources+/api.v1+/artwork.image.delete.tsx new file mode 100644 index 00000000..5b7052cd --- /dev/null +++ b/app/routes/resources+/api.v1+/artwork.image.delete.tsx @@ -0,0 +1,120 @@ +import { useForm } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { TooltipHydrated } from '#app/components/templates/tooltip' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { validateArtworkImageDeleteSubmission } from '#app/models/images/artwork-image.delete.server' +import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { DeleteArtworkImageSchema } from '#app/schema/artwork-image' +import { EntityParentIdType } from '#app/schema/entity' +import { validateNoJS } from '#app/schema/form-data' +import { artworkImageDeleteService } from '#app/services/artwork/image/delete.service' +import { requireUserId } from '#app/utils/auth.server' +import { useIsPending } from '#app/utils/misc' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.DELETE +const schema = DeleteArtworkImageSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateArtworkImageDeleteSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await artworkImageDeleteService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const ArtworkImageDelete = ({ + image, + artwork, +}: { + image: IArtworkImage + artwork: IArtwork +}) => { + const imageId = image.id + const iconText = `Delete image` + + const fetcher = useFetcher() + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + let isHydrated = useHydrated() + + const [form] = useForm({ + id: `artwork-image-delete-${imageId}`, + constraint: getFieldsetConstraint(schema), + lastSubmission, + onValidate: ({ formData }) => { + const parsed = parse(formData, { schema }) + console.log('parsed', parsed) + return parse(formData, { schema }) + }, + }) + + return ( + + + + + + + + + + + + ) +} diff --git a/app/routes/resources+/api.v1+/artwork.image.update.tsx b/app/routes/resources+/api.v1+/artwork.image.update.tsx new file mode 100644 index 00000000..c7c4a5a1 --- /dev/null +++ b/app/routes/resources+/api.v1+/artwork.image.update.tsx @@ -0,0 +1,99 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { validateEditArtworkImageSubmission } from '#app/models/images/artwork-image.update.server' +import { + EditArtworkImageSchema, + MAX_UPLOAD_SIZE, +} from '#app/schema/artwork-image' +import { validateNoJS } from '#app/schema/form-data' +import { artworkImageUpdateService } from '#app/services/artwork/image/update.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.UPDATE +const schema = EditArtworkImageSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateEditArtworkImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await artworkImageUpdateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const ArtworkImageUpdate = ({ image }: { image: IArtworkImage }) => { + const imageId = image.id + const formId = `artwork-image-update-${imageId}` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ +
+
+ ) +} diff --git a/app/schema/artwork-image.ts b/app/schema/artwork-image.ts index 138a74cb..828f9a16 100644 --- a/app/schema/artwork-image.ts +++ b/app/schema/artwork-image.ts @@ -37,8 +37,8 @@ export const NewArtworkImageSchema = z.object({ export const EditArtworkImageSchema = z.object({ id: z.string(), - artworkId: z.string(), - file: FileSchema, + artworkId: z.string().optional(), + file: FileSchema.optional(), name: NameSchema, altText: AltTextSchema.optional(), }) @@ -67,3 +67,15 @@ export const ArtworkImageDataCreateSchema = z.object({ name: NameSchema, altText: AltTextSchema, }) + +export const ArtworkImageDataUpdateSchema = z.object({ + id: z.string(), + contentType: z.string().optional(), + name: NameSchema, + altText: AltTextSchema, +}) + +export const DeleteArtworkImageSchema = z.object({ + id: z.string(), + artworkId: z.string(), +}) diff --git a/app/services/artwork/image/create.service.ts b/app/services/artwork/image/create.service.ts index 9e17aab8..1024a1a3 100644 --- a/app/services/artwork/image/create.service.ts +++ b/app/services/artwork/image/create.service.ts @@ -7,6 +7,7 @@ import { } from '#app/models/images/artwork-image.create.server' import { type IUser } from '#app/models/user/user.server' import { ArtworkImageDataCreateSchema } from '#app/schema/artwork-image' +import { prisma } from '#app/utils/db.server' export const artworkImageCreateService = async ({ userId, @@ -30,19 +31,27 @@ export const artworkImageCreateService = async ({ }) invariant(artwork, 'Artwork not found') - // Step 2: create image + // Step 2: validate image data // zod schema for blob Buffer/File is not working // pass in separately from validation const imageData = ArtworkImageDataCreateSchema.parse({ artworkId, contentType, name, - altText, + altText: altText || 'No description', }) - const createdArtworkImage = await createArtworkImage({ + // Step 3: create the artwork image via promise + const createArtworkImagePromises = [] + + const createArtworkImagePromise = createArtworkImage({ data: { ...imageData, blob }, }) + createArtworkImagePromises.push(createArtworkImagePromise) + + const [createdArtworkImage] = await prisma.$transaction( + createArtworkImagePromises, + ) return { createdArtworkImage, diff --git a/app/services/artwork/image/delete.service.ts b/app/services/artwork/image/delete.service.ts new file mode 100644 index 00000000..43279adb --- /dev/null +++ b/app/services/artwork/image/delete.service.ts @@ -0,0 +1,37 @@ +import { type User } from '@prisma/client' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { + type IArtworkImageDeletedResponse, + deleteArtworkImage, +} from '#app/models/images/artwork-image.delete.server' +import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { prisma } from '#app/utils/db.server' + +export const artworkImageDeleteService = async ({ + userId, + id, + artworkId, +}: { + userId: User['id'] + id: IArtworkImage['id'] + artworkId: IArtwork['id'] +}): Promise => { + try { + const deleteArtworkImagePromises = [] + + const deleteArtworkImagePromise = deleteArtworkImage({ id }) + deleteArtworkImagePromises.push(deleteArtworkImagePromise) + + await prisma.$transaction(deleteArtworkImagePromises) + + return { success: true } + } catch (error) { + console.log('artworkImageDeleteService error:', error) + const errorType = error instanceof Error + const errorMessage = errorType ? error.message : 'An unknown error occurred' + return { + success: false, + message: errorMessage, + } + } +} diff --git a/app/services/artwork/image/update.service.ts b/app/services/artwork/image/update.service.ts new file mode 100644 index 00000000..d7b8896c --- /dev/null +++ b/app/services/artwork/image/update.service.ts @@ -0,0 +1,82 @@ +import { invariant } from '@epic-web/invariant' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { createArtworkImage } from '#app/models/images/artwork-image.create.server' +import { deleteArtworkImage } from '#app/models/images/artwork-image.delete.server' +import { getArtworkImage } from '#app/models/images/artwork-image.get.server' +import { + updateArtworkImage, + type IArtworkImageUpdatedResponse, +} from '#app/models/images/artwork-image.update.server' +import { type IUser } from '#app/models/user/user.server' +import { ArtworkImageDataCreateSchema } from '#app/schema/artwork-image' +import { prisma } from '#app/utils/db.server' + +export const artworkImageUpdateService = async ({ + userId, + id, + blob, + contentType, + name, + altText, +}: { + userId: IUser['id'] + id: IArtwork['id'] + blob?: Buffer + contentType?: string + name: string + altText: string | null +}): Promise => { + try { + // can't seem to update the blob of an image + // so just going to replace with newly created image + if (blob) { + // Step 1: find the artwork image + const artworkImage = await getArtworkImage({ + where: { id }, + }) + invariant(artworkImage, 'Artwork Image not found') + + // Step 2: validata the new image data + const imageData = ArtworkImageDataCreateSchema.parse({ + artworkId: artworkImage.artworkId, + contentType, + name, + altText, + }) + + // Step 3: replace the artwork image via promises + const replaceArtworkImagePromises = [] + + // delete the old image + const deleteArtworkImagePromise = deleteArtworkImage({ id }) + replaceArtworkImagePromises.push(deleteArtworkImagePromise) + + // create the new image + const createArtworkImagePromise = createArtworkImage({ + data: { ...imageData, blob }, + }) + replaceArtworkImagePromises.push(createArtworkImagePromise) + + const [, replacedArtworkImage] = await prisma.$transaction( + replaceArtworkImagePromises, + ) + + return { + success: true, + updatedArtworkImage: replacedArtworkImage, + } + } else { + return await updateArtworkImage({ + id, + name, + altText, + }) + } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error creating artwork generator.', + } + } +} diff --git a/app/strategies/validate-submission.strategy.ts b/app/strategies/validate-submission.strategy.ts index 3d36553e..5b34e8d4 100644 --- a/app/strategies/validate-submission.strategy.ts +++ b/app/strategies/validate-submission.strategy.ts @@ -4,6 +4,7 @@ import { getArtwork } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' import { getArtworkVersion } from '#app/models/artwork-version/artwork-version.get.server' import { getDesign } from '#app/models/design/design.get.server' +import { getArtworkImage } from '#app/models/images/artwork-image.get.server' import { getLayer } from '#app/models/layer/layer.get.server' import { addNotFoundIssue } from '#app/utils/conform-utils' @@ -154,3 +155,32 @@ export class ValidateLayerParentSubmissionStrategy if (!layer) ctx.addIssue(addNotFoundIssue('Layer')) } } + +export class ValidateArtworkImageSubmissionStrategy + implements IValidateSubmissionStrategy +{ + async validateFormDataEntity({ + userId, + data, + ctx, + }: { + userId: User['id'] + data: any + ctx: any + }): Promise { + const { id } = data + // check for image + const image = await getArtworkImage({ + where: { id }, + }) + if (!image) ctx.addIssue(addNotFoundIssue('Image')) + + if (image) { + // check that image belongs to artwork that belongs to user + const artwork = await getArtwork({ + where: { id: image.artworkId, ownerId: userId }, + }) + if (!artwork) ctx.addIssue(addNotFoundIssue('Image')) + } + } +} diff --git a/app/utils/conform-utils.ts b/app/utils/conform-utils.ts index 6f9da3a1..231fcb61 100644 --- a/app/utils/conform-utils.ts +++ b/app/utils/conform-utils.ts @@ -106,16 +106,56 @@ export async function parseEntityImageSubmission({ .superRefine(async (data, ctx) => { strategy.validateFormDataEntity({ userId, data, ctx }) }) - - .transform(async data => { - const file: File = data.file - if (file.size <= 0) return z.NEVER - return { - ...data, - contentType: file.type as string, - blob: Buffer.from(await file.arrayBuffer()), - } - }), + .transform(transformData), async: true, }) } + +type FormDataWithId = { + id?: string + file?: File + name?: string + altText?: string +} + +async function transformData(data: FormDataWithId) { + if (data.id) { + const imageHasFile = Boolean(data.file?.size && data.file?.size > 0) + if (imageHasFile && data.file) { + const fileData = await transformFileData(data.file) + return getImageUpdateData(data, fileData) + } else { + return getImageUpdateData(data) + } + } else { + if (data.file && data.file.size > 0) { + const fileData = await transformFileData(data.file) + return { + ...data, + ...fileData, + } + } else { + return z.NEVER + } + } +} + +function getImageUpdateData( + data: FormDataWithId, + fileData?: { contentType: string; blob: Buffer }, +) { + return { + id: data.id, + name: data.name, + altText: data.altText, + ...(fileData && fileData), + } +} + +async function transformFileData(file: File) { + return { + name: file.name, + contentType: file.type as string, + blob: Buffer.from(await file.arrayBuffer()), + } +} diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts index c94958cc..050549b7 100644 --- a/app/utils/db.server.ts +++ b/app/utils/db.server.ts @@ -3,6 +3,7 @@ import { PrismaClient, type Prisma } from '@prisma/client' import { type DefaultArgs } from '@prisma/client/runtime/library' import chalk from 'chalk' import { ArtworkPrismaExtensions } from './prisma-extensions-artwork' +import { ArtworkImagePrismaExtensions } from './prisma-extensions-artwork-image' import { ArtworkVersionPrismaExtensions } from './prisma-extensions-artwork-version' import { DesignPrismaExtensions, @@ -27,6 +28,7 @@ export type PrismaTransactionType = Omit< export const prismaExtended = remember('prisma', () => { return new PrismaClient({}) .$extends(ArtworkPrismaExtensions) + .$extends(ArtworkImagePrismaExtensions) .$extends(ArtworkVersionPrismaExtensions) .$extends(DesignPrismaQueryExtensions) .$extends(DesignPrismaExtensions) @@ -62,12 +64,12 @@ export const prisma = remember('prisma', () => { e.duration < logThreshold * 1.1 ? 'green' : e.duration < logThreshold * 1.2 - ? 'blue' - : e.duration < logThreshold * 1.3 - ? 'yellow' - : e.duration < logThreshold * 1.4 - ? 'redBright' - : 'red' + ? 'blue' + : e.duration < logThreshold * 1.3 + ? 'yellow' + : e.duration < logThreshold * 1.4 + ? 'redBright' + : 'red' const dur = chalk[color](`${e.duration}ms`) console.info(`prisma:query - ${dur} - ${e.query}`) }) diff --git a/app/utils/prisma-extensions-artwork-image.ts b/app/utils/prisma-extensions-artwork-image.ts new file mode 100644 index 00000000..7b84040e --- /dev/null +++ b/app/utils/prisma-extensions-artwork-image.ts @@ -0,0 +1,57 @@ +import { Prisma } from '@prisma/client' +import { type whereArgsType } from '#app/schema/design' +import { prismaExtended } from './db.server' + +// must be in /utils to actually connect to prisma + +// just do extended when you want to .save() or .delete() +// this has to be in /utils to actually connect to prisma + +// instance methods .save() and .delete() +// https://github.com/prisma/prisma-client-extensions/blob/main/instance-methods/script.ts + +export const ArtworkImagePrismaExtensions = Prisma.defineExtension({ + result: { + artworkImage: { + save: { + needs: { id: true }, + compute(artworkImage) { + return () => { + return prismaExtended.artworkImage.update({ + where: { id: artworkImage.id }, + data: artworkImage, + }) + } + }, + }, + + delete: { + needs: { id: true }, + compute({ id }) { + return () => { + return prismaExtended.artworkImage.delete({ + where: { id }, + }) + } + }, + }, + }, + }, +}) + +// https://github.com/prisma/docs/issues/5058#issuecomment-1636473141 +export type ExtendedArtworkImage = Prisma.Result< + typeof prismaExtended.artworkImage, + any, + 'findFirstOrThrow' +> + +export const findFirstArtworkImageInstance = async ({ + where, +}: { + where: whereArgsType +}): Promise => { + return await prismaExtended.artworkImage.findFirst({ + where, + }) +} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index f35450f8..818b11fb 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -7,6 +7,8 @@ export const Routes = { ARTWORK: { IMAGE: { CREATE: `${pathBase}/artwork/image/create`, + DELETE: `${pathBase}/artwork/image/delete`, + UPDATE: `${pathBase}/artwork/image/update`, }, }, ARTWORK_BRANCH: { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 75b63508..3fa0873a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -236,6 +236,8 @@ model Artwork { @@unique([slug, ownerId]) } +// consider a service +// https://github.com/epicweb-dev/epic-stack/blob/main/docs/decisions/018-images.md model ArtworkImage { id String @id @default(cuid()) name String @default("image") From 71afa4db987b93e5ad7d7cd05126e3722712a544 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 20:45:53 -0400 Subject: [PATCH 08/54] updated remix-development-tools for Storage exceeded quota bug fix --- package-lock.json | 1756 ++++++++++++++++++++++----------------------- package.json | 2 +- vite.config.ts | 2 +- 3 files changed, 874 insertions(+), 886 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3584644f..dc048623 100644 --- a/package-lock.json +++ b/package-lock.json @@ -130,7 +130,7 @@ "prettier": "^3.1.0", "prettier-plugin-sql": "^0.17.0", "prettier-plugin-tailwindcss": "^0.5.7", - "remix-development-tools": "^3.7.4", + "remix-development-tools": "^4.1.6", "remix-flat-routes": "^0.6.2", "tsx": "^4.6.0", "typescript": "^5.3.2", @@ -144,9 +144,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "node_modules/@alloc/quick-lru": { @@ -174,12 +174,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -187,30 +187,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -235,9 +235,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.5.tgz", - "integrity": "sha512-gsUcqS/fPlgAw1kOtpss7uhY6E9SFFANQ6EFX5GTvzUwaV0+sGaZWk6xq22MOdeT9wfxyokW3ceCUvOiRtZciQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -262,12 +262,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.24.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -289,25 +289,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -341,19 +341,19 @@ "dev": true }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "semver": "^6.3.1" }, "engines": { @@ -373,74 +373,79 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -450,35 +455,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -488,89 +493,90 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -624,9 +630,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -636,12 +642,12 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", + "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -651,12 +657,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -666,12 +672,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -681,14 +687,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -698,12 +704,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", - "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -713,16 +719,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -732,12 +738,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", "dev": true, "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5" + "@babel/plugin-transform-react-jsx": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -747,12 +753,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", - "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -762,12 +768,12 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", - "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -777,13 +783,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", - "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -793,15 +799,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", - "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-typescript": "^7.24.1" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -811,17 +817,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", - "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-transform-react-display-name": "^7.24.1", - "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -831,16 +837,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", - "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.24.1", - "@babel/plugin-transform-typescript": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -850,9 +856,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -861,33 +867,33 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -896,13 +902,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -952,27 +958,27 @@ } }, "node_modules/@conform-to/dom": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-0.9.1.tgz", - "integrity": "sha512-+T6QgpLDPZ29j4y5nWW61WMe8qj0cQBymyzbi5sq1f3CcBO3wBbR0VPI7mbm+rLoErHWM3ghhdNlIkw0UPoLLA==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-0.9.2.tgz", + "integrity": "sha512-vYJvXMRxa9Ieslv5eVcak5+0QbcTv+HwdLqRPZ8lhSFZx5qQWJwdou+Iz+ERzsSQHh7QS3DDYG5HmzaN4evf9g==" }, "node_modules/@conform-to/react": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-0.9.1.tgz", - "integrity": "sha512-SibYHk86IFh60LRPk4aJHJOgyLTgb3JYphKvG4vcemyuntdOwncd5YM/GDoccVzeCTmJi4Os/fDtOOAaDcSa9w==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-0.9.2.tgz", + "integrity": "sha512-0eApSadAkM5/l1JNfQqpU+O/9lkjY89riG6jnrW8DWepyGfmN/COZtT7sRXla0SVOspXGJqGcKh+nk1f7Zbpaw==", "dependencies": { - "@conform-to/dom": "0.9.1" + "@conform-to/dom": "0.9.2" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/@conform-to/zod": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-0.9.1.tgz", - "integrity": "sha512-4hWBGzRpSd4RrlBFRXjS6QQ2MWFpMZDbDwz8H/xiq38Xl5yaL1RSZMhOJ4pmsmy8CIKghs78qvaD8ltTFlrNvQ==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-0.9.2.tgz", + "integrity": "sha512-treG9ZcuNuRERQ1uYvJSWT0zZuqHnYTzRwucg20+/WdjgKNSb60Br+Cy6BAHvVQ8dN6wJsGkHenkX2mSVw3xOA==", "peerDependencies": { - "@conform-to/dom": "0.9.1", + "@conform-to/dom": "0.9.2", "zod": "^3.21.0" } }, @@ -1101,9 +1107,9 @@ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" }, "node_modules/@epic-web/remember": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.0.2.tgz", - "integrity": "sha512-K7DcGoRPqVkjVhPEMQzqw7W/c3hq/3LuiI74he6SkXwR6A49aUmXpxmdb6o+NldY4FFtG42U7nL8PrqNGRxXuQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.1.0.tgz", + "integrity": "sha512-FIhO7PFUVEbcnrJOtom8gb4GXog4Z44n4Jxwmw2nkKt4mx8I/q/d0O4tMabjYndM1QX2oXvRYzpZxtP61s2P5A==" }, "node_modules/@epic-web/totp": { "version": "1.1.2", @@ -1510,9 +1516,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2075,9 +2081,9 @@ } }, "node_modules/@prisma/client": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz", - "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.15.0.tgz", + "integrity": "sha512-wPTeTjbd2Q0abOeffN7zCDCbkp9C9cF+e9HPiI64lmpehyq2TepgXE+sY7FXr7Rhbb21prLMnhXX27/E11V09w==", "hasInstallScript": true, "engines": { "node": ">=16.13" @@ -2092,43 +2098,43 @@ } }, "node_modules/@prisma/debug": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", - "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.15.0.tgz", + "integrity": "sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==" }, "node_modules/@prisma/engines": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", - "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.15.0.tgz", + "integrity": "sha512-hXL5Sn9hh/ZpRKWiyPA5GbvF3laqBHKt6Vo70hYqqOhh5e0ZXDzHcdmxNvOefEFeqxra2DMz2hNbFoPvqrVe1w==", "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.14.0", - "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "@prisma/fetch-engine": "5.14.0", - "@prisma/get-platform": "5.14.0" + "@prisma/debug": "5.15.0", + "@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "@prisma/fetch-engine": "5.15.0", + "@prisma/get-platform": "5.15.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", - "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==" + "version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022.tgz", + "integrity": "sha512-3BEgZ41Qb4oWHz9kZNofToRvNeS4LZYaT9pienR1gWkjhky6t6K1NyeWNBkqSj2llgraUNbgMOCQPY4f7Qp5wA==" }, "node_modules/@prisma/fetch-engine": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", - "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.15.0.tgz", + "integrity": "sha512-z6AY5yyXxc20Klj7wwnfGP0iIUkVKzybqapT02zLYR/nf9ynaeN8bq73WRmi1TkLYn+DJ5Qy+JGu7hBf1pE78A==", "dependencies": { - "@prisma/debug": "5.14.0", - "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "@prisma/get-platform": "5.14.0" + "@prisma/debug": "5.15.0", + "@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "@prisma/get-platform": "5.15.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", - "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.15.0.tgz", + "integrity": "sha512-1GULDkW4+/VQb73vihxCBSc4Chc2x88MA+O40tcZFjmBzG4/fF44PaXFxUqKSFltxU9L9GIMLhh0Gfkk/pUbtg==", "dependencies": { - "@prisma/debug": "5.14.0" + "@prisma/debug": "5.15.0" } }, "node_modules/@radix-ui/number": { @@ -4719,9 +4725,9 @@ } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.17.0.tgz", - "integrity": "sha512-njBWwVVFEb5SuGqk1KYiIcuKU3dEPuiaDN42hY72mfuQgeMR/RUZtibAQ5yu2Ii7yok6kewLe4OvztP2oP/IVQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.18.0.tgz", + "integrity": "sha512-9L4RbhS3WNtc/SokIhc0dwgcvs78YSQPakZejsrIgnzLzCi8mS6PeT+BY0+QCtsXxjd1egM8hqcJeB0lukBkXA==", "dev": true, "engines": { "node": ">= 14" @@ -4746,13 +4752,13 @@ } }, "node_modules/@sentry/bundler-plugin-core": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.17.0.tgz", - "integrity": "sha512-aIjCexNsB6DXtl/IngJcUxN7OalsyP5tS/4rqxj6pvqZbeg/7JMlMgy2nOOWsNhy+chX8swThS39dY8pCcEYLQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.18.0.tgz", + "integrity": "sha512-JvxVgsMFmDsU0Dgcx1CeFUC1scxOVSAOzOcE06qKAVm9BZzxHpI53iNfeMOXwVTUolD8LZVIfgOjkiXfwN/UPQ==", "dev": true, "dependencies": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "2.17.0", + "@sentry/babel-plugin-component-annotate": "2.18.0", "@sentry/cli": "^2.22.3", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -4807,9 +4813,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.31.2.tgz", - "integrity": "sha512-2aKyUx6La2P+pplL8+2vO67qJ+c1C79KYWAyQBE0JIT5kvKK9JpwtdNoK1F0/2mRpwhhYPADCz3sVIRqmL8cQQ==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.32.1.tgz", + "integrity": "sha512-MWkbkzZfnlE7s2pPbg4VozRSAeMlIObfZlTIou9ye6XnPt6ZmmxCLOuOgSKMv4sXg6aeqKNzMNiadThxCWyvPg==", "hasInstallScript": true, "dependencies": { "https-proxy-agent": "^5.0.0", @@ -4825,19 +4831,19 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.31.2", - "@sentry/cli-linux-arm": "2.31.2", - "@sentry/cli-linux-arm64": "2.31.2", - "@sentry/cli-linux-i686": "2.31.2", - "@sentry/cli-linux-x64": "2.31.2", - "@sentry/cli-win32-i686": "2.31.2", - "@sentry/cli-win32-x64": "2.31.2" + "@sentry/cli-darwin": "2.32.1", + "@sentry/cli-linux-arm": "2.32.1", + "@sentry/cli-linux-arm64": "2.32.1", + "@sentry/cli-linux-i686": "2.32.1", + "@sentry/cli-linux-x64": "2.32.1", + "@sentry/cli-win32-i686": "2.32.1", + "@sentry/cli-win32-x64": "2.32.1" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.31.2.tgz", - "integrity": "sha512-BHA/JJXj1dlnoZQdK4efRCtHRnbBfzbIZUKAze7oRR1RfNqERI84BVUQeKateD3jWSJXQfEuclIShc61KOpbKw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.32.1.tgz", + "integrity": "sha512-z/lEwANTYPCzbWTZ2+eeeNYxRLllC8knd0h+vtAKlhmGw/fyc/N39cznIFyFu+dLJ6tTdjOWOeikHtKuS/7onw==", "optional": true, "os": [ "darwin" @@ -4847,9 +4853,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.2.tgz", - "integrity": "sha512-W8k5mGYYZz/I/OxZH65YAK7dCkQAl+wbuoASGOQjUy5VDgqH0QJ8kGJufXvFPM+f3ZQGcKAnVsZ6tFqZXETBAw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.32.1.tgz", + "integrity": "sha512-m0lHkn+o4YKBq8KptGZvpT64FAwSl9mYvHZO9/ChnEGIJ/WyJwiN1X1r9JHVaW4iT5lD0Y5FAyq3JLkk0m0XHg==", "cpu": [ "arm" ], @@ -4863,9 +4869,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.2.tgz", - "integrity": "sha512-FLVKkJ/rWvPy/ka7OrUdRW63a/z8HYI1Gt8Pr6rWs50hb7YJja8lM8IO10tYmcFE/tODICsnHO9HTeUg2g2d1w==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.32.1.tgz", + "integrity": "sha512-hsGqHYuecUl1Yhq4MhiRejfh1gNlmhyNPcQEoO/DDRBnGnJyEAdiDpKXJcc2e/lT9k40B55Ob2CP1SeY040T2w==", "cpu": [ "arm64" ], @@ -4879,9 +4885,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.2.tgz", - "integrity": "sha512-A64QtzaPi3MYFpZ+Fwmi0mrSyXgeLJ0cWr4jdeTGrzNpeowSteKgd6tRKU+LVq0k5shKE7wdnHk+jXnoajulMA==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.32.1.tgz", + "integrity": "sha512-SuMLN1/ceFd3Q/B0DVyh5igjetTAF423txiABAHASenEev0lG0vZkRDXFclfgDtDUKRPmOXW7VDMirM3yZWQHQ==", "cpu": [ "x86", "ia32" @@ -4896,9 +4902,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.2.tgz", - "integrity": "sha512-YL/r+15R4mOEiU3mzn7iFQOeFEUB6KxeKGTTrtpeOGynVUGIdq4nV5rHow5JDbIzOuBS3SpOmcIMluvo1NCh0g==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.32.1.tgz", + "integrity": "sha512-x4FGd6xgvFddz8V/dh6jii4wy9qjWyvYLBTz8Fhi9rIP+b8wQ3oxwHIdzntareetZP7C1ggx+hZheiYocNYVwA==", "cpu": [ "x64" ], @@ -4912,9 +4918,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.2.tgz", - "integrity": "sha512-Az/2bmW+TFI059RE0mSBIxTBcoShIclz7BDebmIoCkZ+retrwAzpmBnBCDAHow+Yi43utOow+3/4idGa2OxcLw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.32.1.tgz", + "integrity": "sha512-i6aZma9mFzR+hqMY5VliQZEX6ypP/zUjPK0VtIMYWs5cC6PsQLRmuoeJmy3Z7d4nlh0CdK5NPC813Ej6RY6/vg==", "cpu": [ "x86", "ia32" @@ -4928,9 +4934,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.2.tgz", - "integrity": "sha512-XIzyRnJu539NhpFa+JYkotzVwv3NrZ/4GfHB/JWA2zReRvsk39jJG8D5HOmm0B9JA63QQT7Dt39RW8g3lkmb6w==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.32.1.tgz", + "integrity": "sha512-B58w/lRHLb4MUSjJNfMMw2cQykfimDCMLMmeK+1EiT2RmSeNQliwhhBxYcKk82a8kszH6zg3wT2vCea7LyPUyA==", "cpu": [ "x64" ], @@ -5095,12 +5101,12 @@ } }, "node_modules/@sentry/vite-plugin": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-2.17.0.tgz", - "integrity": "sha512-Zb/dz+8afIt9mFeYc9LclwZQDBUmlVe/FSo2os/jD/6DED21/NQZnPmMIvy2tIRxsa6yTDtBl2rfkNaQLuevsg==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-2.18.0.tgz", + "integrity": "sha512-yY8QSvbMjRpG5pzN6lnW5guZhyTDSGeWwM9tDyT9ix/ShODy/eE6jErisBtlo50lFJuew7x79WXnVykvds4Ddg==", "dev": true, "dependencies": { - "@sentry/bundler-plugin-core": "2.17.0", + "@sentry/bundler-plugin-core": "2.18.0", "unplugin": "1.0.1" }, "engines": { @@ -5944,9 +5950,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz", - "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", + "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", "dev": true, "dependencies": { "@types/node": "*", @@ -6070,9 +6076,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -7013,16 +7019,19 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -7376,9 +7385,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "funding": [ { "type": "opencollective", @@ -7394,10 +7403,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -7527,9 +7536,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "version": "1.0.30001629", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", + "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", "funding": [ { "type": "opencollective", @@ -8699,9 +8708,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -8770,9 +8779,9 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -9178,9 +9187,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.780", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.780.tgz", - "integrity": "sha512-NPtACGFe7vunRYzvYqVRhQvsDrTevxpgDKxG/Vcbe0BTNOY+5+/2mOXSw2ls7ToNbE5Bf/+uQbjTxcmwMozpCw==" + "version": "1.4.796", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz", + "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -9215,9 +9224,9 @@ "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -10020,29 +10029,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", "dev": true, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.3", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11" }, "engines": { "node": ">=4" @@ -10618,9 +10627,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", - "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==", "engines": { "node": ">= 16" }, @@ -11165,15 +11174,15 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "node_modules/glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -12466,9 +12475,9 @@ } }, "node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -12489,9 +12498,9 @@ "dev": true }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", + "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==", "bin": { "jiti": "bin/jiti.js" } @@ -12843,9 +12852,9 @@ } }, "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -14031,9 +14040,9 @@ } }, "node_modules/minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -14153,14 +14162,14 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", "dev": true, "dependencies": { "acorn": "^8.11.3", "pathe": "^1.1.2", - "pkg-types": "^1.1.0", + "pkg-types": "^1.1.1", "ufo": "^1.5.3" } }, @@ -14439,9 +14448,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.62.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", - "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", + "version": "3.63.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", + "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", "dependencies": { "semver": "^7.3.5" }, @@ -15772,9 +15781,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", + "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -15942,12 +15951,12 @@ } }, "node_modules/prisma": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", - "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.15.0.tgz", + "integrity": "sha512-JA81ACQSCi3a7NUOgonOIkdx8PAVkO+HbUOxmd00Yb8DgIIEpr2V9+Qe/j6MLxIgWtE/OtVQ54rVjfYRbZsCfw==", "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.14.0" + "@prisma/engines": "5.15.0" }, "bin": { "prisma": "build/index.js" @@ -16803,9 +16812,9 @@ } }, "node_modules/remix-auth": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.6.0.tgz", - "integrity": "sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.7.0.tgz", + "integrity": "sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==", "dependencies": { "uuid": "^8.3.2" }, @@ -16861,14 +16870,15 @@ } }, "node_modules/remix-development-tools": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-3.7.4.tgz", - "integrity": "sha512-hjqL3WsqvJZRWaK57CjGKjIqwrUxUeCjuqpdVQf1OufdmWTXSM8Gs0ciHlq0Q3Fyer+wfoZoZEUK3HJ2v8KpYg==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.1.6.tgz", + "integrity": "sha512-k2RkkQUVovEKXBxm51ad/MZqnxCaAt3qHYJnLYfTps/S7e/iz71/I0Z/HVqq/7UE446LkuQektk5k7929LLmoA==", "dev": true, "dependencies": { "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-select": "^1.2.2", "beautify": "^0.0.8", + "chalk": "^5.3.0", "clone": "^2.1.2", "clsx": "^2.0.0", "d3-hierarchy": "^3.1.2", @@ -16880,17 +16890,13 @@ "react-diff-viewer-continued": "^3.3.1", "tailwind-merge": "^1.14.0", "uuid": "^9.0.1", - "ws": "^8.14.2", "zod": "^3.22.4" }, - "bin": { - "rdt-cjs-serve": "dist/cli.cjs", - "rdt-serve": "dist/cli.js" - }, "peerDependencies": { "@remix-run/react": ">=1.15", "react": ">=17", - "react-dom": ">=17" + "react-dom": ">=17", + "vite": ">=5.0.0" } }, "node_modules/remix-development-tools/node_modules/@radix-ui/react-dismissable-layer": { @@ -17071,27 +17077,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/remix-development-tools/node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/remix-flat-routes": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/remix-flat-routes/-/remix-flat-routes-0.6.5.tgz", @@ -17177,9 +17162,9 @@ } }, "node_modules/remix-utils/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==", "engines": { "node": ">=16" }, @@ -17341,6 +17326,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -17366,6 +17352,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -17795,9 +17782,9 @@ } }, "node_modules/sonner": { - "version": "1.4.41", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", - "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" @@ -18391,9 +18378,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -18576,6 +18563,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -18815,9 +18803,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/tsconfck": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", - "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", "dev": true, "bin": { "tsconfck": "bin/tsconfck.js" @@ -18849,9 +18837,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -18873,9 +18861,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsx": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.11.0.tgz", - "integrity": "sha512-vzGGELOgAupsNVssAmZjbUDfdm/pWP4R+Kg8TVdsonxbXk0bEpE1qh0yV6/QxUVXaVlNemgcPajGdJJ82n3stg==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.14.1.tgz", + "integrity": "sha512-GU8pPJq8DdxcJDSK6Bc64c2jW8zBK2hb0jzwHZDfjapbwu6AqvFnAElnzZ17Xb9TH5a/j6/sicTCVYF+eO/cmA==", "dev": true, "dependencies": { "esbuild": "~0.20.2", @@ -19309,9 +19297,9 @@ } }, "node_modules/turbo-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.0.1.tgz", - "integrity": "sha512-sm0ZtcX9YWh28p5X8t5McxC2uthrt9p+g0bGE0KTVFhnhNWefpSVCr+67zRNDUOfo4bpXwiOp7otO+dyQ7/y/A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.2.0.tgz", + "integrity": "sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==" }, "node_modules/tw-to-css": { "version": "0.0.12", @@ -19568,9 +19556,9 @@ } }, "node_modules/undici": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz", - "integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==", + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.2.tgz", + "integrity": "sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==", "engines": { "node": ">=18.17" } @@ -20045,9 +20033,9 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", + "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "dev": true, "dependencies": { "esbuild": "^0.20.1", @@ -21073,9 +21061,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", "bin": { "yaml": "bin.mjs" }, @@ -21186,9 +21174,9 @@ }, "dependencies": { "@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, "@alloc/quick-lru": { @@ -21207,37 +21195,37 @@ } }, "@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "requires": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" } }, "@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true }, "@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -21254,9 +21242,9 @@ } }, "@babel/eslint-parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.5.tgz", - "integrity": "sha512-gsUcqS/fPlgAw1kOtpss7uhY6E9SFFANQ6EFX5GTvzUwaV0+sGaZWk6xq22MOdeT9wfxyokW3ceCUvOiRtZciQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", "dev": true, "requires": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -21273,12 +21261,12 @@ } }, "@babel/generator": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", - "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, "requires": { - "@babel/types": "^7.24.5", + "@babel/types": "^7.24.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -21293,22 +21281,22 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, "requires": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -21338,19 +21326,19 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", - "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "semver": "^6.3.1" }, "dependencies": { @@ -21363,150 +21351,156 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } }, "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", - "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dev": true, "requires": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "requires": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", - "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-plugin-utils": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", - "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", "dev": true }, "@babel/helper-replace-supers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", - "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" } }, "@babel/helper-simple-access": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", - "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "requires": { - "@babel/types": "^7.24.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-split-export-declaration": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", - "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "requires": { - "@babel/types": "^7.24.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true }, "@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true }, "@babel/helpers": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", - "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, "requires": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -21547,192 +21541,192 @@ } }, "@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true }, "@babel/plugin-syntax-decorators": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.1.tgz", - "integrity": "sha512-05RJdO/cCrtVWuAaSn1tS3bH8jbsJa/Y1uD186u6J4C/1mnHFxseeuWpsqr9anvo7TUulev7tm7GDwRV+VuhDw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", + "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", - "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", - "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", - "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/types": "^7.23.4" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", "dev": true, "requires": { - "@babel/plugin-transform-react-jsx": "^7.22.5" + "@babel/plugin-transform-react-jsx": "^7.24.7" } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz", - "integrity": "sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", - "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", - "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-typescript": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", - "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-typescript": "^7.24.1" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" } }, "@babel/preset-react": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", - "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-transform-react-display-name": "^7.24.1", - "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" } }, "@babel/preset-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", - "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-syntax-jsx": "^7.24.1", - "@babel/plugin-transform-modules-commonjs": "^7.24.1", - "@babel/plugin-transform-typescript": "^7.24.1" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" } }, "@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/traverse": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", - "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -21778,22 +21772,22 @@ } }, "@conform-to/dom": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-0.9.1.tgz", - "integrity": "sha512-+T6QgpLDPZ29j4y5nWW61WMe8qj0cQBymyzbi5sq1f3CcBO3wBbR0VPI7mbm+rLoErHWM3ghhdNlIkw0UPoLLA==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-0.9.2.tgz", + "integrity": "sha512-vYJvXMRxa9Ieslv5eVcak5+0QbcTv+HwdLqRPZ8lhSFZx5qQWJwdou+Iz+ERzsSQHh7QS3DDYG5HmzaN4evf9g==" }, "@conform-to/react": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-0.9.1.tgz", - "integrity": "sha512-SibYHk86IFh60LRPk4aJHJOgyLTgb3JYphKvG4vcemyuntdOwncd5YM/GDoccVzeCTmJi4Os/fDtOOAaDcSa9w==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-0.9.2.tgz", + "integrity": "sha512-0eApSadAkM5/l1JNfQqpU+O/9lkjY89riG6jnrW8DWepyGfmN/COZtT7sRXla0SVOspXGJqGcKh+nk1f7Zbpaw==", "requires": { - "@conform-to/dom": "0.9.1" + "@conform-to/dom": "0.9.2" } }, "@conform-to/zod": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-0.9.1.tgz", - "integrity": "sha512-4hWBGzRpSd4RrlBFRXjS6QQ2MWFpMZDbDwz8H/xiq38Xl5yaL1RSZMhOJ4pmsmy8CIKghs78qvaD8ltTFlrNvQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-0.9.2.tgz", + "integrity": "sha512-treG9ZcuNuRERQ1uYvJSWT0zZuqHnYTzRwucg20+/WdjgKNSb60Br+Cy6BAHvVQ8dN6wJsGkHenkX2mSVw3xOA==" }, "@emotion/babel-plugin": { "version": "11.11.0", @@ -21919,9 +21913,9 @@ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" }, "@epic-web/remember": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.0.2.tgz", - "integrity": "sha512-K7DcGoRPqVkjVhPEMQzqw7W/c3hq/3LuiI74he6SkXwR6A49aUmXpxmdb6o+NldY4FFtG42U7nL8PrqNGRxXuQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.1.0.tgz", + "integrity": "sha512-FIhO7PFUVEbcnrJOtom8gb4GXog4Z44n4Jxwmw2nkKt4mx8I/q/d0O4tMabjYndM1QX2oXvRYzpZxtP61s2P5A==" }, "@epic-web/totp": { "version": "1.1.2", @@ -22108,9 +22102,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", "dev": true }, "@eslint/eslintrc": { @@ -22538,47 +22532,47 @@ } }, "@prisma/client": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz", - "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==" + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.15.0.tgz", + "integrity": "sha512-wPTeTjbd2Q0abOeffN7zCDCbkp9C9cF+e9HPiI64lmpehyq2TepgXE+sY7FXr7Rhbb21prLMnhXX27/E11V09w==" }, "@prisma/debug": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", - "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.15.0.tgz", + "integrity": "sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==" }, "@prisma/engines": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", - "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.15.0.tgz", + "integrity": "sha512-hXL5Sn9hh/ZpRKWiyPA5GbvF3laqBHKt6Vo70hYqqOhh5e0ZXDzHcdmxNvOefEFeqxra2DMz2hNbFoPvqrVe1w==", "requires": { - "@prisma/debug": "5.14.0", - "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "@prisma/fetch-engine": "5.14.0", - "@prisma/get-platform": "5.14.0" + "@prisma/debug": "5.15.0", + "@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "@prisma/fetch-engine": "5.15.0", + "@prisma/get-platform": "5.15.0" } }, "@prisma/engines-version": { - "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", - "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==" + "version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022.tgz", + "integrity": "sha512-3BEgZ41Qb4oWHz9kZNofToRvNeS4LZYaT9pienR1gWkjhky6t6K1NyeWNBkqSj2llgraUNbgMOCQPY4f7Qp5wA==" }, "@prisma/fetch-engine": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", - "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.15.0.tgz", + "integrity": "sha512-z6AY5yyXxc20Klj7wwnfGP0iIUkVKzybqapT02zLYR/nf9ynaeN8bq73WRmi1TkLYn+DJ5Qy+JGu7hBf1pE78A==", "requires": { - "@prisma/debug": "5.14.0", - "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", - "@prisma/get-platform": "5.14.0" + "@prisma/debug": "5.15.0", + "@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022", + "@prisma/get-platform": "5.15.0" } }, "@prisma/get-platform": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", - "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.15.0.tgz", + "integrity": "sha512-1GULDkW4+/VQb73vihxCBSc4Chc2x88MA+O40tcZFjmBzG4/fF44PaXFxUqKSFltxU9L9GIMLhh0Gfkk/pUbtg==", "requires": { - "@prisma/debug": "5.14.0" + "@prisma/debug": "5.15.0" } }, "@radix-ui/number": { @@ -24068,9 +24062,9 @@ } }, "@sentry/babel-plugin-component-annotate": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.17.0.tgz", - "integrity": "sha512-njBWwVVFEb5SuGqk1KYiIcuKU3dEPuiaDN42hY72mfuQgeMR/RUZtibAQ5yu2Ii7yok6kewLe4OvztP2oP/IVQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.18.0.tgz", + "integrity": "sha512-9L4RbhS3WNtc/SokIhc0dwgcvs78YSQPakZejsrIgnzLzCi8mS6PeT+BY0+QCtsXxjd1egM8hqcJeB0lukBkXA==", "dev": true }, "@sentry/browser": { @@ -24089,13 +24083,13 @@ } }, "@sentry/bundler-plugin-core": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.17.0.tgz", - "integrity": "sha512-aIjCexNsB6DXtl/IngJcUxN7OalsyP5tS/4rqxj6pvqZbeg/7JMlMgy2nOOWsNhy+chX8swThS39dY8pCcEYLQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.18.0.tgz", + "integrity": "sha512-JvxVgsMFmDsU0Dgcx1CeFUC1scxOVSAOzOcE06qKAVm9BZzxHpI53iNfeMOXwVTUolD8LZVIfgOjkiXfwN/UPQ==", "dev": true, "requires": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "2.17.0", + "@sentry/babel-plugin-component-annotate": "2.18.0", "@sentry/cli": "^2.22.3", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -24134,17 +24128,17 @@ } }, "@sentry/cli": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.31.2.tgz", - "integrity": "sha512-2aKyUx6La2P+pplL8+2vO67qJ+c1C79KYWAyQBE0JIT5kvKK9JpwtdNoK1F0/2mRpwhhYPADCz3sVIRqmL8cQQ==", - "requires": { - "@sentry/cli-darwin": "2.31.2", - "@sentry/cli-linux-arm": "2.31.2", - "@sentry/cli-linux-arm64": "2.31.2", - "@sentry/cli-linux-i686": "2.31.2", - "@sentry/cli-linux-x64": "2.31.2", - "@sentry/cli-win32-i686": "2.31.2", - "@sentry/cli-win32-x64": "2.31.2", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.32.1.tgz", + "integrity": "sha512-MWkbkzZfnlE7s2pPbg4VozRSAeMlIObfZlTIou9ye6XnPt6ZmmxCLOuOgSKMv4sXg6aeqKNzMNiadThxCWyvPg==", + "requires": { + "@sentry/cli-darwin": "2.32.1", + "@sentry/cli-linux-arm": "2.32.1", + "@sentry/cli-linux-arm64": "2.32.1", + "@sentry/cli-linux-i686": "2.32.1", + "@sentry/cli-linux-x64": "2.32.1", + "@sentry/cli-win32-i686": "2.32.1", + "@sentry/cli-win32-x64": "2.32.1", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", @@ -24163,45 +24157,45 @@ } }, "@sentry/cli-darwin": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.31.2.tgz", - "integrity": "sha512-BHA/JJXj1dlnoZQdK4efRCtHRnbBfzbIZUKAze7oRR1RfNqERI84BVUQeKateD3jWSJXQfEuclIShc61KOpbKw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.32.1.tgz", + "integrity": "sha512-z/lEwANTYPCzbWTZ2+eeeNYxRLllC8knd0h+vtAKlhmGw/fyc/N39cznIFyFu+dLJ6tTdjOWOeikHtKuS/7onw==", "optional": true }, "@sentry/cli-linux-arm": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.31.2.tgz", - "integrity": "sha512-W8k5mGYYZz/I/OxZH65YAK7dCkQAl+wbuoASGOQjUy5VDgqH0QJ8kGJufXvFPM+f3ZQGcKAnVsZ6tFqZXETBAw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.32.1.tgz", + "integrity": "sha512-m0lHkn+o4YKBq8KptGZvpT64FAwSl9mYvHZO9/ChnEGIJ/WyJwiN1X1r9JHVaW4iT5lD0Y5FAyq3JLkk0m0XHg==", "optional": true }, "@sentry/cli-linux-arm64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.31.2.tgz", - "integrity": "sha512-FLVKkJ/rWvPy/ka7OrUdRW63a/z8HYI1Gt8Pr6rWs50hb7YJja8lM8IO10tYmcFE/tODICsnHO9HTeUg2g2d1w==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.32.1.tgz", + "integrity": "sha512-hsGqHYuecUl1Yhq4MhiRejfh1gNlmhyNPcQEoO/DDRBnGnJyEAdiDpKXJcc2e/lT9k40B55Ob2CP1SeY040T2w==", "optional": true }, "@sentry/cli-linux-i686": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.31.2.tgz", - "integrity": "sha512-A64QtzaPi3MYFpZ+Fwmi0mrSyXgeLJ0cWr4jdeTGrzNpeowSteKgd6tRKU+LVq0k5shKE7wdnHk+jXnoajulMA==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.32.1.tgz", + "integrity": "sha512-SuMLN1/ceFd3Q/B0DVyh5igjetTAF423txiABAHASenEev0lG0vZkRDXFclfgDtDUKRPmOXW7VDMirM3yZWQHQ==", "optional": true }, "@sentry/cli-linux-x64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.31.2.tgz", - "integrity": "sha512-YL/r+15R4mOEiU3mzn7iFQOeFEUB6KxeKGTTrtpeOGynVUGIdq4nV5rHow5JDbIzOuBS3SpOmcIMluvo1NCh0g==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.32.1.tgz", + "integrity": "sha512-x4FGd6xgvFddz8V/dh6jii4wy9qjWyvYLBTz8Fhi9rIP+b8wQ3oxwHIdzntareetZP7C1ggx+hZheiYocNYVwA==", "optional": true }, "@sentry/cli-win32-i686": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.31.2.tgz", - "integrity": "sha512-Az/2bmW+TFI059RE0mSBIxTBcoShIclz7BDebmIoCkZ+retrwAzpmBnBCDAHow+Yi43utOow+3/4idGa2OxcLw==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.32.1.tgz", + "integrity": "sha512-i6aZma9mFzR+hqMY5VliQZEX6ypP/zUjPK0VtIMYWs5cC6PsQLRmuoeJmy3Z7d4nlh0CdK5NPC813Ej6RY6/vg==", "optional": true }, "@sentry/cli-win32-x64": { - "version": "2.31.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.31.2.tgz", - "integrity": "sha512-XIzyRnJu539NhpFa+JYkotzVwv3NrZ/4GfHB/JWA2zReRvsk39jJG8D5HOmm0B9JA63QQT7Dt39RW8g3lkmb6w==", + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.32.1.tgz", + "integrity": "sha512-B58w/lRHLb4MUSjJNfMMw2cQykfimDCMLMmeK+1EiT2RmSeNQliwhhBxYcKk82a8kszH6zg3wT2vCea7LyPUyA==", "optional": true }, "@sentry/core": { @@ -24298,12 +24292,12 @@ } }, "@sentry/vite-plugin": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-2.17.0.tgz", - "integrity": "sha512-Zb/dz+8afIt9mFeYc9LclwZQDBUmlVe/FSo2os/jD/6DED21/NQZnPmMIvy2tIRxsa6yTDtBl2rfkNaQLuevsg==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-2.18.0.tgz", + "integrity": "sha512-yY8QSvbMjRpG5pzN6lnW5guZhyTDSGeWwM9tDyT9ix/ShODy/eE6jErisBtlo50lFJuew7x79WXnVykvds4Ddg==", "dev": true, "requires": { - "@sentry/bundler-plugin-core": "2.17.0", + "@sentry/bundler-plugin-core": "2.18.0", "unplugin": "1.0.1" } }, @@ -25001,9 +24995,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.1.tgz", - "integrity": "sha512-ej0phymbFLoCB26dbbq5PGScsf2JAJ4IJHjG10LalgUV36XKTmA4GdA+PVllKvRk0sEKt64X8975qFnkSi0hqA==", + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", + "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", "dev": true, "requires": { "@types/node": "*", @@ -25127,9 +25121,9 @@ "dev": true }, "@types/node": { - "version": "20.12.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -25829,15 +25823,15 @@ } }, "array.prototype.tosorted": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", - "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "requires": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, @@ -26098,14 +26092,14 @@ } }, "browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "requires": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.0.16" } }, "buffer": { @@ -26190,9 +26184,9 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, "caniuse-lite": { - "version": "1.0.30001621", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", - "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==" + "version": "1.0.30001629", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", + "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==" }, "ccount": { "version": "2.0.1", @@ -27030,9 +27024,9 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -27072,9 +27066,9 @@ "dev": true }, "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -27382,9 +27376,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.780", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.780.tgz", - "integrity": "sha512-NPtACGFe7vunRYzvYqVRhQvsDrTevxpgDKxG/Vcbe0BTNOY+5+/2mOXSw2ls7ToNbE5Bf/+uQbjTxcmwMozpCw==" + "version": "1.4.796", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz", + "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==" }, "emoji-regex": { "version": "9.2.2", @@ -27416,9 +27410,9 @@ "dev": true }, "enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -28145,29 +28139,29 @@ } }, "eslint-plugin-react": { - "version": "7.34.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz", - "integrity": "sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw==", + "version": "7.34.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", + "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", "dev": true, "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlast": "^1.2.4", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.3", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.17", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7", - "object.hasown": "^1.1.3", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.hasown": "^1.1.4", + "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.10" + "string.prototype.matchall": "^4.0.11" }, "dependencies": { "brace-expansion": { @@ -28505,9 +28499,9 @@ } }, "express-rate-limit": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.2.0.tgz", - "integrity": "sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==" + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.3.1.tgz", + "integrity": "sha512-BbaryvkY4wEgDqLgD18/NSy2lDO2jTuT9Y8c1Mpx0X63Yz0sYd5zN6KPe7UvpuSVvV33T6RaE1o1IVZQjHMYgw==" }, "extend": { "version": "3.0.2", @@ -28875,15 +28869,15 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" }, "glob": { - "version": "10.3.16", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz", - "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "requires": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" } }, "glob-parent": { @@ -29779,9 +29773,9 @@ } }, "jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -29794,9 +29788,9 @@ "dev": true }, "jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==" + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", + "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==" }, "js-beautify": { "version": "1.15.1", @@ -30055,9 +30049,9 @@ } }, "loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true }, "local-pkg": { @@ -30826,9 +30820,9 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "minipass": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", - "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==" + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" }, "minipass-collect": { "version": "1.0.2", @@ -30923,14 +30917,14 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", "dev": true, "requires": { "acorn": "^8.11.3", "pathe": "^1.1.2", - "pkg-types": "^1.1.0", + "pkg-types": "^1.1.1", "ufo": "^1.5.3" } }, @@ -31144,9 +31138,9 @@ "dev": true }, "node-abi": { - "version": "3.62.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", - "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", + "version": "3.63.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", + "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", "requires": { "semver": "^7.3.5" } @@ -32064,9 +32058,9 @@ "dev": true }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", + "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", "dev": true }, "prettier-plugin-sql": { @@ -32132,11 +32126,11 @@ } }, "prisma": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", - "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.15.0.tgz", + "integrity": "sha512-JA81ACQSCi3a7NUOgonOIkdx8PAVkO+HbUOxmd00Yb8DgIIEpr2V9+Qe/j6MLxIgWtE/OtVQ54rVjfYRbZsCfw==", "requires": { - "@prisma/engines": "5.14.0" + "@prisma/engines": "5.15.0" } }, "proc-log": { @@ -32767,9 +32761,9 @@ } }, "remix-auth": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.6.0.tgz", - "integrity": "sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.7.0.tgz", + "integrity": "sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==", "requires": { "uuid": "^8.3.2" } @@ -32804,14 +32798,15 @@ } }, "remix-development-tools": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-3.7.4.tgz", - "integrity": "sha512-hjqL3WsqvJZRWaK57CjGKjIqwrUxUeCjuqpdVQf1OufdmWTXSM8Gs0ciHlq0Q3Fyer+wfoZoZEUK3HJ2v8KpYg==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.1.6.tgz", + "integrity": "sha512-k2RkkQUVovEKXBxm51ad/MZqnxCaAt3qHYJnLYfTps/S7e/iz71/I0Z/HVqq/7UE446LkuQektk5k7929LLmoA==", "dev": true, "requires": { "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-select": "^1.2.2", "beautify": "^0.0.8", + "chalk": "^5.3.0", "clone": "^2.1.2", "clsx": "^2.0.0", "d3-hierarchy": "^3.1.2", @@ -32823,7 +32818,6 @@ "react-diff-viewer-continued": "^3.3.1", "tailwind-merge": "^1.14.0", "uuid": "^9.0.1", - "ws": "^8.14.2", "zod": "^3.22.4" }, "dependencies": { @@ -32923,12 +32917,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "dev": true - }, - "ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true } } }, @@ -32963,9 +32951,9 @@ }, "dependencies": { "type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==" + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.0.tgz", + "integrity": "sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==" } } }, @@ -33407,9 +33395,9 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "sonner": { - "version": "1.4.41", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", - "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==" }, "source-map": { "version": "0.7.4", @@ -33839,9 +33827,9 @@ } }, "tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -34190,9 +34178,9 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "tsconfck": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", - "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", "dev": true }, "tsconfig-paths": { @@ -34207,9 +34195,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "tsutils": { "version": "3.21.0", @@ -34227,9 +34215,9 @@ } }, "tsx": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.11.0.tgz", - "integrity": "sha512-vzGGELOgAupsNVssAmZjbUDfdm/pWP4R+Kg8TVdsonxbXk0bEpE1qh0yV6/QxUVXaVlNemgcPajGdJJ82n3stg==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.14.1.tgz", + "integrity": "sha512-GU8pPJq8DdxcJDSK6Bc64c2jW8zBK2hb0jzwHZDfjapbwu6AqvFnAElnzZ17Xb9TH5a/j6/sicTCVYF+eO/cmA==", "dev": true, "requires": { "esbuild": "~0.20.2", @@ -34440,9 +34428,9 @@ } }, "turbo-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.0.1.tgz", - "integrity": "sha512-sm0ZtcX9YWh28p5X8t5McxC2uthrt9p+g0bGE0KTVFhnhNWefpSVCr+67zRNDUOfo4bpXwiOp7otO+dyQ7/y/A==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.2.0.tgz", + "integrity": "sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==" }, "tw-to-css": { "version": "0.0.12", @@ -34622,9 +34610,9 @@ } }, "undici": { - "version": "6.18.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz", - "integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==" + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.2.tgz", + "integrity": "sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg==" }, "undici-types": { "version": "5.26.5", @@ -34935,9 +34923,9 @@ } }, "vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.2.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", + "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", "dev": true, "requires": { "esbuild": "^0.20.1", @@ -35510,9 +35498,9 @@ "dev": true }, "yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==" + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==" }, "yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index ff381341..5af643c1 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "prettier": "^3.1.0", "prettier-plugin-sql": "^0.17.0", "prettier-plugin-tailwindcss": "^0.5.7", - "remix-development-tools": "^3.7.4", + "remix-development-tools": "^4.1.6", "remix-flat-routes": "^0.6.2", "tsx": "^4.6.0", "typescript": "^5.3.2", diff --git a/vite.config.ts b/vite.config.ts index af3b4d4c..b629b524 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import { vitePlugin as remix } from '@remix-run/dev' import { sentryVitePlugin } from '@sentry/vite-plugin' import { glob } from 'glob' -import { remixDevTools } from 'remix-development-tools/vite' +import { remixDevTools } from 'remix-development-tools' import { flatRoutes } from 'remix-flat-routes' import { defineConfig } from 'vite' import tsconfigPaths from 'vite-tsconfig-paths' From 495afd7da1c064854176ac0d19362135e9aa964f Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 21:08:48 -0400 Subject: [PATCH 09/54] image click opens dialog with full view --- app/components/image/image-preview.tsx | 7 ++++ .../layout/sidebar/image-sidebar.tsx | 2 +- .../sidebars.panel.artwork-version.images.tsx | 33 ++++++++++++++++--- app/services/artwork/image/create.service.ts | 2 +- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/components/image/image-preview.tsx b/app/components/image/image-preview.tsx index aa7dcd84..dd7fd360 100644 --- a/app/components/image/image-preview.tsx +++ b/app/components/image/image-preview.tsx @@ -41,6 +41,12 @@ const ImageUploadInput = createContainerComponent({ displayName: 'ImageUploadInput', }) +const ImageFull = createContainerComponent({ + defaultTagName: 'img', + defaultClassName: 'w-full object-contain', + displayName: 'ImageFull', +}) + export { ImagePreviewContainer, ImagePreviewWrapper, @@ -49,4 +55,5 @@ export { noImagePreviewClassName, ImagePreviewSkeleton, ImageUploadInput, + ImageFull, } diff --git a/app/components/layout/sidebar/image-sidebar.tsx b/app/components/layout/sidebar/image-sidebar.tsx index 92e1a89e..7e6dce7f 100644 --- a/app/components/layout/sidebar/image-sidebar.tsx +++ b/app/components/layout/sidebar/image-sidebar.tsx @@ -15,7 +15,7 @@ const ImageSidebarList = createContainerComponent({ const ImageSidebarListItem = createContainerComponent({ defaultTagName: 'li', - defaultClassName: 'flex flex-col px-2', + defaultClassName: 'flex flex-col', displayName: 'ImageSidebarListItem', }) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 7bec129b..57441ff9 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -1,6 +1,6 @@ import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' -import { ImagePreview } from '#app/components/image' +import { ImageFull, ImagePreview } from '#app/components/image' import { FlexRow, ImageSidebar, @@ -12,6 +12,14 @@ import { SidebarPanelHeader, SidebarPanelRowActionsContainer, } from '#app/components/templates' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' import { type IArtwork, type IArtworkWithImages, @@ -50,10 +58,25 @@ const ImageListItem = memo( - + + + + + + + + {image.name} + {image.altText} + + + + ) }, diff --git a/app/services/artwork/image/create.service.ts b/app/services/artwork/image/create.service.ts index 1024a1a3..9119f597 100644 --- a/app/services/artwork/image/create.service.ts +++ b/app/services/artwork/image/create.service.ts @@ -38,7 +38,7 @@ export const artworkImageCreateService = async ({ artworkId, contentType, name, - altText: altText || 'No description', + altText: altText || 'No alt text provided.', }) // Step 3: create the artwork image via promise From dde4df3dc42a49861c40beae95c8a28011a1747f Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 21:29:30 -0400 Subject: [PATCH 10/54] layout changes --- .../sidebars.panel.artwork-version.images.tsx | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 57441ff9..75113f9a 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -55,28 +55,31 @@ const ImageListItem = memo(
{image.name}
- -
- - - - - - - - {image.name} - {image.altText} - - - - + + + + + + + + {image.name} + {image.altText} + + + + + + + + +
) }, From 400d77b57e474d502671d936149fc1d7632132b7 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 21:42:28 -0400 Subject: [PATCH 11/54] confirm for delete image --- .../templates/form/fetcher-icon-confirm.tsx | 101 ++++++++++++++++++ .../api.v1+/artwork.image.delete.tsx | 62 ++++------- 2 files changed, 124 insertions(+), 39 deletions(-) create mode 100644 app/components/templates/form/fetcher-icon-confirm.tsx diff --git a/app/components/templates/form/fetcher-icon-confirm.tsx b/app/components/templates/form/fetcher-icon-confirm.tsx new file mode 100644 index 00000000..78c8b31f --- /dev/null +++ b/app/components/templates/form/fetcher-icon-confirm.tsx @@ -0,0 +1,101 @@ +import { useForm } from '@conform-to/react' +import { getFieldsetConstraint } from '@conform-to/zod' +import { type FetcherWithComponents } from '@remix-run/react' +import { useEffect, useState } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { type z } from 'zod' +import { Button } from '#app/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' +import { type IconName } from '#app/components/ui/icon' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { StatusButton } from '#app/components/ui/status-button' +import { useIsPending } from '#app/utils/misc' +import { TooltipHydrated } from '../tooltip' + +export const FetcherIconConfirm = ({ + fetcher, + route, + schema, + formId, + icon, + iconText, + tooltipText, + dialogTitle, + dialogDescription, + confirmButtonText, + isHydrated, + children, +}: { + fetcher: FetcherWithComponents + route: string + schema: z.ZodSchema + formId: string + icon: IconName + iconText: string + tooltipText: string + dialogTitle: string + dialogDescription: string + confirmButtonText?: string + isHydrated: boolean + children: JSX.Element +}) => { + const [open, setOpen] = useState(false) + + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + const [form] = useForm({ + id: formId, + constraint: getFieldsetConstraint(schema), + lastSubmission, + }) + + // close after successful submission + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data?.status === 'success') { + setOpen(false) + } + }, [fetcher]) + + return ( + + + + + + + + + {dialogTitle} + {dialogDescription} + + + + + {children} + + + + + {confirmButtonText || 'Confirm'} + + + + + ) +} diff --git a/app/routes/resources+/api.v1+/artwork.image.delete.tsx b/app/routes/resources+/api.v1+/artwork.image.delete.tsx index 5b7052cd..b5603d90 100644 --- a/app/routes/resources+/api.v1+/artwork.image.delete.tsx +++ b/app/routes/resources+/api.v1+/artwork.image.delete.tsx @@ -1,16 +1,12 @@ -import { useForm } from '@conform-to/react' -import { getFieldsetConstraint, parse } from '@conform-to/zod' import { json, type ActionFunctionArgs, type LoaderFunctionArgs, } from '@remix-run/node' import { useFetcher } from '@remix-run/react' -import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' -import { TooltipHydrated } from '#app/components/templates/tooltip' -import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' import { type IArtwork } from '#app/models/artwork/artwork.server' import { validateArtworkImageDeleteSubmission } from '#app/models/images/artwork-image.delete.server' import { type IArtworkImage } from '#app/models/images/artwork-image.server' @@ -19,7 +15,6 @@ import { EntityParentIdType } from '#app/schema/entity' import { validateNoJS } from '#app/schema/form-data' import { artworkImageDeleteService } from '#app/services/artwork/image/delete.service' import { requireUserId } from '#app/utils/auth.server' -import { useIsPending } from '#app/utils/misc' import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components @@ -77,44 +72,33 @@ export const ArtworkImageDelete = ({ artwork: IArtwork }) => { const imageId = image.id - const iconText = `Delete image` + const iconText = `Delete Image...` + const formId = `artwork-image-delete-${imageId}` const fetcher = useFetcher() - const lastSubmission = fetcher.data?.submission - const isPending = useIsPending() let isHydrated = useHydrated() - const [form] = useForm({ - id: `artwork-image-delete-${imageId}`, - constraint: getFieldsetConstraint(schema), - lastSubmission, - onValidate: ({ formData }) => { - const parsed = parse(formData, { schema }) - console.log('parsed', parsed) - return parse(formData, { schema }) - }, - }) - return ( - - - - - - - - - +
+ + - - +
+ ) } From 0515b237badb067ea8c729f068728e6a6ab3b217 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 8 Jun 2024 21:46:24 -0400 Subject: [PATCH 12/54] display is back to default --- .../artworks+/$artworkSlug+/__components/sidebars.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx index a1a8d850..c058eb08 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx @@ -14,7 +14,7 @@ export const SidebarLeft = ({ }) => { return ( - + From e68403a3af3376a499d6e0ced63efadcba3fc0a0 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 00:58:00 -0400 Subject: [PATCH 13/54] created asset model to store images with details in attributes column --- .../migration.sql | 26 ++++++++++++++ prisma/schema.prisma | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 prisma/migrations/20240611045632_create_assets/migration.sql diff --git a/prisma/migrations/20240611045632_create_assets/migration.sql b/prisma/migrations/20240611045632_create_assets/migration.sql new file mode 100644 index 00000000..5c4ccca9 --- /dev/null +++ b/prisma/migrations/20240611045632_create_assets/migration.sql @@ -0,0 +1,26 @@ +-- CreateTable +CREATE TABLE "Asset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "slug" TEXT NOT NULL, + "type" TEXT NOT NULL, + "attributes" TEXT NOT NULL DEFAULT '{}', + "blob" BLOB, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ownerId" TEXT NOT NULL, + "artworkId" TEXT, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Asset_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE INDEX "Asset_ownerId_idx" ON "Asset"("ownerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Asset_slug_ownerId_key" ON "Asset"("slug", "ownerId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3fa0873a..784adb53 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,6 +37,7 @@ model User { artworkBranches ArtworkBranch[] layers Layer[] designs Design[] + assets Asset[] } model Note { @@ -225,6 +226,7 @@ model Artwork { mergeRequests ArtworkMergeRequest[] images ArtworkImage[] + assets Asset[] // non-unique foreign key @@index([projectId]) @@ -284,6 +286,7 @@ model Layer { designs Design[] images LayerImage[] + assets Asset[] // non-unique foreign key @@index([ownerId]) @@ -540,6 +543,7 @@ model ArtworkVersion { layers Layer[] designs Design[] + assets Asset[] // non-unique foreign key @@index([branchId]) @@ -574,3 +578,33 @@ model ArtworkMergeRequest { @@index([sourceBranchId]) @@index([targetBranchId]) } + +model Asset { + id String @id @default(cuid()) + name String + description String? + slug String + type String // e.g. image, media, palette, gradient, shapes, etc. + attributes String @default("{}") // json string of attributes specific to the type + blob Bytes? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + ownerId String + + artworkId String? + artwork Artwork? @relation(fields: [artworkId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + artworkVersionId String? + artworkVersion ArtworkVersion? @relation(fields: [artworkVersionId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + layer Layer? @relation(fields: [layerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + layerId String? + + // non-unique foreign key + @@index([ownerId]) + // Unique constraint for slug scoped to ownerId + @@unique([slug, ownerId]) +} From f57ba069695b48e7b57b9a314adcb79a52f25489 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 01:03:01 -0400 Subject: [PATCH 14/54] assets for projects too --- .../migration.sql | 31 +++++++++++++++++++ prisma/schema.prisma | 4 +++ 2 files changed, 35 insertions(+) create mode 100644 prisma/migrations/20240611050246_add_assets_to_projects/migration.sql diff --git a/prisma/migrations/20240611050246_add_assets_to_projects/migration.sql b/prisma/migrations/20240611050246_add_assets_to_projects/migration.sql new file mode 100644 index 00000000..efb9e62b --- /dev/null +++ b/prisma/migrations/20240611050246_add_assets_to_projects/migration.sql @@ -0,0 +1,31 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Asset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "slug" TEXT NOT NULL, + "type" TEXT NOT NULL, + "attributes" TEXT NOT NULL DEFAULT '{}', + "blob" BLOB, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ownerId" TEXT NOT NULL, + "projectId" TEXT, + "artworkId" TEXT, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Asset_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Asset" ("artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "slug", "type", "updatedAt") SELECT "artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "slug", "type", "updatedAt" FROM "Asset"; +DROP TABLE "Asset"; +ALTER TABLE "new_Asset" RENAME TO "Asset"; +CREATE INDEX "Asset_ownerId_idx" ON "Asset"("ownerId"); +CREATE UNIQUE INDEX "Asset_slug_ownerId_key" ON "Asset"("slug", "ownerId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 784adb53..cedd1425 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -197,6 +197,7 @@ model Project { ownerId String artworks Artwork[] + assets Asset[] // non-unique foreign key @@index([ownerId]) @@ -594,6 +595,9 @@ model Asset { owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade) ownerId String + projectId String? + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + artworkId String? artwork Artwork? @relation(fields: [artworkId], references: [id], onDelete: Cascade, onUpdate: Cascade) From b8daa3a7992853d0611220c76a404fed392a6419 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 20:14:24 -0400 Subject: [PATCH 15/54] image as asset type with json attributes for flexibility, can create --- .../templates/form/fetcher-image-upload.tsx | 17 ++- app/models/artwork/artwork.get.server.ts | 21 ++++ app/models/artwork/artwork.server.ts | 5 + app/models/asset/asset.server.ts | 31 ++++++ app/models/asset/image/image.create.server.ts | 56 ++++++++++ app/models/asset/image/image.get.server.ts | 31 ++++++ app/models/asset/image/image.server.ts | 17 +++ .../$branchSlug.$versionSlug.tsx | 4 +- .../sidebars.panel.artwork-version.images.tsx | 62 ++++++----- .../$artworkSlug+/__components/sidebars.tsx | 2 +- .../api.v1+/artwork.image.create.tsx | 6 +- .../api.v1+/artwork.image.delete.tsx | 4 +- .../api.v1+/artwork.image.update.tsx | 4 +- .../api.v1+/asset.image.artwork.create.tsx | 100 +++++++++++++++++ .../resources+/artwork-images.$imageId.tsx | 7 +- app/schema/asset.ts | 23 ++++ app/schema/asset/__shared.ts | 10 ++ app/schema/asset/image.ts | 80 +++++++++++++ app/schema/design.ts | 7 -- .../asset.image.artwork.create.service.ts | 76 +++++++++++++ app/utils/asset.ts | 105 ++++++++++++++++++ app/utils/asset/image.ts | 21 ++++ app/utils/conform-utils.ts | 52 +-------- .../conform/transform-asset-image.server.ts | 57 ++++++++++ app/utils/routes.const.ts | 7 ++ .../migration.sql | 35 ++++++ prisma/schema.prisma | 3 - 27 files changed, 738 insertions(+), 105 deletions(-) create mode 100644 app/models/asset/asset.server.ts create mode 100644 app/models/asset/image/image.create.server.ts create mode 100644 app/models/asset/image/image.get.server.ts create mode 100644 app/models/asset/image/image.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork.create.tsx create mode 100644 app/schema/asset.ts create mode 100644 app/schema/asset/__shared.ts create mode 100644 app/schema/asset/image.ts create mode 100644 app/services/asset.image.artwork.create.service.ts create mode 100644 app/utils/asset.ts create mode 100644 app/utils/asset/image.ts create mode 100644 app/utils/conform/transform-asset-image.server.ts create mode 100644 prisma/migrations/20240611222346_remove_slug_from_asset/migration.sql diff --git a/app/components/templates/form/fetcher-image-upload.tsx b/app/components/templates/form/fetcher-image-upload.tsx index 46678d45..7ebf5286 100644 --- a/app/components/templates/form/fetcher-image-upload.tsx +++ b/app/components/templates/form/fetcher-image-upload.tsx @@ -32,7 +32,7 @@ import { Icon, type IconName } from '#app/components/ui/icon' import { Label } from '#app/components/ui/label' import { PanelIconButton } from '#app/components/ui/panel-icon-button' import { StatusButton } from '#app/components/ui/status-button' -import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { getArtworkImgSrc, useIsPending } from '#app/utils/misc' import { TooltipHydrated } from '../tooltip' @@ -54,7 +54,7 @@ export const FetcherImageUpload = ({ route: string schema: z.ZodSchema formId: string - image?: IArtworkImage + image?: IAssetImage icon: IconName iconText: string tooltipText: string @@ -65,7 +65,7 @@ export const FetcherImageUpload = ({ }) => { const [open, setOpen] = useState(false) const [name, setName] = useState(image?.name ?? '') - const [altText, setAltText] = useState(image?.altText ?? '') + const [altText, setAltText] = useState(image?.attributes.altText ?? '') const lastSubmission = fetcher.data?.submission const isPending = useIsPending() @@ -76,6 +76,7 @@ export const FetcherImageUpload = ({ defaultValue: { id: image?.id ?? '', name, + description: image?.description ?? '', altText, }, }) @@ -112,7 +113,6 @@ export const FetcherImageUpload = ({ {...form.props} > - {children} @@ -182,6 +182,15 @@ export const FetcherImageUpload = ({ /> + @@ -89,3 +92,21 @@ export const getArtworkWithImages = async ({ }) return artwork } + +export const getArtworkWithAssets = async ({ + where, +}: { + where: queryArtworkWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const artwork = await prisma.artwork.findFirst({ + where, + include: { + assets: true, + }, + }) + invariant(artwork, 'Artwork not found') + + const validatedAssets = deserializeAssets({ assets: artwork.assets }) + return { ...artwork, assets: validatedAssets } +} diff --git a/app/models/artwork/artwork.server.ts b/app/models/artwork/artwork.server.ts index 614fc670..65f309d5 100644 --- a/app/models/artwork/artwork.server.ts +++ b/app/models/artwork/artwork.server.ts @@ -1,6 +1,7 @@ import { type Artwork } from '@prisma/client' import { type DateOrString } from '#app/definitions/prisma-helper' import { type IArtworkBranchWithVersions } from '../artwork-branch/artwork-branch.server' +import { type IAssetParsed } from '../asset/asset.server' import { type IArtworkImage } from '../images/artwork-image.server' import { type IProjectWithArtworks } from '../project/project.server' @@ -23,3 +24,7 @@ export interface IArtworkWithBranchesAndVersions extends IArtwork { export interface IArtworkWithImages extends IArtwork { images: IArtworkImage[] } + +export interface IArtworkWithAssets extends IArtwork { + assets: IAssetParsed[] +} diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts new file mode 100644 index 00000000..83965e83 --- /dev/null +++ b/app/models/asset/asset.server.ts @@ -0,0 +1,31 @@ +import { type Asset } from '@prisma/client' +import { type DateOrString } from '#app/definitions/prisma-helper' +import { type assetTypeEnum } from '#app/schema/asset' +import { + type IAssetImage, + type IAssetAttributesImage, +} from './image/image.server' + +// Omitting 'createdAt' and 'updatedAt' from the Asset interface +// prisma query returns a string for these fields +// omit type string to ensure type safety with assetTypeEnum +// omit attributes string so that extended asset types can insert their own attributes +type BaseAsset = Omit + +export interface IAsset extends BaseAsset { + type: string + attributes: string + createdAt: DateOrString + updatedAt: DateOrString +} + +export type IAssetAttributes = IAssetAttributesImage + +export interface IAssetParsed extends BaseAsset { + type: assetTypeEnum + attributes: IAssetAttributes + createdAt: DateOrString + updatedAt: DateOrString +} + +export type IAssetType = IAssetImage diff --git a/app/models/asset/image/image.create.server.ts b/app/models/asset/image/image.create.server.ts new file mode 100644 index 00000000..c55e5f17 --- /dev/null +++ b/app/models/asset/image/image.create.server.ts @@ -0,0 +1,56 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { type IUser } from '#app/models/user/user.server' +import { type AssetTypeEnum } from '#app/schema/asset' +import { NewAssetImageArtworkSchema } from '#app/schema/asset/image' +import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAsset } from '../asset.server' + +export interface IAssetImageCreatedResponse { + success: boolean + message?: string + createdAssetImage?: IAsset +} + +export const validateNewAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkParentSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: NewAssetImageArtworkSchema, + strategy, + }) +} + +export const createAssetImageArtwork = ({ + data, +}: { + data: { + ownerId: IUser['id'] + artworkId: IArtwork['id'] + name: string + description?: string + type: typeof AssetTypeEnum.IMAGE + attributes: { + contentType: string + altText?: string + } + blob: Buffer + } +}) => { + const { attributes, ...rest } = data + const jsonAttributes = JSON.stringify(attributes) + console.log('about to create...', jsonAttributes) + return prisma.asset.create({ + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts new file mode 100644 index 00000000..82991dd7 --- /dev/null +++ b/app/models/asset/image/image.get.server.ts @@ -0,0 +1,31 @@ +import { invariant } from '@epic-web/invariant' +import { AssetTypeEnum } from '#app/schema/asset' +import { parseAssetImageAttributes } from '#app/utils/asset/image' +import { prisma } from '#app/utils/db.server' +import { type IAssetImageSrc, type IAssetImage } from './image.server' + +export const getAssetImageArtwork = async ({ + id, +}: { + id: IAssetImage['id'] +}): Promise => { + const image = await prisma.asset.findUnique({ + where: { + id, + type: AssetTypeEnum.IMAGE, + }, + select: { + attributes: true, + blob: true, + }, + }) + invariant(image, 'Asset Image Not found: ' + id) + invariant(image.blob, 'Asset Image has no blob: ' + id) + + const attributes = parseAssetImageAttributes(image.attributes) + + return { + contentType: attributes.contentType, + blob: image.blob, + } +} diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts new file mode 100644 index 00000000..0c879d0b --- /dev/null +++ b/app/models/asset/image/image.server.ts @@ -0,0 +1,17 @@ +import { type AssetTypeEnum } from '#app/schema/asset' +import { type IAssetParsed } from '../asset.server' + +export interface IAssetImage extends IAssetParsed { + type: typeof AssetTypeEnum.IMAGE + attributes: IAssetAttributesImage +} + +export interface IAssetAttributesImage { + altText?: string + contentType: string +} + +export interface IAssetImageSrc { + contentType: string + blob: Buffer +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx index 2fb9e48f..d82a7623 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx @@ -12,7 +12,7 @@ import { FlexColumn, FlexRow, } from '#app/components/layout' -import { getArtworkWithImages } from '#app/models/artwork/artwork.get.server' +import { getArtworkWithAssets } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' import { getArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.get.server' import { getUserBasic } from '#app/models/user/user.get.server' @@ -34,7 +34,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { invariantResponse(owner, 'Owner not found', { status: 404 }) // https://sergiodxa.com/tutorials/avoid-waterfalls-of-queries-in-remix-loaders - const artwork = await getArtworkWithImages({ + const artwork = await getArtworkWithAssets({ where: { slug: params.artworkSlug, ownerId: owner.id }, }) invariantResponse(artwork, 'Artwork not found', { status: 404 }) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 75113f9a..b1bbaead 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -20,59 +20,55 @@ import { DialogTitle, DialogTrigger, } from '#app/components/ui/dialog' -import { - type IArtwork, - type IArtworkWithImages, -} from '#app/models/artwork/artwork.server' -import { type IArtworkImage } from '#app/models/images/artwork-image.server' -import { ArtworkImageCreate } from '#app/routes/resources+/api.v1+/artwork.image.create' +import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { ArtworkImageDelete } from '#app/routes/resources+/api.v1+/artwork.image.delete' import { ArtworkImageUpdate } from '#app/routes/resources+/api.v1+/artwork.image.update' +import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' +import { AssetTypeEnum } from '#app/schema/asset' +import { filterAssetType } from '#app/utils/asset' import { useRouteLoaderMatchData } from '#app/utils/matches' import { getArtworkImgSrc } from '#app/utils/misc' import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' -const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithImages }) => { - return +const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { + return }) ImageCreate.displayName = 'ImageCreate' -const ImageUpdate = memo(({ image }: { image: IArtworkImage }) => { +const ImageUpdate = memo(({ image }: { image: IAssetImage }) => { return }) ImageUpdate.displayName = 'ImageUpdate' const ImageDelete = memo( - ({ image, artwork }: { image: IArtworkImage; artwork: IArtwork }) => { + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { return }, ) ImageDelete.displayName = 'ImageDelete' const ImageListItem = memo( - ({ image, artwork }: { image: IArtworkImage; artwork: IArtwork }) => { + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { + const { id, name, attributes } = image + const { altText } = attributes + return ( - + -
{image.name}
+
{name}
- + - {image.name} - {image.altText} + {name} + {altText} - + @@ -92,9 +88,14 @@ export const PanelArtworkVersionImages = ({}: {}) => { matches, artworkVersionLoaderRoute, ) + const { assets } = artwork as IArtworkWithAssets + const images: IAssetImage[] = filterAssetType({ + assets, + type: AssetTypeEnum.IMAGE, + }) const artworkImageCreate = useCallback( - () => , + () => , [artwork], ) @@ -109,8 +110,17 @@ export const PanelArtworkVersionImages = ({}: {}) => { - {artwork.images.map(image => ( - + {images.length === 0 && ( + +
No images
+
+ )} + {images.map(image => ( + ))}
diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx index c058eb08..a1a8d850 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx @@ -14,7 +14,7 @@ export const SidebarLeft = ({ }) => { return ( - + diff --git a/app/routes/resources+/api.v1+/artwork.image.create.tsx b/app/routes/resources+/api.v1+/artwork.image.create.tsx index 5013b466..54682bf9 100644 --- a/app/routes/resources+/api.v1+/artwork.image.create.tsx +++ b/app/routes/resources+/api.v1+/artwork.image.create.tsx @@ -12,9 +12,9 @@ import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image import { type IArtwork } from '#app/models/artwork/artwork.server' import { validateNewArtworkImageSubmission } from '#app/models/images/artwork-image.create.server' import { - NewArtworkImageSchema, MAX_UPLOAD_SIZE, -} from '#app/schema/artwork-image' + NewAssetImageArtworkSchema, +} from '#app/schema/asset/image' import { validateNoJS } from '#app/schema/form-data' import { artworkImageCreateService } from '#app/services/artwork/image/create.service' import { requireUserId } from '#app/utils/auth.server' @@ -23,7 +23,7 @@ import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.CREATE -const schema = NewArtworkImageSchema +const schema = NewAssetImageArtworkSchema // auth GET request to endpoint export async function loader({ request }: LoaderFunctionArgs) { diff --git a/app/routes/resources+/api.v1+/artwork.image.delete.tsx b/app/routes/resources+/api.v1+/artwork.image.delete.tsx index b5603d90..afea4f4b 100644 --- a/app/routes/resources+/api.v1+/artwork.image.delete.tsx +++ b/app/routes/resources+/api.v1+/artwork.image.delete.tsx @@ -8,8 +8,8 @@ import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' import { type IArtwork } from '#app/models/artwork/artwork.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { validateArtworkImageDeleteSubmission } from '#app/models/images/artwork-image.delete.server' -import { type IArtworkImage } from '#app/models/images/artwork-image.server' import { DeleteArtworkImageSchema } from '#app/schema/artwork-image' import { EntityParentIdType } from '#app/schema/entity' import { validateNoJS } from '#app/schema/form-data' @@ -68,7 +68,7 @@ export const ArtworkImageDelete = ({ image, artwork, }: { - image: IArtworkImage + image: IAssetImage artwork: IArtwork }) => { const imageId = image.id diff --git a/app/routes/resources+/api.v1+/artwork.image.update.tsx b/app/routes/resources+/api.v1+/artwork.image.update.tsx index c7c4a5a1..d7ef54c7 100644 --- a/app/routes/resources+/api.v1+/artwork.image.update.tsx +++ b/app/routes/resources+/api.v1+/artwork.image.update.tsx @@ -9,7 +9,7 @@ import { useFetcher } from '@remix-run/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' -import { type IArtworkImage } from '#app/models/images/artwork-image.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { validateEditArtworkImageSubmission } from '#app/models/images/artwork-image.update.server' import { EditArtworkImageSchema, @@ -70,7 +70,7 @@ export async function action({ request }: ActionFunctionArgs) { ) } -export const ArtworkImageUpdate = ({ image }: { image: IArtworkImage }) => { +export const ArtworkImageUpdate = ({ image }: { image: IAssetImage }) => { const imageId = image.id const formId = `artwork-image-update-${imageId}` diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx new file mode 100644 index 00000000..066db1d2 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx @@ -0,0 +1,100 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.server' +import { + MAX_UPLOAD_SIZE, + NewAssetImageArtworkSchema, +} from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkCreateService } from '#app/services/asset.image.artwork.create.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK.CREATE +const schema = NewAssetImageArtworkSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateNewAssetImageArtworkSubmission({ + userId, + formData, + }) + console.log('validation:', status, submission) + + if (status === 'success') { + const { success, message } = await assetImageArtworkCreateService({ + userId, + ...submission.value, + }) + console.log('service:', success, message) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkCreate = ({ artwork }: { artwork: IArtwork }) => { + const artworkId = artwork.id + const formId = `asset-image-artwork-${artworkId}-create` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ +
+
+ ) +} diff --git a/app/routes/resources+/artwork-images.$imageId.tsx b/app/routes/resources+/artwork-images.$imageId.tsx index 31bee401..d7f150f3 100644 --- a/app/routes/resources+/artwork-images.$imageId.tsx +++ b/app/routes/resources+/artwork-images.$imageId.tsx @@ -1,13 +1,10 @@ import { invariantResponse } from '@epic-web/invariant' import { type LoaderFunctionArgs } from '@remix-run/node' -import { prisma } from '#app/utils/db.server.ts' +import { getAssetImageArtwork } from '#app/models/asset/image/image.get.server' export async function loader({ params }: LoaderFunctionArgs) { invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) - const image = await prisma.artworkImage.findUnique({ - where: { id: params.imageId }, - select: { contentType: true, blob: true }, - }) + const image = await getAssetImageArtwork({ id: params.imageId }) invariantResponse(image, 'Not found', { status: 404 }) diff --git a/app/schema/asset.ts b/app/schema/asset.ts new file mode 100644 index 00000000..41bffdb7 --- /dev/null +++ b/app/schema/asset.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type ILayer } from '#app/models/layer/layer.server' +import { type IProject } from '#app/models/project/project.server' +import { type ObjectValues } from '#app/utils/typescript-helpers' +import { AssetAttributesImageSchema } from './asset/image' + +export const AssetTypeEnum = { + IMAGE: 'image', + // add more asset types here +} as const +export type assetTypeEnum = ObjectValues + +export type AssetParentType = IProject | IArtwork | IArtworkVersion | ILayer + +// Dummy schema for demonstration +// ! remove this when adding more asset types +export const DummySchema = z.object({}) +export const AssetAttributesSchema = z.union([ + AssetAttributesImageSchema, + DummySchema, +]) diff --git a/app/schema/asset/__shared.ts b/app/schema/asset/__shared.ts new file mode 100644 index 00000000..14193c67 --- /dev/null +++ b/app/schema/asset/__shared.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +const MAX_NAME_LENGTH = 240 +export const AssetNameSchema = z.string().max(MAX_NAME_LENGTH) + +const MAX_DESCRIPTION_LENGTH = 255 +export const AssetDescriptionSchema = z + .string() + .max(MAX_DESCRIPTION_LENGTH) + .optional() diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts new file mode 100644 index 00000000..6d6276df --- /dev/null +++ b/app/schema/asset/image.ts @@ -0,0 +1,80 @@ +import { z } from 'zod' +import { AssetDescriptionSchema, AssetNameSchema } from './__shared' + +const MAX_ALT_TEXT_LENGTH = 240 +const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH).optional() + +const MAX_MEGABYTES = 6 +export const MAX_UPLOAD_SIZE = 1024 * 1024 * MAX_MEGABYTES +const ACCEPTED_IMAGE_TYPES = [ + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/webp', + 'image/gif', +] + +const FileSchema = z + .instanceof(File) + .refine(file => file.size > 0, 'Image is required') + .refine( + file => file.size <= MAX_UPLOAD_SIZE, + 'Image size must be less than 3MB', + ) + .refine( + file => ACCEPTED_IMAGE_TYPES.includes(file.type), + 'Image must be a JPEG, PNG, WEBP, or GIF', + ) + +// use this to (de)serealize data to/from the db +export const AssetAttributesImageSchema = z.object({ + altText: AltTextSchema, + contentType: z.string(), +}) + +// zod schema for blob Buffer/File is not working +// pass in separately from validation +export const AssetImageCreateDataSchema = z.object({ + name: AssetNameSchema, + description: AssetDescriptionSchema, + type: z.literal('image'), + attributes: AssetAttributesImageSchema, + ownerId: z.string(), +}) + +export const ArtworkImageDataUpdateSchema = z.object({ + id: z.string(), + contentType: z.string().optional(), + name: AssetNameSchema, + altText: AltTextSchema, +}) + +export const NewAssetImageSchema = z.object({ + file: FileSchema, + name: AssetNameSchema, + description: AssetDescriptionSchema, + altText: AltTextSchema, +}) + +export const EditArtworkImageSchema = z.object({ + id: z.string(), + file: FileSchema.optional(), + name: AssetNameSchema, + altText: AltTextSchema, +}) + +export const DeleteArtworkImageSchema = z.object({ + id: z.string(), +}) + +// parent is artwork + +const ArtworkParentSchema = z.object({ + artworkId: z.string(), +}) + +export const NewAssetImageArtworkSchema = + NewAssetImageSchema.merge(ArtworkParentSchema) + +export const AssetImageArtworkCreateDataSchema = + AssetImageCreateDataSchema.merge(ArtworkParentSchema) diff --git a/app/schema/design.ts b/app/schema/design.ts index 9f7c0c38..b5409407 100644 --- a/app/schema/design.ts +++ b/app/schema/design.ts @@ -32,13 +32,6 @@ export type DesignParentType = | IArtworkVersionWithDesignsAndLayers | ILayerWithDesigns -export const DesignParentTypeIdEnum = { - ARTWORK_VERSION_ID: 'artworkVersionId', - LAYER_ID: 'layerId', - // add more design types here -} as const -export type designParentTypeIdEnum = ObjectValues - export const DesignCloneSourceTypeEnum = { ARTWORK_VERSION: 'artworkVersion', LAYER: 'layer', diff --git a/app/services/asset.image.artwork.create.service.ts b/app/services/asset.image.artwork.create.service.ts new file mode 100644 index 00000000..8eca2d11 --- /dev/null +++ b/app/services/asset.image.artwork.create.service.ts @@ -0,0 +1,76 @@ +import { invariant } from '@epic-web/invariant' +import { getArtwork } from '#app/models/artwork/artwork.get.server' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { + type IAssetImageCreatedResponse, + createAssetImageArtwork, +} from '#app/models/asset/image/image.create.server' +import { type IUser } from '#app/models/user/user.server' +import { AssetTypeEnum } from '#app/schema/asset' +import { AssetImageArtworkCreateDataSchema } from '#app/schema/asset/image' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkCreateService = async ({ + userId, + artworkId, + name, + description, + blob, + contentType, + altText, +}: { + userId: IUser['id'] + artworkId: IArtwork['id'] + name: string + description?: string + blob: Buffer + contentType: string + altText: string | null +}): Promise => { + try { + // Step 1: verify the artwork exists + const artwork = await getArtwork({ + where: { id: artworkId, ownerId: userId }, + }) + invariant(artwork, 'Artwork not found') + + // Step 2: validate asset image data + // zod schema for blob Buffer/File is not working + // pass in separately from validation + const data = { + name, + description, + type: AssetTypeEnum.IMAGE, + attributes: { + contentType, + altText: altText || 'No alt text provided.', + }, + ownerId: userId, + artworkId, + } + console.log('data:', data) + const assetImageData = AssetImageArtworkCreateDataSchema.parse(data) + console.log('assetImageData:', assetImageData) + + // Step 3: create the asset image via promise + const createAssetImagePromise = createAssetImageArtwork({ + data: { ...assetImageData, blob }, + }) + + // Step 4: execute the transaction + const [createdAssetImage] = await prisma.$transaction([ + createAssetImagePromise, + ]) + + return { + createdAssetImage, + success: true, + } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkCreateService', + } + } +} diff --git a/app/utils/asset.ts b/app/utils/asset.ts new file mode 100644 index 00000000..5006358d --- /dev/null +++ b/app/utils/asset.ts @@ -0,0 +1,105 @@ +import { ZodError } from 'zod' +import { + type IAssetParsed, + type IAsset, + type IAssetAttributes, + type IAssetType, +} from '#app/models/asset/asset.server' +import { type assetTypeEnum } from '#app/schema/asset' +import { AssetAttributesImageSchema } from '#app/schema/asset/image' + +// Function to parse attributes from JSON string +export const parseAttributes = (asset: IAsset): IAssetParsed => { + try { + return { + ...asset, + type: asset.type as assetTypeEnum, + attributes: JSON.parse(asset.attributes) as IAssetAttributes, + } + } catch (error: any) { + throw new Error( + `Failed to parse attributes for asset with ID ${asset.id}: ${error.message}`, + ) + } +} + +// Function to stringify attributes to JSON string +export const stringifyAttributes = (asset: IAssetParsed): IAsset => { + try { + return { + ...asset, + attributes: JSON.stringify(asset.attributes), + } + } catch (error: any) { + throw new Error( + `Failed to stringify attributes for asset with ID ${asset.id}: ${error.message}`, + ) + } +} + +export const deserializeAssets = ({ + assets, +}: { + assets: IAsset[] +}): IAssetParsed[] => { + return assets.map(asset => { + return deserializeAsset({ asset }) + }) +} + +export const deserializeAsset = ({ + asset, +}: { + asset: IAsset +}): IAssetParsed => { + const parsedAsset = parseAttributes(asset) + const { type, attributes } = parsedAsset + + const validatedAssets = validateAttributes({ + attributes, + type, + }) + + return { + ...asset, + type, + attributes: validatedAssets, + } +} + +export const validateAttributes = ({ + attributes, + type, +}: { + attributes: IAssetAttributes + type: assetTypeEnum +}) => { + try { + switch (type) { + case 'image': + return AssetAttributesImageSchema.parse(attributes) + default: + throw new Error(`Unsupported asset type: ${type}`) + } + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset type ${type}: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset type ${type}: ${error.message}`, + ) + } + } +} + +export const filterAssetType = ({ + assets, + type, +}: { + assets: IAssetParsed[] + type: assetTypeEnum +}): IAssetType[] => { + return assets.filter(asset => asset.type === type) +} diff --git a/app/utils/asset/image.ts b/app/utils/asset/image.ts new file mode 100644 index 00000000..35c494c4 --- /dev/null +++ b/app/utils/asset/image.ts @@ -0,0 +1,21 @@ +import { ZodError } from 'zod' +import { type IAssetAttributesImage } from '#app/models/asset/image/image.server' +import { AssetAttributesImageSchema } from '#app/schema/asset/image' + +export const parseAssetImageAttributes = ( + attributes: string, +): IAssetAttributesImage => { + try { + return AssetAttributesImageSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/utils/conform-utils.ts b/app/utils/conform-utils.ts index 231fcb61..60bfa44a 100644 --- a/app/utils/conform-utils.ts +++ b/app/utils/conform-utils.ts @@ -2,6 +2,7 @@ import { type Submission } from '@conform-to/react' import { parse } from '@conform-to/zod' import { z } from 'zod' import { type IValidateSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { transformAssetImageData } from './conform/transform-asset-image.server' export const notSubmissionResponse = (submission: Submission) => ({ status: 'idle', submission }) as const @@ -106,56 +107,7 @@ export async function parseEntityImageSubmission({ .superRefine(async (data, ctx) => { strategy.validateFormDataEntity({ userId, data, ctx }) }) - .transform(transformData), + .transform(transformAssetImageData), async: true, }) } - -type FormDataWithId = { - id?: string - file?: File - name?: string - altText?: string -} - -async function transformData(data: FormDataWithId) { - if (data.id) { - const imageHasFile = Boolean(data.file?.size && data.file?.size > 0) - if (imageHasFile && data.file) { - const fileData = await transformFileData(data.file) - return getImageUpdateData(data, fileData) - } else { - return getImageUpdateData(data) - } - } else { - if (data.file && data.file.size > 0) { - const fileData = await transformFileData(data.file) - return { - ...data, - ...fileData, - } - } else { - return z.NEVER - } - } -} - -function getImageUpdateData( - data: FormDataWithId, - fileData?: { contentType: string; blob: Buffer }, -) { - return { - id: data.id, - name: data.name, - altText: data.altText, - ...(fileData && fileData), - } -} - -async function transformFileData(file: File) { - return { - name: file.name, - contentType: file.type as string, - blob: Buffer.from(await file.arrayBuffer()), - } -} diff --git a/app/utils/conform/transform-asset-image.server.ts b/app/utils/conform/transform-asset-image.server.ts new file mode 100644 index 00000000..c9154021 --- /dev/null +++ b/app/utils/conform/transform-asset-image.server.ts @@ -0,0 +1,57 @@ +import { z } from 'zod' + +type FormDataWithId = { + id?: string + file?: File + name?: string + altText?: string +} + +export async function transformAssetImageData(data: FormDataWithId) { + return data.id + ? transformAssetImageDataUpdate(data) + : transformAssetImageDataCreate(data) +} + +async function transformAssetImageDataUpdate(data: FormDataWithId) { + const imageHasFile = Boolean(data.file?.size && data.file?.size > 0) + if (imageHasFile && data.file) { + // new image file, update the data and the file + const fileData = await transformFileData(data.file) + return getImageUpdateData(data, fileData) + } else { + // no new image file, just update the data + return getImageUpdateData(data) + } +} + +async function transformAssetImageDataCreate(data: FormDataWithId) { + if (data.file && data.file.size > 0) { + const fileData = await transformFileData(data.file) + return { + ...data, + ...fileData, + } + } else { + return z.NEVER + } +} + +function getImageUpdateData( + data: FormDataWithId, + fileData?: { contentType: string; blob: Buffer }, +) { + return { + id: data.id, + name: data.name, + altText: data.altText, + ...(fileData && fileData), + } +} + +async function transformFileData(file: File) { + return { + contentType: file.type as string, + blob: Buffer.from(await file.arrayBuffer()), + } +} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 818b11fb..0b8f1c82 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -43,6 +43,13 @@ export const Routes = { }, }, }, + ASSET: { + IMAGE: { + ARTWORK: { + CREATE: `${pathBase}/asset/image/artwork/create`, + }, + }, + }, DESIGN: { TYPE: { LAYOUT: { diff --git a/prisma/migrations/20240611222346_remove_slug_from_asset/migration.sql b/prisma/migrations/20240611222346_remove_slug_from_asset/migration.sql new file mode 100644 index 00000000..9bf9af0a --- /dev/null +++ b/prisma/migrations/20240611222346_remove_slug_from_asset/migration.sql @@ -0,0 +1,35 @@ +/* + Warnings: + + - You are about to drop the column `slug` on the `Asset` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Asset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "type" TEXT NOT NULL, + "attributes" TEXT NOT NULL DEFAULT '{}', + "blob" BLOB, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ownerId" TEXT NOT NULL, + "projectId" TEXT, + "artworkId" TEXT, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Asset_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Asset" ("artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt") SELECT "artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt" FROM "Asset"; +DROP TABLE "Asset"; +ALTER TABLE "new_Asset" RENAME TO "Asset"; +CREATE INDEX "Asset_ownerId_idx" ON "Asset"("ownerId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cedd1425..7efb6fa4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -584,7 +584,6 @@ model Asset { id String @id @default(cuid()) name String description String? - slug String type String // e.g. image, media, palette, gradient, shapes, etc. attributes String @default("{}") // json string of attributes specific to the type blob Bytes? @@ -609,6 +608,4 @@ model Asset { // non-unique foreign key @@index([ownerId]) - // Unique constraint for slug scoped to ownerId - @@unique([slug, ownerId]) } From d930ca6e6209f439d76080d27e1d6ec0ac1e1fb9 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 20:23:24 -0400 Subject: [PATCH 16/54] cleanup --- app/models/asset/image/image.create.server.ts | 4 ++-- .../api.v1+/asset.image.artwork.create.tsx | 2 -- app/utils/asset/image.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/models/asset/image/image.create.server.ts b/app/models/asset/image/image.create.server.ts index c55e5f17..415bb2d8 100644 --- a/app/models/asset/image/image.create.server.ts +++ b/app/models/asset/image/image.create.server.ts @@ -4,6 +4,7 @@ import { type IUser } from '#app/models/user/user.server' import { type AssetTypeEnum } from '#app/schema/asset' import { NewAssetImageArtworkSchema } from '#app/schema/asset/image' import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { stringifyAssetImageAttributes } from '#app/utils/asset/image' import { validateEntityImageSubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' import { type IAsset } from '../asset.server' @@ -45,8 +46,7 @@ export const createAssetImageArtwork = ({ } }) => { const { attributes, ...rest } = data - const jsonAttributes = JSON.stringify(attributes) - console.log('about to create...', jsonAttributes) + const jsonAttributes = stringifyAssetImageAttributes(attributes) return prisma.asset.create({ data: { ...rest, diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx index 066db1d2..bd20bccd 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx @@ -45,14 +45,12 @@ export async function action({ request }: ActionFunctionArgs) { userId, formData, }) - console.log('validation:', status, submission) if (status === 'success') { const { success, message } = await assetImageArtworkCreateService({ userId, ...submission.value, }) - console.log('service:', success, message) createSuccess = success errorMessage = message || '' diff --git a/app/utils/asset/image.ts b/app/utils/asset/image.ts index 35c494c4..b88dc600 100644 --- a/app/utils/asset/image.ts +++ b/app/utils/asset/image.ts @@ -19,3 +19,21 @@ export const parseAssetImageAttributes = ( } } } + +export const stringifyAssetImageAttributes = ( + attributes: IAssetAttributesImage, +): string => { + try { + return JSON.stringify(AssetAttributesImageSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} From 915ce9fcb09bb7491350baca54b126e76175fd51 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 21:30:33 -0400 Subject: [PATCH 17/54] image as asset type with json attributes for flexibility, can delete --- app/models/asset/asset.get.server.ts | 42 ++++++++ app/models/asset/image/image.delete.server.ts | 33 ++++++ .../sidebars.panel.artwork-version.images.tsx | 4 +- .../api.v1+/asset.image.artwork.delete.tsx | 101 ++++++++++++++++++ .../api.v1+/asset.image.artwork.update.tsx | 99 +++++++++++++++++ app/schema/asset/image.ts | 19 +++- .../asset.image.artwork.delete.service.ts | 31 ++++++ .../validate-submission.strategy.ts | 21 ++++ app/utils/routes.const.ts | 2 + 9 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 app/models/asset/asset.get.server.ts create mode 100644 app/models/asset/image/image.delete.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork.update.tsx create mode 100644 app/services/asset.image.artwork.delete.service.ts diff --git a/app/models/asset/asset.get.server.ts b/app/models/asset/asset.get.server.ts new file mode 100644 index 00000000..4ba27b0e --- /dev/null +++ b/app/models/asset/asset.get.server.ts @@ -0,0 +1,42 @@ +import { z } from 'zod' +import { prisma } from '#app/utils/db.server' +import { type IAsset } from './asset.server' + +export type queryAssetWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryAssetWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for artwork branch.', + ) + } +} + +export const getAsset = async ({ + where, +}: { + where: queryAssetWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + return await prisma.asset.findFirst({ + where, + }) +} diff --git a/app/models/asset/image/image.delete.server.ts b/app/models/asset/image/image.delete.server.ts new file mode 100644 index 00000000..eb95f802 --- /dev/null +++ b/app/models/asset/image/image.delete.server.ts @@ -0,0 +1,33 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' + +export interface IAssetImageDeletedResponse { + success: boolean + message?: string +} + +export const validateDeleteAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + // not validateEntityImageSubmission + // there is no image file to parse and transform + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteAssetImageArtworkSchema, + strategy, + }) +} + +export const deleteAssetImage = ({ id }: { id: IAssetImage['id'] }) => { + return prisma.asset.delete({ + where: { id }, + }) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index b1bbaead..f768ce96 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -22,9 +22,9 @@ import { } from '#app/components/ui/dialog' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { ArtworkImageDelete } from '#app/routes/resources+/api.v1+/artwork.image.delete' import { ArtworkImageUpdate } from '#app/routes/resources+/api.v1+/artwork.image.update' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' +import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' import { AssetTypeEnum } from '#app/schema/asset' import { filterAssetType } from '#app/utils/asset' import { useRouteLoaderMatchData } from '#app/utils/matches' @@ -43,7 +43,7 @@ ImageUpdate.displayName = 'ImageUpdate' const ImageDelete = memo( ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { - return + return }, ) ImageDelete.displayName = 'ImageDelete' diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx new file mode 100644 index 00000000..5cd07fba --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { validateDeleteAssetImageArtworkSubmission } from '#app/models/asset/image/image.delete.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkDeleteService } from '#app/services/asset.image.artwork.delete.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK.DELETE +const schema = DeleteAssetImageArtworkSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateDeleteAssetImageArtworkSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkDeleteService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkDelete = ({ + image, + artwork, +}: { + image: IAssetImage + artwork: IArtwork +}) => { + const imageId = image.id + const artworkId = artwork.id + const iconText = `Delete Image...` + const formId = `asset-image--${imageId}-artwork-${artworkId}-delete` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx new file mode 100644 index 00000000..2d59d367 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx @@ -0,0 +1,99 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditArtworkImageSubmission } from '#app/models/images/artwork-image.update.server' +import { + EditAssetImageArtworkSchema, + MAX_UPLOAD_SIZE, +} from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { artworkImageUpdateService } from '#app/services/artwork/image/update.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK.UPDATE +const schema = EditAssetImageArtworkSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateEditArtworkImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await artworkImageUpdateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const ArtworkImageUpdate = ({ image }: { image: IAssetImage }) => { + const imageId = image.id + const formId = `asset-image-art-update-${imageId}` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ +
+
+ ) +} diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index 6d6276df..86c82855 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -26,6 +26,8 @@ const FileSchema = z 'Image must be a JPEG, PNG, WEBP, or GIF', ) +// asset image validation before saving to db + // use this to (de)serealize data to/from the db export const AssetAttributesImageSchema = z.object({ altText: AltTextSchema, @@ -49,6 +51,8 @@ export const ArtworkImageDataUpdateSchema = z.object({ altText: AltTextSchema, }) +// form data validation + export const NewAssetImageSchema = z.object({ file: FileSchema, name: AssetNameSchema, @@ -56,14 +60,15 @@ export const NewAssetImageSchema = z.object({ altText: AltTextSchema, }) -export const EditArtworkImageSchema = z.object({ +export const EditAssetImageSchema = z.object({ id: z.string(), file: FileSchema.optional(), name: AssetNameSchema, + description: AssetDescriptionSchema, altText: AltTextSchema, }) -export const DeleteArtworkImageSchema = z.object({ +export const DeleteAssetImageSchema = z.object({ id: z.string(), }) @@ -73,8 +78,14 @@ const ArtworkParentSchema = z.object({ artworkId: z.string(), }) +export const AssetImageArtworkCreateDataSchema = + AssetImageCreateDataSchema.merge(ArtworkParentSchema) + export const NewAssetImageArtworkSchema = NewAssetImageSchema.merge(ArtworkParentSchema) -export const AssetImageArtworkCreateDataSchema = - AssetImageCreateDataSchema.merge(ArtworkParentSchema) +export const EditAssetImageArtworkSchema = + EditAssetImageSchema.merge(ArtworkParentSchema) + +export const DeleteAssetImageArtworkSchema = + DeleteAssetImageSchema.merge(ArtworkParentSchema) diff --git a/app/services/asset.image.artwork.delete.service.ts b/app/services/asset.image.artwork.delete.service.ts new file mode 100644 index 00000000..865098bd --- /dev/null +++ b/app/services/asset.image.artwork.delete.service.ts @@ -0,0 +1,31 @@ +import { + deleteAssetImage, + type IAssetImageDeletedResponse, +} from '#app/models/asset/image/image.delete.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type IUser } from '#app/models/user/user.server' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkDeleteService = async ({ + userId, + id, +}: { + userId: IUser['id'] + id: IAssetImage['id'] +}): Promise => { + try { + // Step 1: delete the asset image via promise + const deleteArtworkImagePromise = deleteAssetImage({ id }) + + // Step 2: execute the transaction + await prisma.$transaction([deleteArtworkImagePromise]) + + return { success: true } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkDeleteService', + } + } +} diff --git a/app/strategies/validate-submission.strategy.ts b/app/strategies/validate-submission.strategy.ts index 5b34e8d4..2b4c5306 100644 --- a/app/strategies/validate-submission.strategy.ts +++ b/app/strategies/validate-submission.strategy.ts @@ -3,6 +3,7 @@ import { type z } from 'zod' import { getArtwork } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' import { getArtworkVersion } from '#app/models/artwork-version/artwork-version.get.server' +import { getAsset } from '#app/models/asset/asset.get.server' import { getDesign } from '#app/models/design/design.get.server' import { getArtworkImage } from '#app/models/images/artwork-image.get.server' import { getLayer } from '#app/models/layer/layer.get.server' @@ -184,3 +185,23 @@ export class ValidateArtworkImageSubmissionStrategy } } } + +export class ValidateAssetSubmissionStrategy + implements IValidateSubmissionStrategy +{ + async validateFormDataEntity({ + userId, + data, + ctx, + }: { + userId: User['id'] + data: any + ctx: any + }): Promise { + const { id } = data + const asset = await getAsset({ + where: { id, ownerId: userId }, + }) + if (!asset) ctx.addIssue(addNotFoundIssue('Asset')) + } +} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 0b8f1c82..12792667 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -47,6 +47,8 @@ export const Routes = { IMAGE: { ARTWORK: { CREATE: `${pathBase}/asset/image/artwork/create`, + DELETE: `${pathBase}/asset/image/artwork/delete`, + UPDATE: `${pathBase}/asset/image/artwork/update`, }, }, }, From bab40c1b7c4995181ee62982838be8d469bcde0e Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Tue, 11 Jun 2024 23:43:41 -0400 Subject: [PATCH 18/54] image as asset type with json attributes for flexibility, can update --- app/models/asset/asset.get.server.ts | 2 +- app/models/asset/image/image.get.server.ts | 51 +++++++++- app/models/asset/image/image.update.server.ts | 55 +++++++++++ .../sidebars.panel.artwork-version.images.tsx | 12 ++- .../api.v1+/asset.image.artwork.update.tsx | 21 ++-- .../resources+/artwork-images.$imageId.tsx | 4 +- app/schema/asset/image.ts | 13 +-- .../asset.image.artwork.create.service.ts | 10 +- .../asset.image.artwork.update.service.ts | 99 +++++++++++++++++++ app/utils/asset.ts | 58 +++-------- .../conform/transform-asset-image.server.ts | 4 +- 11 files changed, 250 insertions(+), 79 deletions(-) create mode 100644 app/models/asset/image/image.update.server.ts create mode 100644 app/services/asset.image.artwork.update.service.ts diff --git a/app/models/asset/asset.get.server.ts b/app/models/asset/asset.get.server.ts index 4ba27b0e..9c7b5a29 100644 --- a/app/models/asset/asset.get.server.ts +++ b/app/models/asset/asset.get.server.ts @@ -25,7 +25,7 @@ const validateQueryWhereArgsPresent = (where: queryAssetWhereArgsType) => { if (Object.keys(missingValues).length > 0) { console.log('Missing values:', missingValues) throw new Error( - 'Null or undefined values are not allowed in query parameters for artwork branch.', + 'Null or undefined values are not allowed in query parameters for asset.', ) } } diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 82991dd7..972258dc 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -1,10 +1,59 @@ import { invariant } from '@epic-web/invariant' +import { z } from 'zod' import { AssetTypeEnum } from '#app/schema/asset' +import { deserializeAsset } from '#app/utils/asset' import { parseAssetImageAttributes } from '#app/utils/asset/image' import { prisma } from '#app/utils/db.server' import { type IAssetImageSrc, type IAssetImage } from './image.server' -export const getAssetImageArtwork = async ({ +export type queryAssetImageWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryAssetImageWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for asset image.', + ) + } +} + +export const getAssetImage = async ({ + where, +}: { + where: queryAssetImageWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const asset = await prisma.asset.findFirst({ + where: { + ...where, + type: AssetTypeEnum.IMAGE, + }, + }) + invariant(asset, 'Asset Image Not found') + return deserializeAsset({ asset }) as IAssetImage +} + +// just return the minimum required data +// for loading the image from the route url +export const getAssetImageArtworkSrc = async ({ id, }: { id: IAssetImage['id'] diff --git a/app/models/asset/image/image.update.server.ts b/app/models/asset/image/image.update.server.ts new file mode 100644 index 00000000..ad048811 --- /dev/null +++ b/app/models/asset/image/image.update.server.ts @@ -0,0 +1,55 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { EditAssetImageArtworkSchema } from '#app/schema/asset/image' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { stringifyAssetImageAttributes } from '#app/utils/asset/image' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAsset } from '../asset.server' +import { type IAssetImage } from './image.server' + +export interface IAssetImageUpdatedResponse { + success: boolean + message?: string + updatedAssetImage?: IAsset +} + +export const validateEditAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: EditAssetImageArtworkSchema, + strategy, + }) +} + +export const updateAssetImageArtwork = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: { + artworkId: IArtwork['id'] + name: string + description?: string + attributes: { + contentType: string + altText?: string + } + } +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index f768ce96..9aa8942a 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -22,9 +22,9 @@ import { } from '#app/components/ui/dialog' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { ArtworkImageUpdate } from '#app/routes/resources+/api.v1+/artwork.image.update' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' +import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' import { AssetTypeEnum } from '#app/schema/asset' import { filterAssetType } from '#app/utils/asset' import { useRouteLoaderMatchData } from '#app/utils/matches' @@ -36,9 +36,11 @@ const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { }) ImageCreate.displayName = 'ImageCreate' -const ImageUpdate = memo(({ image }: { image: IAssetImage }) => { - return -}) +const ImageUpdate = memo( + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { + return + }, +) ImageUpdate.displayName = 'ImageUpdate' const ImageDelete = memo( @@ -72,7 +74,7 @@ const ImageListItem = memo( - +
diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx index 2d59d367..7e22e428 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx @@ -9,14 +9,15 @@ import { useFetcher } from '@remix-run/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { validateEditArtworkImageSubmission } from '#app/models/images/artwork-image.update.server' +import { validateEditAssetImageArtworkSubmission } from '#app/models/asset/image/image.update.server' import { EditAssetImageArtworkSchema, MAX_UPLOAD_SIZE, } from '#app/schema/asset/image' import { validateNoJS } from '#app/schema/form-data' -import { artworkImageUpdateService } from '#app/services/artwork/image/update.service' +import { assetImageArtworkUpdateService } from '#app/services/asset.image.artwork.update.service' import { requireUserId } from '#app/utils/auth.server' import { Routes } from '#app/utils/routes.const' @@ -41,13 +42,13 @@ export async function action({ request }: ActionFunctionArgs) { let createSuccess = false let errorMessage = '' - const { status, submission } = await validateEditArtworkImageSubmission({ + const { status, submission } = await validateEditAssetImageArtworkSubmission({ userId, formData, }) if (status === 'success') { - const { success, message } = await artworkImageUpdateService({ + const { success, message } = await assetImageArtworkUpdateService({ userId, ...submission.value, }) @@ -70,9 +71,16 @@ export async function action({ request }: ActionFunctionArgs) { ) } -export const ArtworkImageUpdate = ({ image }: { image: IAssetImage }) => { +export const AssetImageArtworkUpdate = ({ + image, + artwork, +}: { + image: IAssetImage + artwork: IArtwork +}) => { const imageId = image.id - const formId = `asset-image-art-update-${imageId}` + const artworkId = artwork.id + const formId = `asset-image-${imageId}-artwork-${artworkId}-update` const fetcher = useFetcher() let isHydrated = useHydrated() @@ -93,6 +101,7 @@ export const ArtworkImageUpdate = ({ image }: { image: IAssetImage }) => { >
+
) diff --git a/app/routes/resources+/artwork-images.$imageId.tsx b/app/routes/resources+/artwork-images.$imageId.tsx index d7f150f3..249b79a4 100644 --- a/app/routes/resources+/artwork-images.$imageId.tsx +++ b/app/routes/resources+/artwork-images.$imageId.tsx @@ -1,10 +1,10 @@ import { invariantResponse } from '@epic-web/invariant' import { type LoaderFunctionArgs } from '@remix-run/node' -import { getAssetImageArtwork } from '#app/models/asset/image/image.get.server' +import { getAssetImageArtworkSrc } from '#app/models/asset/image/image.get.server' export async function loader({ params }: LoaderFunctionArgs) { invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) - const image = await getAssetImageArtwork({ id: params.imageId }) + const image = await getAssetImageArtworkSrc({ id: params.imageId }) invariantResponse(image, 'Not found', { status: 404 }) diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index 86c82855..b24bbc7a 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -36,7 +36,7 @@ export const AssetAttributesImageSchema = z.object({ // zod schema for blob Buffer/File is not working // pass in separately from validation -export const AssetImageCreateDataSchema = z.object({ +export const AssetImageDataSchema = z.object({ name: AssetNameSchema, description: AssetDescriptionSchema, type: z.literal('image'), @@ -44,13 +44,6 @@ export const AssetImageCreateDataSchema = z.object({ ownerId: z.string(), }) -export const ArtworkImageDataUpdateSchema = z.object({ - id: z.string(), - contentType: z.string().optional(), - name: AssetNameSchema, - altText: AltTextSchema, -}) - // form data validation export const NewAssetImageSchema = z.object({ @@ -78,8 +71,8 @@ const ArtworkParentSchema = z.object({ artworkId: z.string(), }) -export const AssetImageArtworkCreateDataSchema = - AssetImageCreateDataSchema.merge(ArtworkParentSchema) +export const AssetImageArtworkDataSchema = + AssetImageDataSchema.merge(ArtworkParentSchema) export const NewAssetImageArtworkSchema = NewAssetImageSchema.merge(ArtworkParentSchema) diff --git a/app/services/asset.image.artwork.create.service.ts b/app/services/asset.image.artwork.create.service.ts index 8eca2d11..e01e924e 100644 --- a/app/services/asset.image.artwork.create.service.ts +++ b/app/services/asset.image.artwork.create.service.ts @@ -7,7 +7,7 @@ import { } from '#app/models/asset/image/image.create.server' import { type IUser } from '#app/models/user/user.server' import { AssetTypeEnum } from '#app/schema/asset' -import { AssetImageArtworkCreateDataSchema } from '#app/schema/asset/image' +import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' import { prisma } from '#app/utils/db.server' export const assetImageArtworkCreateService = async ({ @@ -15,17 +15,17 @@ export const assetImageArtworkCreateService = async ({ artworkId, name, description, - blob, contentType, altText, + blob, }: { userId: IUser['id'] artworkId: IArtwork['id'] name: string description?: string - blob: Buffer contentType: string altText: string | null + blob: Buffer }): Promise => { try { // Step 1: verify the artwork exists @@ -48,9 +48,7 @@ export const assetImageArtworkCreateService = async ({ ownerId: userId, artworkId, } - console.log('data:', data) - const assetImageData = AssetImageArtworkCreateDataSchema.parse(data) - console.log('assetImageData:', assetImageData) + const assetImageData = AssetImageArtworkDataSchema.parse(data) // Step 3: create the asset image via promise const createAssetImagePromise = createAssetImageArtwork({ diff --git a/app/services/asset.image.artwork.update.service.ts b/app/services/asset.image.artwork.update.service.ts new file mode 100644 index 00000000..39e06d26 --- /dev/null +++ b/app/services/asset.image.artwork.update.service.ts @@ -0,0 +1,99 @@ +import { invariant } from '@epic-web/invariant' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { createAssetImageArtwork } from '#app/models/asset/image/image.create.server' +import { deleteAssetImage } from '#app/models/asset/image/image.delete.server' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { + type IAssetImageUpdatedResponse, + updateAssetImageArtwork, +} from '#app/models/asset/image/image.update.server' +import { type IUser } from '#app/models/user/user.server' +import { AssetTypeEnum } from '#app/schema/asset' +import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkUpdateService = async ({ + userId, + id, + artworkId, + name, + description, + blob, + contentType, + altText, +}: { + userId: IUser['id'] + id: IAssetImage['id'] + artworkId: IArtwork['id'] + name: string + description?: string + blob: Buffer + contentType: string + altText: string | null +}): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, artworkId, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + + // Step 2: validate asset image data + const data = { + name, + description, + type: AssetTypeEnum.IMAGE, + attributes: { + contentType: contentType ?? assetImage.attributes.contentType, + altText: altText || 'No alt text provided.', + }, + ownerId: assetImage.ownerId, + artworkId: assetImage.artworkId, + } + const assetImageData = AssetImageArtworkDataSchema.parse(data) + + if (blob) { + // Step 3: delete the asset image with old blob via promise + const deleteArtworkImagePromise = deleteAssetImage({ id }) + + // Step 4: create the asset image with new blob via promise + const createAssetImagePromise = createAssetImageArtwork({ + data: { ...assetImageData, blob }, + }) + + // Step 5: execute the transaction + const [, updatedAssetImage] = await prisma.$transaction([ + deleteArtworkImagePromise, + createAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } else { + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageArtwork({ + id, + data: { ...assetImageData }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkCreateService', + } + } +} diff --git a/app/utils/asset.ts b/app/utils/asset.ts index 5006358d..605ae28a 100644 --- a/app/utils/asset.ts +++ b/app/utils/asset.ts @@ -2,49 +2,17 @@ import { ZodError } from 'zod' import { type IAssetParsed, type IAsset, - type IAssetAttributes, type IAssetType, } from '#app/models/asset/asset.server' -import { type assetTypeEnum } from '#app/schema/asset' -import { AssetAttributesImageSchema } from '#app/schema/asset/image' - -// Function to parse attributes from JSON string -export const parseAttributes = (asset: IAsset): IAssetParsed => { - try { - return { - ...asset, - type: asset.type as assetTypeEnum, - attributes: JSON.parse(asset.attributes) as IAssetAttributes, - } - } catch (error: any) { - throw new Error( - `Failed to parse attributes for asset with ID ${asset.id}: ${error.message}`, - ) - } -} - -// Function to stringify attributes to JSON string -export const stringifyAttributes = (asset: IAssetParsed): IAsset => { - try { - return { - ...asset, - attributes: JSON.stringify(asset.attributes), - } - } catch (error: any) { - throw new Error( - `Failed to stringify attributes for asset with ID ${asset.id}: ${error.message}`, - ) - } -} +import { AssetTypeEnum, type assetTypeEnum } from '#app/schema/asset' +import { parseAssetImageAttributes } from './asset/image' export const deserializeAssets = ({ assets, }: { assets: IAsset[] }): IAssetParsed[] => { - return assets.map(asset => { - return deserializeAsset({ asset }) - }) + return assets.map(asset => deserializeAsset({ asset })) } export const deserializeAsset = ({ @@ -52,32 +20,32 @@ export const deserializeAsset = ({ }: { asset: IAsset }): IAssetParsed => { - const parsedAsset = parseAttributes(asset) - const { type, attributes } = parsedAsset + const type = asset.type as assetTypeEnum + const { attributes } = asset - const validatedAssets = validateAttributes({ - attributes, + const validatedAssetAttributes = validateAssetAttributes({ type, + attributes, }) return { ...asset, type, - attributes: validatedAssets, + attributes: validatedAssetAttributes, } } -export const validateAttributes = ({ - attributes, +export const validateAssetAttributes = ({ type, + attributes, }: { - attributes: IAssetAttributes type: assetTypeEnum + attributes: IAsset['attributes'] }) => { try { switch (type) { - case 'image': - return AssetAttributesImageSchema.parse(attributes) + case AssetTypeEnum.IMAGE: + return parseAssetImageAttributes(attributes) default: throw new Error(`Unsupported asset type: ${type}`) } diff --git a/app/utils/conform/transform-asset-image.server.ts b/app/utils/conform/transform-asset-image.server.ts index c9154021..ffe89502 100644 --- a/app/utils/conform/transform-asset-image.server.ts +++ b/app/utils/conform/transform-asset-image.server.ts @@ -42,9 +42,7 @@ function getImageUpdateData( fileData?: { contentType: string; blob: Buffer }, ) { return { - id: data.id, - name: data.name, - altText: data.altText, + ...data, ...(fileData && fileData), } } From 7a6cadcabf0d62c069c974509f324bb55cc93a19 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 12:33:03 -0400 Subject: [PATCH 19/54] refactoring interfaces to offer better flexibility in extending attributes json; testing with image dimensions and size; added ExifReader to read image data from server --- app/models/asset/asset.server.ts | 23 + app/models/asset/image/image.create.server.ts | 39 +- app/models/asset/image/image.server.ts | 21 +- app/models/asset/image/image.update.server.ts | 36 +- .../sidebars.panel.artwork-version.images.tsx | 24 +- app/schema/asset/image.ts | 10 + .../asset.image.artwork.create.service.ts | 29 +- .../asset.image.artwork.update.service.ts | 30 +- app/utils/asset/image.ts | 4 + .../conform/transform-asset-image.server.ts | 31 +- package-lock.json | 889 ++++++++++-------- package.json | 1 + 12 files changed, 649 insertions(+), 488 deletions(-) diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index 83965e83..39e48af1 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -1,6 +1,7 @@ import { type Asset } from '@prisma/client' import { type DateOrString } from '#app/definitions/prisma-helper' import { type assetTypeEnum } from '#app/schema/asset' +import { type IUser } from '../user/user.server' import { type IAssetImage, type IAssetAttributesImage, @@ -19,6 +20,9 @@ export interface IAsset extends BaseAsset { updatedAt: DateOrString } +// when adding attributes to an asset type, +// make sure it starts as optional or is set to a default value +// for when parsing the asset from the deserializer export type IAssetAttributes = IAssetAttributesImage export interface IAssetParsed extends BaseAsset { @@ -29,3 +33,22 @@ export interface IAssetParsed extends BaseAsset { } export type IAssetType = IAssetImage + +interface IAssetData { + name: string + description?: string +} + +export interface IAssetSubmission extends IAssetData { + userId: IUser['id'] +} + +export interface IAssetCreateData extends IAssetData { + ownerId: IUser['id'] + type: assetTypeEnum + attributes: IAssetAttributes +} + +export interface IAssetUpdateData extends IAssetData { + attributes: IAssetAttributes +} diff --git a/app/models/asset/image/image.create.server.ts b/app/models/asset/image/image.create.server.ts index 415bb2d8..d6863139 100644 --- a/app/models/asset/image/image.create.server.ts +++ b/app/models/asset/image/image.create.server.ts @@ -1,13 +1,16 @@ import { type IntentActionArgs } from '#app/definitions/intent-action-args' import { type IArtwork } from '#app/models/artwork/artwork.server' -import { type IUser } from '#app/models/user/user.server' import { type AssetTypeEnum } from '#app/schema/asset' import { NewAssetImageArtworkSchema } from '#app/schema/asset/image' import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' import { stringifyAssetImageAttributes } from '#app/utils/asset/image' import { validateEntityImageSubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' -import { type IAsset } from '../asset.server' +import { type IAssetCreateData, type IAsset } from '../asset.server' +import { + type IAssetImageSubmission, + type IAssetAttributesImage, +} from './image.server' export interface IAssetImageCreatedResponse { success: boolean @@ -29,21 +32,29 @@ export const validateNewAssetImageArtworkSubmission = async ({ }) } +export interface IAssetImageCreateSubmission extends IAssetImageSubmission { + blob: Buffer +} + +export interface IAssetImageArtworkCreateSubmission + extends IAssetImageCreateSubmission { + artworkId: IArtwork['id'] +} + +interface IAssetImageCreateData extends IAssetCreateData { + type: typeof AssetTypeEnum.IMAGE + attributes: IAssetAttributesImage + blob: Buffer +} + +interface IAssetImageArtworkCreateData extends IAssetImageCreateData { + artworkId: IArtwork['id'] +} + export const createAssetImageArtwork = ({ data, }: { - data: { - ownerId: IUser['id'] - artworkId: IArtwork['id'] - name: string - description?: string - type: typeof AssetTypeEnum.IMAGE - attributes: { - contentType: string - altText?: string - } - blob: Buffer - } + data: IAssetImageArtworkCreateData }) => { const { attributes, ...rest } = data const jsonAttributes = stringifyAssetImageAttributes(attributes) diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts index 0c879d0b..4f74ccce 100644 --- a/app/models/asset/image/image.server.ts +++ b/app/models/asset/image/image.server.ts @@ -1,17 +1,32 @@ import { type AssetTypeEnum } from '#app/schema/asset' -import { type IAssetParsed } from '../asset.server' +import { type IAssetSubmission, type IAssetParsed } from '../asset.server' export interface IAssetImage extends IAssetParsed { type: typeof AssetTypeEnum.IMAGE attributes: IAssetAttributesImage } -export interface IAssetAttributesImage { - altText?: string +export interface IAssetImageFileData { contentType: string + height: number + width: number + size: number + lastModified?: number + filename: string +} + +// when adding attributes to an asset type, +// make sure it starts as optional or is set to a default value +// for when parsing the asset from the deserializer +export interface IAssetAttributesImage extends IAssetImageFileData { + altText?: string } export interface IAssetImageSrc { contentType: string blob: Buffer } + +export interface IAssetImageSubmission + extends IAssetSubmission, + IAssetAttributesImage {} diff --git a/app/models/asset/image/image.update.server.ts b/app/models/asset/image/image.update.server.ts index ad048811..3956a211 100644 --- a/app/models/asset/image/image.update.server.ts +++ b/app/models/asset/image/image.update.server.ts @@ -5,8 +5,12 @@ import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submis import { stringifyAssetImageAttributes } from '#app/utils/asset/image' import { validateEntityImageSubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' -import { type IAsset } from '../asset.server' -import { type IAssetImage } from './image.server' +import { type IAssetUpdateData, type IAsset } from '../asset.server' +import { + type IAssetImageSubmission, + type IAssetAttributesImage, + type IAssetImage, +} from './image.server' export interface IAssetImageUpdatedResponse { success: boolean @@ -28,20 +32,30 @@ export const validateEditAssetImageArtworkSubmission = async ({ }) } +export interface IAssetImageUpdateSubmission extends IAssetImageSubmission { + id: IAssetImage['id'] + blob?: Buffer +} + +export interface IAssetImageArtworkUpdateSubmission + extends IAssetImageUpdateSubmission { + artworkId: IArtwork['id'] +} + +interface IAssetImageUpdateData extends IAssetUpdateData { + attributes: IAssetAttributesImage +} + +interface IAssetImageArtworkUpdateData extends IAssetImageUpdateData { + artworkId: IArtwork['id'] +} + export const updateAssetImageArtwork = ({ id, data, }: { id: IAssetImage['id'] - data: { - artworkId: IArtwork['id'] - name: string - description?: string - attributes: { - contentType: string - altText?: string - } - } + data: IAssetImageArtworkUpdateData }) => { const { attributes, ...rest } = data const jsonAttributes = stringifyAssetImageAttributes(attributes) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 9aa8942a..4c0dbc6f 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -2,6 +2,7 @@ import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' import { ImageFull, ImagePreview } from '#app/components/image' import { + FlexColumn, FlexRow, ImageSidebar, ImageSidebarList, @@ -27,6 +28,7 @@ import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.im import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' import { AssetTypeEnum } from '#app/schema/asset' import { filterAssetType } from '#app/utils/asset' +import { sizeInMB } from '#app/utils/asset/image' import { useRouteLoaderMatchData } from '#app/utils/matches' import { getArtworkImgSrc } from '#app/utils/misc' import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' @@ -53,14 +55,14 @@ ImageDelete.displayName = 'ImageDelete' const ImageListItem = memo( ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { const { id, name, attributes } = image - const { altText } = attributes + const { altText, height, width, size } = attributes return (
{name}
- + @@ -73,9 +75,21 @@ const ImageListItem = memo( - - - + + + + + + + + + {width}x{height} + + +
{sizeInMB(size)} MB
+
+
+
diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index b24bbc7a..bfc0cbe7 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -4,6 +4,8 @@ import { AssetDescriptionSchema, AssetNameSchema } from './__shared' const MAX_ALT_TEXT_LENGTH = 240 const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH).optional() +const DimensionsSchema = z.number().int().positive() + const MAX_MEGABYTES = 6 export const MAX_UPLOAD_SIZE = 1024 * 1024 * MAX_MEGABYTES const ACCEPTED_IMAGE_TYPES = [ @@ -29,9 +31,17 @@ const FileSchema = z // asset image validation before saving to db // use this to (de)serealize data to/from the db +// when adding attributes to an asset type, +// make sure it starts as optional or is set to a default value +// for when parsing the asset from the deserializer export const AssetAttributesImageSchema = z.object({ altText: AltTextSchema, contentType: z.string(), + height: DimensionsSchema, + width: DimensionsSchema, + size: z.number(), + lastModified: z.number().optional(), + filename: z.string(), }) // zod schema for blob Buffer/File is not working diff --git a/app/services/asset.image.artwork.create.service.ts b/app/services/asset.image.artwork.create.service.ts index e01e924e..c110919f 100644 --- a/app/services/asset.image.artwork.create.service.ts +++ b/app/services/asset.image.artwork.create.service.ts @@ -1,11 +1,10 @@ import { invariant } from '@epic-web/invariant' import { getArtwork } from '#app/models/artwork/artwork.get.server' -import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IAssetImageCreatedResponse, createAssetImageArtwork, + type IAssetImageArtworkCreateSubmission, } from '#app/models/asset/image/image.create.server' -import { type IUser } from '#app/models/user/user.server' import { AssetTypeEnum } from '#app/schema/asset' import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' import { prisma } from '#app/utils/db.server' @@ -15,18 +14,15 @@ export const assetImageArtworkCreateService = async ({ artworkId, name, description, - contentType, - altText, blob, -}: { - userId: IUser['id'] - artworkId: IArtwork['id'] - name: string - description?: string - contentType: string - altText: string | null - blob: Buffer -}): Promise => { + altText, + contentType, + height, + width, + size, + lastModified, + filename, +}: IAssetImageArtworkCreateSubmission): Promise => { try { // Step 1: verify the artwork exists const artwork = await getArtwork({ @@ -42,8 +38,13 @@ export const assetImageArtworkCreateService = async ({ description, type: AssetTypeEnum.IMAGE, attributes: { - contentType, altText: altText || 'No alt text provided.', + contentType, + height, + width, + size, + lastModified, + filename, }, ownerId: userId, artworkId, diff --git a/app/services/asset.image.artwork.update.service.ts b/app/services/asset.image.artwork.update.service.ts index 39e06d26..abc1192e 100644 --- a/app/services/asset.image.artwork.update.service.ts +++ b/app/services/asset.image.artwork.update.service.ts @@ -1,14 +1,12 @@ import { invariant } from '@epic-web/invariant' -import { type IArtwork } from '#app/models/artwork/artwork.server' import { createAssetImageArtwork } from '#app/models/asset/image/image.create.server' import { deleteAssetImage } from '#app/models/asset/image/image.delete.server' import { getAssetImage } from '#app/models/asset/image/image.get.server' -import { type IAssetImage } from '#app/models/asset/image/image.server' import { type IAssetImageUpdatedResponse, updateAssetImageArtwork, + type IAssetImageArtworkUpdateSubmission, } from '#app/models/asset/image/image.update.server' -import { type IUser } from '#app/models/user/user.server' import { AssetTypeEnum } from '#app/schema/asset' import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' import { prisma } from '#app/utils/db.server' @@ -20,24 +18,21 @@ export const assetImageArtworkUpdateService = async ({ name, description, blob, - contentType, altText, -}: { - userId: IUser['id'] - id: IAssetImage['id'] - artworkId: IArtwork['id'] - name: string - description?: string - blob: Buffer - contentType: string - altText: string | null -}): Promise => { + contentType, + height, + width, + size, + lastModified, + filename, +}: IAssetImageArtworkUpdateSubmission): Promise => { try { // Step 1: verify the asset image exists const assetImage = await getAssetImage({ where: { id, artworkId, ownerId: userId }, }) invariant(assetImage, 'Asset Image not found') + const { attributes: assetImageAttributes } = assetImage // Step 2: validate asset image data const data = { @@ -45,8 +40,13 @@ export const assetImageArtworkUpdateService = async ({ description, type: AssetTypeEnum.IMAGE, attributes: { - contentType: contentType ?? assetImage.attributes.contentType, altText: altText || 'No alt text provided.', + contentType: contentType ?? assetImageAttributes.contentType, + height: height ?? assetImageAttributes.height, + width: width ?? assetImageAttributes.width, + size: size ?? assetImageAttributes.size, + lastModified: lastModified ?? assetImageAttributes.lastModified, + filename: filename ?? assetImageAttributes.filename, }, ownerId: assetImage.ownerId, artworkId: assetImage.artworkId, diff --git a/app/utils/asset/image.ts b/app/utils/asset/image.ts index b88dc600..596b1471 100644 --- a/app/utils/asset/image.ts +++ b/app/utils/asset/image.ts @@ -37,3 +37,7 @@ export const stringifyAssetImageAttributes = ( } } } + +export const sizeInMB = (sizeInBytes: number) => { + return (sizeInBytes / 1024 / 1024).toFixed(2) +} diff --git a/app/utils/conform/transform-asset-image.server.ts b/app/utils/conform/transform-asset-image.server.ts index ffe89502..8826d4b5 100644 --- a/app/utils/conform/transform-asset-image.server.ts +++ b/app/utils/conform/transform-asset-image.server.ts @@ -1,4 +1,6 @@ +import ExifReader from 'exifreader' import { z } from 'zod' +import { type IAssetImageFileData } from '#app/models/asset/image/image.server' type FormDataWithId = { id?: string @@ -7,6 +9,10 @@ type FormDataWithId = { altText?: string } +interface FileData extends IAssetImageFileData { + blob: Buffer +} + export async function transformAssetImageData(data: FormDataWithId) { return data.id ? transformAssetImageDataUpdate(data) @@ -30,26 +36,35 @@ async function transformAssetImageDataCreate(data: FormDataWithId) { const fileData = await transformFileData(data.file) return { ...data, - ...fileData, + ...(fileData || {}), } } else { return z.NEVER } } -function getImageUpdateData( - data: FormDataWithId, - fileData?: { contentType: string; blob: Buffer }, -) { +function getImageUpdateData(data: FormDataWithId, fileData?: FileData) { return { ...data, ...(fileData && fileData), } } -async function transformFileData(file: File) { +async function transformFileData(file: File): Promise { + const arrayBuffer = await file.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + const metadata = await ExifReader.load(buffer) + const height = metadata?.['Image Height']?.value ?? 0 + const width = metadata?.['Image Width']?.value ?? 0 + return { - contentType: file.type as string, - blob: Buffer.from(await file.arrayBuffer()), + contentType: file.type, + blob: buffer, + height, + width, + size: file.size, + lastModified: file.lastModified, + filename: file.name, } } diff --git a/package-lock.json b/package-lock.json index dc048623..d70113ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "dotenv": "^16.3.1", "eslint-plugin-remix-react-routes": "^1.0.5", "execa": "^8.0.1", + "exifreader": "^4.23.2", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "get-port": "^7.0.0", @@ -1659,6 +1660,7 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", @@ -1708,6 +1710,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@isaacs/cliui": { @@ -4685,40 +4688,40 @@ } }, "node_modules/@sentry-internal/feedback": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.116.0.tgz", - "integrity": "sha512-tmfO+RTCrhIWMs3yg8X0axhbjWRZLsldSfoXBgfjNCk/XwkYiVGp7WnYVbb+IO+01mHCsis9uaYOBggLgFRB5Q==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.117.0.tgz", + "integrity": "sha512-4X+NnnY17W74TymgLFH7/KPTVYpEtoMMJh8HzVdCmHTOE6j32XKBeBMRaXBhmNYmEgovgyRKKf2KvtSfgw+V1Q==", "dependencies": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.116.0.tgz", - "integrity": "sha512-Sy0ydY7A97JY/IFTIj8U25kHqR5rL9oBk3HFE5EK9Phw56irVhHzEwLWae0jlFeCQEWoBYqpPgO5vXsaYzrWvw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.117.0.tgz", + "integrity": "sha512-7hjIhwEcoosr+BIa0AyEssB5xwvvlzUpvD5fXu4scd3I3qfX8gdnofO96a8r+LrQm3bSj+eN+4TfKEtWb7bU5A==", "dependencies": { - "@sentry/core": "7.116.0", - "@sentry/replay": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/replay": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry-internal/tracing": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.116.0.tgz", - "integrity": "sha512-y5ppEmoOlfr77c/HqsEXR72092qmGYS4QE5gSz5UZFn9CiinEwGfEorcg2xIrrCuU7Ry/ZU2VLz9q3xd04drRA==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.117.0.tgz", + "integrity": "sha512-fAIyijNvKBZNA12IcKo+dOYDRTNrzNsdzbm3DP37vJRKVQu19ucqP4Y6InvKokffDP2HZPzFPDoGXYuXkDhUZg==", "dependencies": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=8" @@ -4734,18 +4737,18 @@ } }, "node_modules/@sentry/browser": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.116.0.tgz", - "integrity": "sha512-2aosATT5qE+QLKgTmyF9t5Emsluy1MBczYNuPmLhDxGNfB+MA86S8u7Hb0CpxdwjS0nt14gmbiOtJHoeAF3uTw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.117.0.tgz", + "integrity": "sha512-29X9HlvDEKIaWp6XKlNPPSNND0U6P/ede5WA2nVHfs1zJLWdZ7/ijuMc0sH/CueEkqHe/7gt94hBcI7HOU/wSw==", "dependencies": { - "@sentry-internal/feedback": "7.116.0", - "@sentry-internal/replay-canvas": "7.116.0", - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/integrations": "7.116.0", - "@sentry/replay": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/feedback": "7.117.0", + "@sentry-internal/replay-canvas": "7.117.0", + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/integrations": "7.117.0", + "@sentry/replay": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=8" @@ -4963,25 +4966,25 @@ } }, "node_modules/@sentry/core": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.116.0.tgz", - "integrity": "sha512-J6Wmjjx+o7RwST0weTU1KaKUAlzbc8MGkJV1rcHM9xjNTWTva+nrcCM3vFBagnk2Gm/zhwv3h0PvWEqVyp3U1Q==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.117.0.tgz", + "integrity": "sha512-1XZ4/d/DEwnfM2zBMloXDwX+W7s76lGKQMgd8bwgPJZjjEztMJ7X0uopKAGwlQcjn242q+hsCBR6C+fSuI5kvg==", "dependencies": { - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/integrations": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.116.0.tgz", - "integrity": "sha512-UZb60gaF+7veh1Yv79RiGvgGYOnU6xA97H+hI6tKgc1uT20YpItO4X56Vhp0lvyEyUGFZzBRRH1jpMDPNGPkqw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.117.0.tgz", + "integrity": "sha512-U3suSZysmU9EiQqg0ga5CxveAyNbi9IVdsapMDq5EQGNcVDvheXtULs+BOc11WYP3Kw2yWB38VDqLepfc/Fg2g==", "dependencies": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "localforage": "^1.8.1" }, "engines": { @@ -4989,15 +4992,15 @@ } }, "node_modules/@sentry/node": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.116.0.tgz", - "integrity": "sha512-HB/4TrJWbnu6swNzkid+MlwzLwY/D/klGt3R0aatgrgWPo2jJm6bSl4LUT39Cr2eg5I1gsREQtXE2mAlC6gm8w==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.117.0.tgz", + "integrity": "sha512-0MWXdT8dv1MtQGF0aeB8LQTBTJS1L1Vz24+wvdXroR3/52mPYrPWlzuc7+Ew/Dlqdlb5LKVIlkuDSRWj8UKpTQ==", "dependencies": { - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/integrations": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/integrations": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=8" @@ -5023,14 +5026,14 @@ } }, "node_modules/@sentry/react": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.116.0.tgz", - "integrity": "sha512-b7sYSIewK/h3dGzm7Rx6tBUzA6w7zw6m5rVIO3fWCy7T3xEUDggUaqklrFVHXUYx2yjzEgTFPg/Dd2NrSzua4w==", - "dependencies": { - "@sentry/browser": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.117.0.tgz", + "integrity": "sha512-aK+yaEP2esBhaczGU96Y7wkqB4umSIlRAzobv7ER88EGHzZulRaocTpQO8HJJGDHm4D8rD+E893BHnghkoqp4Q==", + "dependencies": { + "@sentry/browser": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -5041,17 +5044,17 @@ } }, "node_modules/@sentry/remix": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.116.0.tgz", - "integrity": "sha512-edOiZDn1lFoqsGKfFCLPh8ZqSyWaIvyjmJWYePVHXn6lQJsKRe4d1TewNAm0ZCTQhshZkJkAkG73/KQjQ0vvHw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.117.0.tgz", + "integrity": "sha512-u6VHt4lsCeCpiABBEVpM1PEPmgtGGbmLotKJOC8amcX7UOxwmbYe/mRfOkffbh9w959E+SH2f393NtCia8wrRg==", "dependencies": { "@remix-run/router": "1.x", "@sentry/cli": "^2.28.0", - "@sentry/core": "7.116.0", - "@sentry/node": "7.116.0", - "@sentry/react": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "@sentry/core": "7.117.0", + "@sentry/node": "7.117.0", + "@sentry/react": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "glob": "^10.3.4", "yargs": "^17.6.0" }, @@ -5068,33 +5071,33 @@ } }, "node_modules/@sentry/replay": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.116.0.tgz", - "integrity": "sha512-OrpDtV54pmwZuKp3g7PDiJg6ruRMJKOCzK08TF7IPsKrr4x4UQn56rzMOiABVuTjuS8lNfAWDar6c6vxXFz5KA==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.117.0.tgz", + "integrity": "sha512-V4DfU+x4UsA4BsufbQ8jHYa5H0q5PYUgso2X1PR31g1fpx7yiYguSmCfz1UryM6KkH92dfTnqXapDB44kXOqzQ==", "dependencies": { - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/types": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.116.0.tgz", - "integrity": "sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.117.0.tgz", + "integrity": "sha512-5dtdulcUttc3F0Te7ekZmpSp/ebt/CA71ELx0uyqVGjWsSAINwskFD77sdcjqvZWek//WjiYX1+GRKlpJ1QqsA==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.116.0.tgz", - "integrity": "sha512-Vn9fcvwTq91wJvCd7WTMWozimqMi+dEZ3ie3EICELC2diONcN16ADFdzn65CQQbYwmUzRjN9EjDN2k41pKZWhQ==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.117.0.tgz", + "integrity": "sha512-KkcLY8643SGBiDyPvMQOubBkwVX5IPknMHInc7jYC8pDVncGp7C65Wi506bCNPpKCWspUd/0VDNWOOen51/qKA==", "dependencies": { - "@sentry/types": "7.116.0" + "@sentry/types": "7.117.0" }, "engines": { "node": ">=8" @@ -5426,12 +5429,12 @@ "dev": true }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", - "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.3.2", + "@adobe/css-tools": "^4.4.0", "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", @@ -6457,9 +6460,9 @@ "dev": true }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", - "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", "dev": true, "dependencies": { "@babel/core": "^7.24.5", @@ -6664,6 +6667,15 @@ "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==" }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -7536,9 +7548,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001629", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", - "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", "funding": [ { "type": "opencollective", @@ -9187,9 +9199,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.796", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz", - "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==" + "version": "1.4.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", + "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -10565,6 +10577,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exifreader": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.23.2.tgz", + "integrity": "sha512-rSZIEZUYYPz4j1HMIt3rVfjdaGf34Il3NnDx48eLtqOGpBsA30g1LIRw5kOH8sIm5jT5+1o7izZo98sXWkh3Rg==", + "hasInstallScript": true, + "optionalDependencies": { + "@xmldom/xmldom": "^0.8.10" + } + }, "node_modules/exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -10894,9 +10915,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.0.tgz", + "integrity": "sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -12498,9 +12519,9 @@ "dev": true }, "node_modules/jiti": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", - "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "bin": { "jiti": "bin/jiti.js" } @@ -12792,9 +12813,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "engines": { "node": ">=14" }, @@ -14448,9 +14469,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.63.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", - "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", + "version": "3.64.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.64.0.tgz", + "integrity": "sha512-lxowHVCx3o1zfKJthjWh6WI8Eyi4gdTaK9bUc3oTjYv9j8sp5gSiufkOvoYZ1LgmZKngWUkS5a8G1RSuLWtPgg==", "dependencies": { "semver": "^7.3.5" }, @@ -15781,9 +15802,9 @@ } }, "node_modules/prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", - "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -16435,6 +16456,16 @@ "react": "^18.3.1" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", + "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", + "dev": true, + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -16870,9 +16901,9 @@ } }, "node_modules/remix-development-tools": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.1.6.tgz", - "integrity": "sha512-k2RkkQUVovEKXBxm51ad/MZqnxCaAt3qHYJnLYfTps/S7e/iz71/I0Z/HVqq/7UE446LkuQektk5k7929LLmoA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.2.0.tgz", + "integrity": "sha512-e1x3n6hPXph2EuFzZiXwU+OzXeyOn8gAMlLH2dUoOgxnCHMmRGSBTOSbtlCsqn9SS7RqrYcOvH7/Nqiy+OJUhQ==", "dev": true, "dependencies": { "@radix-ui/react-accordion": "^1.1.2", @@ -16888,6 +16919,7 @@ "date-fns": "^2.30.0", "es-module-lexer": "^1.4.1", "react-diff-viewer-continued": "^3.3.1", + "react-hotkeys-hook": "^4.5.0", "tailwind-merge": "^1.14.0", "uuid": "^9.0.1", "zod": "^3.22.4" @@ -18861,12 +18893,12 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/tsx": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.14.1.tgz", - "integrity": "sha512-GU8pPJq8DdxcJDSK6Bc64c2jW8zBK2hb0jzwHZDfjapbwu6AqvFnAElnzZ17Xb9TH5a/j6/sicTCVYF+eO/cmA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.15.2.tgz", + "integrity": "sha512-kIZTOCmR37nEw0qxQks2dR+eZWSXydhTGmz7yx94vEiJtJGBTkUl0D/jt/5fey+CNdm6i3Cp+29WKRay9ScQUw==", "dev": true, "dependencies": { - "esbuild": "~0.20.2", + "esbuild": "~0.21.4", "get-tsconfig": "^4.7.5" }, "bin": { @@ -18880,9 +18912,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -18896,9 +18928,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -18912,9 +18944,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -18928,9 +18960,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -18944,9 +18976,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -18960,9 +18992,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -18976,9 +19008,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -18992,9 +19024,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -19008,9 +19040,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -19024,9 +19056,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -19040,9 +19072,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -19056,9 +19088,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -19072,9 +19104,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -19088,9 +19120,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -19104,9 +19136,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -19120,9 +19152,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -19136,9 +19168,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -19152,9 +19184,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -19168,9 +19200,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -19184,9 +19216,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -19200,9 +19232,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -19216,9 +19248,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -19232,9 +19264,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -19248,9 +19280,9 @@ } }, "node_modules/tsx/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -19260,29 +19292,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/tunnel-agent": { @@ -24031,34 +24063,34 @@ } }, "@sentry-internal/feedback": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.116.0.tgz", - "integrity": "sha512-tmfO+RTCrhIWMs3yg8X0axhbjWRZLsldSfoXBgfjNCk/XwkYiVGp7WnYVbb+IO+01mHCsis9uaYOBggLgFRB5Q==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.117.0.tgz", + "integrity": "sha512-4X+NnnY17W74TymgLFH7/KPTVYpEtoMMJh8HzVdCmHTOE6j32XKBeBMRaXBhmNYmEgovgyRKKf2KvtSfgw+V1Q==", "requires": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry-internal/replay-canvas": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.116.0.tgz", - "integrity": "sha512-Sy0ydY7A97JY/IFTIj8U25kHqR5rL9oBk3HFE5EK9Phw56irVhHzEwLWae0jlFeCQEWoBYqpPgO5vXsaYzrWvw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.117.0.tgz", + "integrity": "sha512-7hjIhwEcoosr+BIa0AyEssB5xwvvlzUpvD5fXu4scd3I3qfX8gdnofO96a8r+LrQm3bSj+eN+4TfKEtWb7bU5A==", "requires": { - "@sentry/core": "7.116.0", - "@sentry/replay": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/replay": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry-internal/tracing": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.116.0.tgz", - "integrity": "sha512-y5ppEmoOlfr77c/HqsEXR72092qmGYS4QE5gSz5UZFn9CiinEwGfEorcg2xIrrCuU7Ry/ZU2VLz9q3xd04drRA==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.117.0.tgz", + "integrity": "sha512-fAIyijNvKBZNA12IcKo+dOYDRTNrzNsdzbm3DP37vJRKVQu19ucqP4Y6InvKokffDP2HZPzFPDoGXYuXkDhUZg==", "requires": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry/babel-plugin-component-annotate": { @@ -24068,18 +24100,18 @@ "dev": true }, "@sentry/browser": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.116.0.tgz", - "integrity": "sha512-2aosATT5qE+QLKgTmyF9t5Emsluy1MBczYNuPmLhDxGNfB+MA86S8u7Hb0CpxdwjS0nt14gmbiOtJHoeAF3uTw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.117.0.tgz", + "integrity": "sha512-29X9HlvDEKIaWp6XKlNPPSNND0U6P/ede5WA2nVHfs1zJLWdZ7/ijuMc0sH/CueEkqHe/7gt94hBcI7HOU/wSw==", "requires": { - "@sentry-internal/feedback": "7.116.0", - "@sentry-internal/replay-canvas": "7.116.0", - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/integrations": "7.116.0", - "@sentry/replay": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/feedback": "7.117.0", + "@sentry-internal/replay-canvas": "7.117.0", + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/integrations": "7.117.0", + "@sentry/replay": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry/bundler-plugin-core": { @@ -24199,35 +24231,35 @@ "optional": true }, "@sentry/core": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.116.0.tgz", - "integrity": "sha512-J6Wmjjx+o7RwST0weTU1KaKUAlzbc8MGkJV1rcHM9xjNTWTva+nrcCM3vFBagnk2Gm/zhwv3h0PvWEqVyp3U1Q==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.117.0.tgz", + "integrity": "sha512-1XZ4/d/DEwnfM2zBMloXDwX+W7s76lGKQMgd8bwgPJZjjEztMJ7X0uopKAGwlQcjn242q+hsCBR6C+fSuI5kvg==", "requires": { - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry/integrations": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.116.0.tgz", - "integrity": "sha512-UZb60gaF+7veh1Yv79RiGvgGYOnU6xA97H+hI6tKgc1uT20YpItO4X56Vhp0lvyEyUGFZzBRRH1jpMDPNGPkqw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.117.0.tgz", + "integrity": "sha512-U3suSZysmU9EiQqg0ga5CxveAyNbi9IVdsapMDq5EQGNcVDvheXtULs+BOc11WYP3Kw2yWB38VDqLepfc/Fg2g==", "requires": { - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "localforage": "^1.8.1" } }, "@sentry/node": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.116.0.tgz", - "integrity": "sha512-HB/4TrJWbnu6swNzkid+MlwzLwY/D/klGt3R0aatgrgWPo2jJm6bSl4LUT39Cr2eg5I1gsREQtXE2mAlC6gm8w==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.117.0.tgz", + "integrity": "sha512-0MWXdT8dv1MtQGF0aeB8LQTBTJS1L1Vz24+wvdXroR3/52mPYrPWlzuc7+Ew/Dlqdlb5LKVIlkuDSRWj8UKpTQ==", "requires": { - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/integrations": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/integrations": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry/profiling-node": { @@ -24240,55 +24272,55 @@ } }, "@sentry/react": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.116.0.tgz", - "integrity": "sha512-b7sYSIewK/h3dGzm7Rx6tBUzA6w7zw6m5rVIO3fWCy7T3xEUDggUaqklrFVHXUYx2yjzEgTFPg/Dd2NrSzua4w==", - "requires": { - "@sentry/browser": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.117.0.tgz", + "integrity": "sha512-aK+yaEP2esBhaczGU96Y7wkqB4umSIlRAzobv7ER88EGHzZulRaocTpQO8HJJGDHm4D8rD+E893BHnghkoqp4Q==", + "requires": { + "@sentry/browser": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "hoist-non-react-statics": "^3.3.2" } }, "@sentry/remix": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.116.0.tgz", - "integrity": "sha512-edOiZDn1lFoqsGKfFCLPh8ZqSyWaIvyjmJWYePVHXn6lQJsKRe4d1TewNAm0ZCTQhshZkJkAkG73/KQjQ0vvHw==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/remix/-/remix-7.117.0.tgz", + "integrity": "sha512-u6VHt4lsCeCpiABBEVpM1PEPmgtGGbmLotKJOC8amcX7UOxwmbYe/mRfOkffbh9w959E+SH2f393NtCia8wrRg==", "requires": { "@remix-run/router": "1.x", "@sentry/cli": "^2.28.0", - "@sentry/core": "7.116.0", - "@sentry/node": "7.116.0", - "@sentry/react": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0", + "@sentry/core": "7.117.0", + "@sentry/node": "7.117.0", + "@sentry/react": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0", "glob": "^10.3.4", "yargs": "^17.6.0" } }, "@sentry/replay": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.116.0.tgz", - "integrity": "sha512-OrpDtV54pmwZuKp3g7PDiJg6ruRMJKOCzK08TF7IPsKrr4x4UQn56rzMOiABVuTjuS8lNfAWDar6c6vxXFz5KA==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.117.0.tgz", + "integrity": "sha512-V4DfU+x4UsA4BsufbQ8jHYa5H0q5PYUgso2X1PR31g1fpx7yiYguSmCfz1UryM6KkH92dfTnqXapDB44kXOqzQ==", "requires": { - "@sentry-internal/tracing": "7.116.0", - "@sentry/core": "7.116.0", - "@sentry/types": "7.116.0", - "@sentry/utils": "7.116.0" + "@sentry-internal/tracing": "7.117.0", + "@sentry/core": "7.117.0", + "@sentry/types": "7.117.0", + "@sentry/utils": "7.117.0" } }, "@sentry/types": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.116.0.tgz", - "integrity": "sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ==" + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.117.0.tgz", + "integrity": "sha512-5dtdulcUttc3F0Te7ekZmpSp/ebt/CA71ELx0uyqVGjWsSAINwskFD77sdcjqvZWek//WjiYX1+GRKlpJ1QqsA==" }, "@sentry/utils": { - "version": "7.116.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.116.0.tgz", - "integrity": "sha512-Vn9fcvwTq91wJvCd7WTMWozimqMi+dEZ3ie3EICELC2diONcN16ADFdzn65CQQbYwmUzRjN9EjDN2k41pKZWhQ==", + "version": "7.117.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.117.0.tgz", + "integrity": "sha512-KkcLY8643SGBiDyPvMQOubBkwVX5IPknMHInc7jYC8pDVncGp7C65Wi506bCNPpKCWspUd/0VDNWOOen51/qKA==", "requires": { - "@sentry/types": "7.116.0" + "@sentry/types": "7.117.0" } }, "@sentry/vite-plugin": { @@ -24527,12 +24559,12 @@ } }, "@testing-library/jest-dom": { - "version": "6.4.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", - "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", + "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", "dev": true, "requires": { - "@adobe/css-tools": "^4.3.2", + "@adobe/css-tools": "^4.4.0", "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", @@ -25408,9 +25440,9 @@ "dev": true }, "@vitejs/plugin-react": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", - "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", "dev": true, "requires": { "@babel/core": "^7.24.5", @@ -25564,6 +25596,12 @@ "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==" }, + "@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "optional": true + }, "@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -26184,9 +26222,9 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, "caniuse-lite": { - "version": "1.0.30001629", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", - "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==" + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==" }, "ccount": { "version": "2.0.1", @@ -27376,9 +27414,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.796", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz", - "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==" + "version": "1.4.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", + "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==" }, "emoji-regex": { "version": "9.2.2", @@ -28424,6 +28462,14 @@ "strip-final-newline": "^3.0.0" } }, + "exifreader": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-4.23.2.tgz", + "integrity": "sha512-rSZIEZUYYPz4j1HMIt3rVfjdaGf34Il3NnDx48eLtqOGpBsA30g1LIRw5kOH8sIm5jT5+1o7izZo98sXWkh3Rg==", + "requires": { + "@xmldom/xmldom": "^0.8.10" + } + }, "exit-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", @@ -28684,9 +28730,9 @@ } }, "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.0.tgz", + "integrity": "sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==", "requires": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -29788,9 +29834,9 @@ "dev": true }, "jiti": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", - "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==" + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==" }, "js-beautify": { "version": "1.15.1", @@ -30000,9 +30046,9 @@ } }, "lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==" }, "lines-and-columns": { "version": "1.2.4", @@ -31138,9 +31184,9 @@ "dev": true }, "node-abi": { - "version": "3.63.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", - "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", + "version": "3.64.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.64.0.tgz", + "integrity": "sha512-lxowHVCx3o1zfKJthjWh6WI8Eyi4gdTaK9bUc3oTjYv9j8sp5gSiufkOvoYZ1LgmZKngWUkS5a8G1RSuLWtPgg==", "requires": { "semver": "^7.3.5" } @@ -32058,9 +32104,9 @@ "dev": true }, "prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", - "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true }, "prettier-plugin-sql": { @@ -32496,6 +32542,12 @@ "scheduler": "^0.23.2" } }, + "react-hotkeys-hook": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz", + "integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==", + "dev": true + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -32798,9 +32850,9 @@ } }, "remix-development-tools": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.1.6.tgz", - "integrity": "sha512-k2RkkQUVovEKXBxm51ad/MZqnxCaAt3qHYJnLYfTps/S7e/iz71/I0Z/HVqq/7UE446LkuQektk5k7929LLmoA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/remix-development-tools/-/remix-development-tools-4.2.0.tgz", + "integrity": "sha512-e1x3n6hPXph2EuFzZiXwU+OzXeyOn8gAMlLH2dUoOgxnCHMmRGSBTOSbtlCsqn9SS7RqrYcOvH7/Nqiy+OJUhQ==", "dev": true, "requires": { "@radix-ui/react-accordion": "^1.1.2", @@ -32816,6 +32868,7 @@ "date-fns": "^2.30.0", "es-module-lexer": "^1.4.1", "react-diff-viewer-continued": "^3.3.1", + "react-hotkeys-hook": "^4.5.0", "tailwind-merge": "^1.14.0", "uuid": "^9.0.1", "zod": "^3.22.4" @@ -34215,206 +34268,206 @@ } }, "tsx": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.14.1.tgz", - "integrity": "sha512-GU8pPJq8DdxcJDSK6Bc64c2jW8zBK2hb0jzwHZDfjapbwu6AqvFnAElnzZ17Xb9TH5a/j6/sicTCVYF+eO/cmA==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.15.2.tgz", + "integrity": "sha512-kIZTOCmR37nEw0qxQks2dR+eZWSXydhTGmz7yx94vEiJtJGBTkUl0D/jt/5fey+CNdm6i3Cp+29WKRay9ScQUw==", "dev": true, "requires": { - "esbuild": "~0.20.2", + "esbuild": "~0.21.4", "fsevents": "~2.3.3", "get-tsconfig": "^4.7.5" }, "dependencies": { "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, "esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "requires": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } } } diff --git a/package.json b/package.json index 5af643c1..0ba5ddf8 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "dotenv": "^16.3.1", "eslint-plugin-remix-react-routes": "^1.0.5", "execa": "^8.0.1", + "exifreader": "^4.23.2", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "get-port": "^7.0.0", From ad1885a43fc2f5ba606e6d5a8c3d5b7fae7e9c02 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 13:05:47 -0400 Subject: [PATCH 20/54] separating files --- ...ars.panel.artwork-version.images.image.tsx | 87 +++++++++++++++++ .../sidebars.panel.artwork-version.images.tsx | 95 +++---------------- 2 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx new file mode 100644 index 00000000..f1c423aa --- /dev/null +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx @@ -0,0 +1,87 @@ +import { memo } from 'react' +import { ImageFull, ImagePreview } from '#app/components/image' +import { + FlexColumn, + FlexRow, + ImageSidebarListItem, +} from '#app/components/layout' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' +import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' +import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' +import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' +import { sizeInMB } from '#app/utils/asset/image' +import { getArtworkImgSrc } from '#app/utils/misc' + +const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { + return +}) +ImageCreate.displayName = 'ImageCreate' + +const ImageUpdate = memo( + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { + return + }, +) +ImageUpdate.displayName = 'ImageUpdate' + +const ImageDelete = memo( + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { + return + }, +) +ImageDelete.displayName = 'ImageDelete' + +export const ImageListItem = memo( + ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { + const { id, name, attributes } = image + const { altText, height, width, size } = attributes + + return ( + + +
{name}
+
+ + + + + + + + {name} + {altText} + + + + + + + + + + + + + {width}x{height} + + +
{sizeInMB(size)} MB
+
+
+
+
+
+
+ ) + }, +) +ImageListItem.displayName = 'ImageListItem' diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 4c0dbc6f..2c5f7bb7 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -1,9 +1,6 @@ import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' -import { ImageFull, ImagePreview } from '#app/components/image' import { - FlexColumn, - FlexRow, ImageSidebar, ImageSidebarList, ImageSidebarListItem, @@ -13,91 +10,20 @@ import { SidebarPanelHeader, SidebarPanelRowActionsContainer, } from '#app/components/templates' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '#app/components/ui/dialog' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' -import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' -import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' import { AssetTypeEnum } from '#app/schema/asset' import { filterAssetType } from '#app/utils/asset' -import { sizeInMB } from '#app/utils/asset/image' import { useRouteLoaderMatchData } from '#app/utils/matches' -import { getArtworkImgSrc } from '#app/utils/misc' import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' +import { ImageListItem } from './sidebars.panel.artwork-version.images.image' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { return }) ImageCreate.displayName = 'ImageCreate' -const ImageUpdate = memo( - ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { - return - }, -) -ImageUpdate.displayName = 'ImageUpdate' - -const ImageDelete = memo( - ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { - return - }, -) -ImageDelete.displayName = 'ImageDelete' - -const ImageListItem = memo( - ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { - const { id, name, attributes } = image - const { altText, height, width, size } = attributes - - return ( - - -
{name}
-
- - - - - - - - {name} - {altText} - - - - - - - - - - - - - {width}x{height} - - -
{sizeInMB(size)} MB
-
-
-
-
-
-
- ) - }, -) -ImageListItem.displayName = 'ImageListItem' - export const PanelArtworkVersionImages = ({}: {}) => { const matches = useMatches() const { artwork } = useRouteLoaderMatchData( @@ -115,6 +41,17 @@ export const PanelArtworkVersionImages = ({}: {}) => { [artwork], ) + const renderImageListItem = useCallback( + (image: IAssetImage) => ( + + ), + [artwork], + ) + return (
@@ -131,13 +68,7 @@ export const PanelArtworkVersionImages = ({}: {}) => {
No images
)} - {images.map(image => ( - - ))} + {images.map(renderImageListItem)}
From 691635b9cdafba03ea57582c34092e00b1a15cab Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 13:06:32 -0400 Subject: [PATCH 21/54] removed old artworkImage instances since asset with type image is working --- app/models/artwork/artwork.get.server.ts | 16 --- app/models/artwork/artwork.server.ts | 5 - .../images/artwork-image.create.server.ts | 43 -------- .../images/artwork-image.delete.server.ts | 31 ------ app/models/images/artwork-image.get.server.ts | 32 ------ app/models/images/artwork-image.server.ts | 12 -- .../images/artwork-image.update.server.ts | 68 ------------ app/models/images/layer-image.ts | 11 -- .../api.v1+/artwork.image.create.tsx | 97 ---------------- .../api.v1+/artwork.image.delete.tsx | 104 ------------------ .../api.v1+/artwork.image.update.tsx | 99 ----------------- app/schema/artwork-image.ts | 81 -------------- app/services/artwork/image/create.service.ts | 67 ----------- app/services/artwork/image/delete.service.ts | 37 ------- app/services/artwork/image/update.service.ts | 82 -------------- .../asset.image.artwork.delete.service.ts | 4 +- .../asset.image.artwork.update.service.ts | 4 +- .../validate-submission.strategy.ts | 30 ----- app/utils/db.server.ts | 2 - app/utils/prisma-extensions-artwork-image.ts | 57 ---------- app/utils/routes.const.ts | 7 -- .../migration.sql | 16 +++ prisma/schema.prisma | 38 ------- 23 files changed, 20 insertions(+), 923 deletions(-) delete mode 100644 app/models/images/artwork-image.create.server.ts delete mode 100644 app/models/images/artwork-image.delete.server.ts delete mode 100644 app/models/images/artwork-image.get.server.ts delete mode 100644 app/models/images/artwork-image.server.ts delete mode 100644 app/models/images/artwork-image.update.server.ts delete mode 100644 app/models/images/layer-image.ts delete mode 100644 app/routes/resources+/api.v1+/artwork.image.create.tsx delete mode 100644 app/routes/resources+/api.v1+/artwork.image.delete.tsx delete mode 100644 app/routes/resources+/api.v1+/artwork.image.update.tsx delete mode 100644 app/schema/artwork-image.ts delete mode 100644 app/services/artwork/image/create.service.ts delete mode 100644 app/services/artwork/image/delete.service.ts delete mode 100644 app/services/artwork/image/update.service.ts delete mode 100644 app/utils/prisma-extensions-artwork-image.ts create mode 100644 prisma/migrations/20240612170015_remove_artwork_image_and_layer_image/migration.sql diff --git a/app/models/artwork/artwork.get.server.ts b/app/models/artwork/artwork.get.server.ts index 1383a4f4..df74b718 100644 --- a/app/models/artwork/artwork.get.server.ts +++ b/app/models/artwork/artwork.get.server.ts @@ -6,7 +6,6 @@ import { type IArtworkWithProject, type IArtwork, type IArtworkWithBranchesAndVersions, - type IArtworkWithImages, type IArtworkWithAssets, } from '../artwork/artwork.server' @@ -78,21 +77,6 @@ export const getArtworkWithProject = async ({ return artwork } -export const getArtworkWithImages = async ({ - where, -}: { - where: queryArtworkWhereArgsType -}): Promise => { - validateQueryWhereArgsPresent(where) - const artwork = await prisma.artwork.findFirst({ - where, - include: { - images: true, - }, - }) - return artwork -} - export const getArtworkWithAssets = async ({ where, }: { diff --git a/app/models/artwork/artwork.server.ts b/app/models/artwork/artwork.server.ts index 65f309d5..1251f9c3 100644 --- a/app/models/artwork/artwork.server.ts +++ b/app/models/artwork/artwork.server.ts @@ -2,7 +2,6 @@ import { type Artwork } from '@prisma/client' import { type DateOrString } from '#app/definitions/prisma-helper' import { type IArtworkBranchWithVersions } from '../artwork-branch/artwork-branch.server' import { type IAssetParsed } from '../asset/asset.server' -import { type IArtworkImage } from '../images/artwork-image.server' import { type IProjectWithArtworks } from '../project/project.server' // Omitting 'createdAt' and 'updatedAt' from the Artwork interface @@ -21,10 +20,6 @@ export interface IArtworkWithBranchesAndVersions extends IArtwork { branches: IArtworkBranchWithVersions[] } -export interface IArtworkWithImages extends IArtwork { - images: IArtworkImage[] -} - export interface IArtworkWithAssets extends IArtwork { assets: IAssetParsed[] } diff --git a/app/models/images/artwork-image.create.server.ts b/app/models/images/artwork-image.create.server.ts deleted file mode 100644 index 811c5fc7..00000000 --- a/app/models/images/artwork-image.create.server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { NewArtworkImageSchema } from '#app/schema/artwork-image' -import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntityImageSubmission } from '#app/utils/conform-utils' -import { prisma } from '#app/utils/db.server' -import { type IArtwork } from '../artwork/artwork.server' -import { type IArtworkImage } from './artwork-image.server' - -export interface IArtworkImageCreatedResponse { - success: boolean - message?: string - createdArtworkImage?: IArtworkImage -} - -export const validateNewArtworkImageSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateArtworkParentSubmissionStrategy() - - return await validateEntityImageSubmission({ - userId, - formData, - schema: NewArtworkImageSchema, - strategy, - }) -} - -export const createArtworkImage = ({ - data, -}: { - data: { - artworkId: IArtwork['id'] - name: string - altText: string | null - contentType: string - blob: Buffer - } -}) => { - return prisma.artworkImage.create({ - data, - }) -} diff --git a/app/models/images/artwork-image.delete.server.ts b/app/models/images/artwork-image.delete.server.ts deleted file mode 100644 index 86b3dc02..00000000 --- a/app/models/images/artwork-image.delete.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { DeleteArtworkImageSchema } from '#app/schema/artwork-image' -import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntitySubmission } from '#app/utils/conform-utils' -import { prisma } from '#app/utils/db.server' -import { type IArtworkImage } from './artwork-image.server' - -export interface IArtworkImageDeletedResponse { - success: boolean - message?: string -} - -export const validateArtworkImageDeleteSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateArtworkParentSubmissionStrategy() - - return await validateEntitySubmission({ - userId, - formData, - schema: DeleteArtworkImageSchema, - strategy, - }) -} - -export const deleteArtworkImage = ({ id }: { id: IArtworkImage['id'] }) => { - return prisma.artworkImage.delete({ - where: { id }, - }) -} diff --git a/app/models/images/artwork-image.get.server.ts b/app/models/images/artwork-image.get.server.ts deleted file mode 100644 index d7399f3c..00000000 --- a/app/models/images/artwork-image.get.server.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from 'zod' -import { prisma } from '#app/utils/db.server' -import { type IArtworkImage } from './artwork-image.server' - -export type queryArtworkWhereArgsType = z.infer -const whereArgs = z.object({ - id: z.string().optional(), - artworkId: z.string().optional(), -}) - -// TODO: Add schemas for each type of query and parse with zod -// aka if by id that should be present, if by slug that should be present -// owner id should be present unless admin (not set up yet) -const validateQueryWhereArgsPresent = (where: queryArtworkWhereArgsType) => { - if (Object.values(where).some(value => !value)) { - throw new Error( - 'Null or undefined values are not allowed in query parameters for artwork.', - ) - } -} - -export const getArtworkImage = async ({ - where, -}: { - where: queryArtworkWhereArgsType -}): Promise => { - validateQueryWhereArgsPresent(where) - const artwork = await prisma.artworkImage.findFirst({ - where, - }) - return artwork -} diff --git a/app/models/images/artwork-image.server.ts b/app/models/images/artwork-image.server.ts deleted file mode 100644 index 260ec1d0..00000000 --- a/app/models/images/artwork-image.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type ArtworkImage } from '@prisma/client' -import { type DateOrString } from '#app/definitions/prisma-helper' - -// Omitting 'createdAt' and 'updatedAt' from the ArtworkImage interface -// prisma query returns a string for these fields -// also excluding 'blob' field since this will be served from a resource route -type BaseArtworkImage = Omit - -export interface IArtworkImage extends BaseArtworkImage { - createdAt: DateOrString - updatedAt: DateOrString -} diff --git a/app/models/images/artwork-image.update.server.ts b/app/models/images/artwork-image.update.server.ts deleted file mode 100644 index 9ebce2b7..00000000 --- a/app/models/images/artwork-image.update.server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { - ArtworkImageDataUpdateSchema, - EditArtworkImageSchema, -} from '#app/schema/artwork-image' -import { ValidateArtworkImageSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntityImageSubmission } from '#app/utils/conform-utils' -import { findFirstArtworkImageInstance } from '#app/utils/prisma-extensions-artwork-image' -import { type IArtworkImage } from './artwork-image.server' - -export interface IArtworkImageUpdatedResponse { - success: boolean - message?: string - updatedArtworkImage?: IArtworkImage -} - -export const validateEditArtworkImageSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateArtworkImageSubmissionStrategy() - - return await validateEntityImageSubmission({ - userId, - formData, - schema: EditArtworkImageSchema, - strategy, - }) -} - -const getArtworkImageInstance = async ({ id }: { id: IArtworkImage['id'] }) => { - return await findFirstArtworkImageInstance({ - where: { id }, - }) -} - -export const updateArtworkImage = async ({ - id, - name, - altText, -}: { - id: IArtworkImage['id'] - name: string - altText: string | null -}): Promise => { - const artworkImage = await getArtworkImageInstance({ id }) - if (!artworkImage) return { success: false } - - try { - const data = ArtworkImageDataUpdateSchema.parse({ id, name, altText }) - - artworkImage.name = data.name - artworkImage.altText = data.altText - artworkImage.updatedAt = new Date() - await artworkImage.save() - - return { success: true, updatedArtworkImage: artworkImage } - } catch (error) { - // consider how to handle this error where this is called - console.log('updateArtworkImage error:', error) - const errorType = error instanceof Error - const errorMessage = errorType ? error.message : 'An unknown error occurred' - return { - success: false, - message: errorMessage, - } - } -} diff --git a/app/models/images/layer-image.ts b/app/models/images/layer-image.ts deleted file mode 100644 index 3bdad45c..00000000 --- a/app/models/images/layer-image.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type LayerImage } from '@prisma/client' -import { type DateOrString } from '#app/definitions/prisma-helper' - -// Omitting 'createdAt' and 'updatedAt' from the LayerImage interface -// prisma query returns a string for these fields -type BaseLayerImage = Omit - -export interface ILayerImage extends BaseLayerImage { - createdAt: DateOrString - updatedAt: DateOrString -} diff --git a/app/routes/resources+/api.v1+/artwork.image.create.tsx b/app/routes/resources+/api.v1+/artwork.image.create.tsx deleted file mode 100644 index 54682bf9..00000000 --- a/app/routes/resources+/api.v1+/artwork.image.create.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { - json, - type ActionFunctionArgs, - type LoaderFunctionArgs, - unstable_createMemoryUploadHandler as createMemoryUploadHandler, - unstable_parseMultipartFormData as parseMultipartFormData, -} from '@remix-run/node' -import { useFetcher } from '@remix-run/react' -import { redirectBack } from 'remix-utils/redirect-back' -import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { validateNewArtworkImageSubmission } from '#app/models/images/artwork-image.create.server' -import { - MAX_UPLOAD_SIZE, - NewAssetImageArtworkSchema, -} from '#app/schema/asset/image' -import { validateNoJS } from '#app/schema/form-data' -import { artworkImageCreateService } from '#app/services/artwork/image/create.service' -import { requireUserId } from '#app/utils/auth.server' -import { Routes } from '#app/utils/routes.const' - -// https://www.epicweb.dev/full-stack-components - -const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.CREATE -const schema = NewAssetImageArtworkSchema - -// auth GET request to endpoint -export async function loader({ request }: LoaderFunctionArgs) { - await requireUserId(request) - return json({}) -} - -export async function action({ request }: ActionFunctionArgs) { - const userId = await requireUserId(request) - const formData = await parseMultipartFormData( - request, - createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), - ) - const noJS = validateNoJS({ formData }) - - let createSuccess = false - let errorMessage = '' - const { status, submission } = await validateNewArtworkImageSubmission({ - userId, - formData, - }) - - if (status === 'success') { - const { success, message } = await artworkImageCreateService({ - userId, - ...submission.value, - }) - createSuccess = success - errorMessage = message || '' - } - - if (noJS) { - throw redirectBack(request, { - fallback: '/', - }) - } - - return json( - { status, submission, message: errorMessage }, - { - status: status === 'error' || !createSuccess ? 422 : 201, - }, - ) -} - -export const ArtworkImageCreate = ({ artwork }: { artwork: IArtwork }) => { - const artworkId = artwork.id - const formId = `artwork-image-create-${artworkId}` - - const fetcher = useFetcher() - let isHydrated = useHydrated() - - return ( - -
- -
-
- ) -} diff --git a/app/routes/resources+/api.v1+/artwork.image.delete.tsx b/app/routes/resources+/api.v1+/artwork.image.delete.tsx deleted file mode 100644 index afea4f4b..00000000 --- a/app/routes/resources+/api.v1+/artwork.image.delete.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - json, - type ActionFunctionArgs, - type LoaderFunctionArgs, -} from '@remix-run/node' -import { useFetcher } from '@remix-run/react' -import { redirectBack } from 'remix-utils/redirect-back' -import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { type IAssetImage } from '#app/models/asset/image/image.server' -import { validateArtworkImageDeleteSubmission } from '#app/models/images/artwork-image.delete.server' -import { DeleteArtworkImageSchema } from '#app/schema/artwork-image' -import { EntityParentIdType } from '#app/schema/entity' -import { validateNoJS } from '#app/schema/form-data' -import { artworkImageDeleteService } from '#app/services/artwork/image/delete.service' -import { requireUserId } from '#app/utils/auth.server' -import { Routes } from '#app/utils/routes.const' - -// https://www.epicweb.dev/full-stack-components - -const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.DELETE -const schema = DeleteArtworkImageSchema - -// auth GET request to endpoint -export async function loader({ request }: LoaderFunctionArgs) { - await requireUserId(request) - return json({}) -} - -export async function action({ request }: ActionFunctionArgs) { - const userId = await requireUserId(request) - const formData = await request.formData() - const noJS = validateNoJS({ formData }) - - let createSuccess = false - let errorMessage = '' - const { status, submission } = await validateArtworkImageDeleteSubmission({ - userId, - formData, - }) - - if (status === 'success') { - const { success, message } = await artworkImageDeleteService({ - userId, - ...submission.value, - }) - - createSuccess = success - errorMessage = message || '' - } - - if (noJS) { - throw redirectBack(request, { - fallback: '/', - }) - } - - return json( - { status, submission, message: errorMessage }, - { - status: status === 'error' || !createSuccess ? 422 : 201, - }, - ) -} - -export const ArtworkImageDelete = ({ - image, - artwork, -}: { - image: IAssetImage - artwork: IArtwork -}) => { - const imageId = image.id - const iconText = `Delete Image...` - const formId = `artwork-image-delete-${imageId}` - - const fetcher = useFetcher() - let isHydrated = useHydrated() - - return ( - -
- - -
-
- ) -} diff --git a/app/routes/resources+/api.v1+/artwork.image.update.tsx b/app/routes/resources+/api.v1+/artwork.image.update.tsx deleted file mode 100644 index d7ef54c7..00000000 --- a/app/routes/resources+/api.v1+/artwork.image.update.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - json, - type ActionFunctionArgs, - type LoaderFunctionArgs, - unstable_createMemoryUploadHandler as createMemoryUploadHandler, - unstable_parseMultipartFormData as parseMultipartFormData, -} from '@remix-run/node' -import { useFetcher } from '@remix-run/react' -import { redirectBack } from 'remix-utils/redirect-back' -import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' -import { type IAssetImage } from '#app/models/asset/image/image.server' -import { validateEditArtworkImageSubmission } from '#app/models/images/artwork-image.update.server' -import { - EditArtworkImageSchema, - MAX_UPLOAD_SIZE, -} from '#app/schema/artwork-image' -import { validateNoJS } from '#app/schema/form-data' -import { artworkImageUpdateService } from '#app/services/artwork/image/update.service' -import { requireUserId } from '#app/utils/auth.server' -import { Routes } from '#app/utils/routes.const' - -// https://www.epicweb.dev/full-stack-components - -const route = Routes.RESOURCES.API.V1.ARTWORK.IMAGE.UPDATE -const schema = EditArtworkImageSchema - -// auth GET request to endpoint -export async function loader({ request }: LoaderFunctionArgs) { - await requireUserId(request) - return json({}) -} - -export async function action({ request }: ActionFunctionArgs) { - const userId = await requireUserId(request) - const formData = await parseMultipartFormData( - request, - createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), - ) - const noJS = validateNoJS({ formData }) - - let createSuccess = false - let errorMessage = '' - const { status, submission } = await validateEditArtworkImageSubmission({ - userId, - formData, - }) - - if (status === 'success') { - const { success, message } = await artworkImageUpdateService({ - userId, - ...submission.value, - }) - - createSuccess = success - errorMessage = message || '' - } - - if (noJS) { - throw redirectBack(request, { - fallback: '/', - }) - } - - return json( - { status, submission, message: errorMessage }, - { - status: status === 'error' || !createSuccess ? 422 : 201, - }, - ) -} - -export const ArtworkImageUpdate = ({ image }: { image: IAssetImage }) => { - const imageId = image.id - const formId = `artwork-image-update-${imageId}` - - const fetcher = useFetcher() - let isHydrated = useHydrated() - - return ( - -
- -
-
- ) -} diff --git a/app/schema/artwork-image.ts b/app/schema/artwork-image.ts deleted file mode 100644 index 828f9a16..00000000 --- a/app/schema/artwork-image.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { z } from 'zod' - -const MAX_NAME_LENGTH = 240 -const NameSchema = z.string().max(MAX_NAME_LENGTH) - -const MAX_ALT_TEXT_LENGTH = 240 -const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH) - -const MAX_MEGABYTES = 6 -export const MAX_UPLOAD_SIZE = 1024 * 1024 * MAX_MEGABYTES -const ACCEPTED_IMAGE_TYPES = [ - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/webp', - 'image/gif', -] - -const FileSchema = z - .instanceof(File) - .refine(file => file.size > 0, 'Image is required') - .refine( - file => file.size <= MAX_UPLOAD_SIZE, - 'Image size must be less than 3MB', - ) - .refine( - file => ACCEPTED_IMAGE_TYPES.includes(file.type), - 'Image must be a JPEG, PNG, WEBP, or GIF', - ) - -export const NewArtworkImageSchema = z.object({ - artworkId: z.string(), - file: FileSchema, - name: NameSchema, - altText: AltTextSchema.optional(), -}) - -export const EditArtworkImageSchema = z.object({ - id: z.string(), - artworkId: z.string().optional(), - file: FileSchema.optional(), - name: NameSchema, - altText: AltTextSchema.optional(), -}) - -// issues with zod and Buffer -// https://github.com/colinhacks/zod/issues/387 -// const BlobSchema = z.custom(data => { -// return typeof window === 'undefined' -// ? data instanceof Buffer -// : data instanceof File -// }, 'Data is not an instance of a Buffer or File') - -export const ArtworkImageDataCreateSchema = z.object({ - artworkId: z.string(), - - // blob: BlobSchema, - // https://github.com/colinhacks/zod/issues/925 - // blob: z.instanceof(Buffer), - // blob: z.unknown().refine(val => val !== undefined, { - // message: 'stream must be defined', - // }), - // blob: z.any().refine(val => val !== undefined, { - // message: 'stream must be defined', - // }), - contentType: z.string(), - name: NameSchema, - altText: AltTextSchema, -}) - -export const ArtworkImageDataUpdateSchema = z.object({ - id: z.string(), - contentType: z.string().optional(), - name: NameSchema, - altText: AltTextSchema, -}) - -export const DeleteArtworkImageSchema = z.object({ - id: z.string(), - artworkId: z.string(), -}) diff --git a/app/services/artwork/image/create.service.ts b/app/services/artwork/image/create.service.ts deleted file mode 100644 index 9119f597..00000000 --- a/app/services/artwork/image/create.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { invariant } from '@epic-web/invariant' -import { getArtwork } from '#app/models/artwork/artwork.get.server' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { - createArtworkImage, - type IArtworkImageCreatedResponse, -} from '#app/models/images/artwork-image.create.server' -import { type IUser } from '#app/models/user/user.server' -import { ArtworkImageDataCreateSchema } from '#app/schema/artwork-image' -import { prisma } from '#app/utils/db.server' - -export const artworkImageCreateService = async ({ - userId, - artworkId, - blob, - contentType, - name, - altText, -}: { - userId: IUser['id'] - artworkId: IArtwork['id'] - blob: Buffer - contentType: string - name: string - altText: string | null -}): Promise => { - try { - // Step 1: find the artwork - const artwork = await getArtwork({ - where: { id: artworkId, ownerId: userId }, - }) - invariant(artwork, 'Artwork not found') - - // Step 2: validate image data - // zod schema for blob Buffer/File is not working - // pass in separately from validation - const imageData = ArtworkImageDataCreateSchema.parse({ - artworkId, - contentType, - name, - altText: altText || 'No alt text provided.', - }) - - // Step 3: create the artwork image via promise - const createArtworkImagePromises = [] - - const createArtworkImagePromise = createArtworkImage({ - data: { ...imageData, blob }, - }) - createArtworkImagePromises.push(createArtworkImagePromise) - - const [createdArtworkImage] = await prisma.$transaction( - createArtworkImagePromises, - ) - - return { - createdArtworkImage, - success: true, - } - } catch (error) { - console.log(error) - return { - success: false, - message: 'Unknown error creating artwork generator.', - } - } -} diff --git a/app/services/artwork/image/delete.service.ts b/app/services/artwork/image/delete.service.ts deleted file mode 100644 index 43279adb..00000000 --- a/app/services/artwork/image/delete.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type User } from '@prisma/client' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { - type IArtworkImageDeletedResponse, - deleteArtworkImage, -} from '#app/models/images/artwork-image.delete.server' -import { type IArtworkImage } from '#app/models/images/artwork-image.server' -import { prisma } from '#app/utils/db.server' - -export const artworkImageDeleteService = async ({ - userId, - id, - artworkId, -}: { - userId: User['id'] - id: IArtworkImage['id'] - artworkId: IArtwork['id'] -}): Promise => { - try { - const deleteArtworkImagePromises = [] - - const deleteArtworkImagePromise = deleteArtworkImage({ id }) - deleteArtworkImagePromises.push(deleteArtworkImagePromise) - - await prisma.$transaction(deleteArtworkImagePromises) - - return { success: true } - } catch (error) { - console.log('artworkImageDeleteService error:', error) - const errorType = error instanceof Error - const errorMessage = errorType ? error.message : 'An unknown error occurred' - return { - success: false, - message: errorMessage, - } - } -} diff --git a/app/services/artwork/image/update.service.ts b/app/services/artwork/image/update.service.ts deleted file mode 100644 index d7b8896c..00000000 --- a/app/services/artwork/image/update.service.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { invariant } from '@epic-web/invariant' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { createArtworkImage } from '#app/models/images/artwork-image.create.server' -import { deleteArtworkImage } from '#app/models/images/artwork-image.delete.server' -import { getArtworkImage } from '#app/models/images/artwork-image.get.server' -import { - updateArtworkImage, - type IArtworkImageUpdatedResponse, -} from '#app/models/images/artwork-image.update.server' -import { type IUser } from '#app/models/user/user.server' -import { ArtworkImageDataCreateSchema } from '#app/schema/artwork-image' -import { prisma } from '#app/utils/db.server' - -export const artworkImageUpdateService = async ({ - userId, - id, - blob, - contentType, - name, - altText, -}: { - userId: IUser['id'] - id: IArtwork['id'] - blob?: Buffer - contentType?: string - name: string - altText: string | null -}): Promise => { - try { - // can't seem to update the blob of an image - // so just going to replace with newly created image - if (blob) { - // Step 1: find the artwork image - const artworkImage = await getArtworkImage({ - where: { id }, - }) - invariant(artworkImage, 'Artwork Image not found') - - // Step 2: validata the new image data - const imageData = ArtworkImageDataCreateSchema.parse({ - artworkId: artworkImage.artworkId, - contentType, - name, - altText, - }) - - // Step 3: replace the artwork image via promises - const replaceArtworkImagePromises = [] - - // delete the old image - const deleteArtworkImagePromise = deleteArtworkImage({ id }) - replaceArtworkImagePromises.push(deleteArtworkImagePromise) - - // create the new image - const createArtworkImagePromise = createArtworkImage({ - data: { ...imageData, blob }, - }) - replaceArtworkImagePromises.push(createArtworkImagePromise) - - const [, replacedArtworkImage] = await prisma.$transaction( - replaceArtworkImagePromises, - ) - - return { - success: true, - updatedArtworkImage: replacedArtworkImage, - } - } else { - return await updateArtworkImage({ - id, - name, - altText, - }) - } - } catch (error) { - console.log(error) - return { - success: false, - message: 'Unknown error creating artwork generator.', - } - } -} diff --git a/app/services/asset.image.artwork.delete.service.ts b/app/services/asset.image.artwork.delete.service.ts index 865098bd..ba202c71 100644 --- a/app/services/asset.image.artwork.delete.service.ts +++ b/app/services/asset.image.artwork.delete.service.ts @@ -15,10 +15,10 @@ export const assetImageArtworkDeleteService = async ({ }): Promise => { try { // Step 1: delete the asset image via promise - const deleteArtworkImagePromise = deleteAssetImage({ id }) + const deleteAssetImageArtworkPromise = deleteAssetImage({ id }) // Step 2: execute the transaction - await prisma.$transaction([deleteArtworkImagePromise]) + await prisma.$transaction([deleteAssetImageArtworkPromise]) return { success: true } } catch (error) { diff --git a/app/services/asset.image.artwork.update.service.ts b/app/services/asset.image.artwork.update.service.ts index abc1192e..6dbaa39e 100644 --- a/app/services/asset.image.artwork.update.service.ts +++ b/app/services/asset.image.artwork.update.service.ts @@ -55,7 +55,7 @@ export const assetImageArtworkUpdateService = async ({ if (blob) { // Step 3: delete the asset image with old blob via promise - const deleteArtworkImagePromise = deleteAssetImage({ id }) + const deleteAssetImageArtworkPromise = deleteAssetImage({ id }) // Step 4: create the asset image with new blob via promise const createAssetImagePromise = createAssetImageArtwork({ @@ -64,7 +64,7 @@ export const assetImageArtworkUpdateService = async ({ // Step 5: execute the transaction const [, updatedAssetImage] = await prisma.$transaction([ - deleteArtworkImagePromise, + deleteAssetImageArtworkPromise, createAssetImagePromise, ]) diff --git a/app/strategies/validate-submission.strategy.ts b/app/strategies/validate-submission.strategy.ts index 2b4c5306..a2655d75 100644 --- a/app/strategies/validate-submission.strategy.ts +++ b/app/strategies/validate-submission.strategy.ts @@ -5,7 +5,6 @@ import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get. import { getArtworkVersion } from '#app/models/artwork-version/artwork-version.get.server' import { getAsset } from '#app/models/asset/asset.get.server' import { getDesign } from '#app/models/design/design.get.server' -import { getArtworkImage } from '#app/models/images/artwork-image.get.server' import { getLayer } from '#app/models/layer/layer.get.server' import { addNotFoundIssue } from '#app/utils/conform-utils' @@ -157,35 +156,6 @@ export class ValidateLayerParentSubmissionStrategy } } -export class ValidateArtworkImageSubmissionStrategy - implements IValidateSubmissionStrategy -{ - async validateFormDataEntity({ - userId, - data, - ctx, - }: { - userId: User['id'] - data: any - ctx: any - }): Promise { - const { id } = data - // check for image - const image = await getArtworkImage({ - where: { id }, - }) - if (!image) ctx.addIssue(addNotFoundIssue('Image')) - - if (image) { - // check that image belongs to artwork that belongs to user - const artwork = await getArtwork({ - where: { id: image.artworkId, ownerId: userId }, - }) - if (!artwork) ctx.addIssue(addNotFoundIssue('Image')) - } - } -} - export class ValidateAssetSubmissionStrategy implements IValidateSubmissionStrategy { diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts index 050549b7..6d6f3ac6 100644 --- a/app/utils/db.server.ts +++ b/app/utils/db.server.ts @@ -3,7 +3,6 @@ import { PrismaClient, type Prisma } from '@prisma/client' import { type DefaultArgs } from '@prisma/client/runtime/library' import chalk from 'chalk' import { ArtworkPrismaExtensions } from './prisma-extensions-artwork' -import { ArtworkImagePrismaExtensions } from './prisma-extensions-artwork-image' import { ArtworkVersionPrismaExtensions } from './prisma-extensions-artwork-version' import { DesignPrismaExtensions, @@ -28,7 +27,6 @@ export type PrismaTransactionType = Omit< export const prismaExtended = remember('prisma', () => { return new PrismaClient({}) .$extends(ArtworkPrismaExtensions) - .$extends(ArtworkImagePrismaExtensions) .$extends(ArtworkVersionPrismaExtensions) .$extends(DesignPrismaQueryExtensions) .$extends(DesignPrismaExtensions) diff --git a/app/utils/prisma-extensions-artwork-image.ts b/app/utils/prisma-extensions-artwork-image.ts deleted file mode 100644 index 7b84040e..00000000 --- a/app/utils/prisma-extensions-artwork-image.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' -import { prismaExtended } from './db.server' - -// must be in /utils to actually connect to prisma - -// just do extended when you want to .save() or .delete() -// this has to be in /utils to actually connect to prisma - -// instance methods .save() and .delete() -// https://github.com/prisma/prisma-client-extensions/blob/main/instance-methods/script.ts - -export const ArtworkImagePrismaExtensions = Prisma.defineExtension({ - result: { - artworkImage: { - save: { - needs: { id: true }, - compute(artworkImage) { - return () => { - return prismaExtended.artworkImage.update({ - where: { id: artworkImage.id }, - data: artworkImage, - }) - } - }, - }, - - delete: { - needs: { id: true }, - compute({ id }) { - return () => { - return prismaExtended.artworkImage.delete({ - where: { id }, - }) - } - }, - }, - }, - }, -}) - -// https://github.com/prisma/docs/issues/5058#issuecomment-1636473141 -export type ExtendedArtworkImage = Prisma.Result< - typeof prismaExtended.artworkImage, - any, - 'findFirstOrThrow' -> - -export const findFirstArtworkImageInstance = async ({ - where, -}: { - where: whereArgsType -}): Promise => { - return await prismaExtended.artworkImage.findFirst({ - where, - }) -} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 12792667..83c2a091 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -4,13 +4,6 @@ export const Routes = { RESOURCES: { API: { V1: { - ARTWORK: { - IMAGE: { - CREATE: `${pathBase}/artwork/image/create`, - DELETE: `${pathBase}/artwork/image/delete`, - UPDATE: `${pathBase}/artwork/image/update`, - }, - }, ARTWORK_BRANCH: { CREATE: `${pathBase}/artwork-branch/create`, }, diff --git a/prisma/migrations/20240612170015_remove_artwork_image_and_layer_image/migration.sql b/prisma/migrations/20240612170015_remove_artwork_image_and_layer_image/migration.sql new file mode 100644 index 00000000..7871deeb --- /dev/null +++ b/prisma/migrations/20240612170015_remove_artwork_image_and_layer_image/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the `ArtworkImage` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `LayerImage` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "ArtworkImage"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "LayerImage"; +PRAGMA foreign_keys=on; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7efb6fa4..4d955fb6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -226,7 +226,6 @@ model Artwork { branches ArtworkBranch[] mergeRequests ArtworkMergeRequest[] - images ArtworkImage[] assets Asset[] // non-unique foreign key @@ -239,24 +238,6 @@ model Artwork { @@unique([slug, ownerId]) } -// consider a service -// https://github.com/epicweb-dev/epic-stack/blob/main/docs/decisions/018-images.md -model ArtworkImage { - id String @id @default(cuid()) - name String @default("image") - altText String? - contentType String - blob Bytes - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - artwork Artwork @relation(fields: [artworkId], references: [id], onDelete: Cascade, onUpdate: Cascade) - artworkId String - - // non-unique foreign key - @@index([artworkId]) -} model Layer { id String @id @default(cuid()) @@ -286,7 +267,6 @@ model Layer { children Layer[] @relation("ParentChildLayer") designs Design[] - images LayerImage[] assets Asset[] // non-unique foreign key @@ -299,24 +279,6 @@ model Layer { @@unique([slug, ownerId, artworkVersionId]) } -// refactor image structure later to use a single table for all images -// mayber follow design type architecture -model LayerImage { - id String @id @default(cuid()) - altText String? - contentType String - blob Bytes - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - layer Layer @relation(fields: [layerId], references: [id], onDelete: Cascade, onUpdate: Cascade) - layerId String - - // non-unique foreign key - @@index([layerId]) -} - model Design { id String @id @default(cuid()) type String // e.g. palette, size, fill, stroke, line, etc. From 579684bae6a7ab3925c453e733f6596ea5f06149 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 13:36:50 -0400 Subject: [PATCH 22/54] protecting asset image endpoint for artworks --- .../templates/form/fetcher-image-upload.tsx | 7 ++++--- app/models/asset/image/image.get.server.ts | 6 ++++++ .../sidebars.panel.artwork-version.images.image.tsx | 7 ++++--- .../sidebars.panel.artwork-version.images.tsx | 4 ++-- .../resources+/api.v1+/asset.image.artwork.update.tsx | 3 +++ ...eId.tsx => artwork.$artworkId.images.$imageId.tsx} | 11 +++++++++-- app/utils/misc.tsx | 10 ++++++++-- 7 files changed, 36 insertions(+), 12 deletions(-) rename app/routes/resources+/{artwork-images.$imageId.tsx => artwork.$artworkId.images.$imageId.tsx} (62%) diff --git a/app/components/templates/form/fetcher-image-upload.tsx b/app/components/templates/form/fetcher-image-upload.tsx index 7ebf5286..6d790563 100644 --- a/app/components/templates/form/fetcher-image-upload.tsx +++ b/app/components/templates/form/fetcher-image-upload.tsx @@ -33,7 +33,7 @@ import { Label } from '#app/components/ui/label' import { PanelIconButton } from '#app/components/ui/panel-icon-button' import { StatusButton } from '#app/components/ui/status-button' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { getArtworkImgSrc, useIsPending } from '#app/utils/misc' +import { useIsPending } from '#app/utils/misc' import { TooltipHydrated } from '../tooltip' export const FetcherImageUpload = ({ @@ -42,6 +42,7 @@ export const FetcherImageUpload = ({ schema, formId, image, + imgSrc, icon, iconText, tooltipText, @@ -55,6 +56,7 @@ export const FetcherImageUpload = ({ schema: z.ZodSchema formId: string image?: IAssetImage + imgSrc?: string icon: IconName iconText: string tooltipText: string @@ -82,8 +84,7 @@ export const FetcherImageUpload = ({ }) const [previewImage, setPreviewImage] = useState( - // TODO: get image to strategy for other image types when needed - fields.id.defaultValue ? getArtworkImgSrc(fields.id.defaultValue) : null, + imgSrc ?? null, ) // close after successful submission diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 972258dc..3fb7e698 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -55,12 +55,18 @@ export const getAssetImage = async ({ // for loading the image from the route url export const getAssetImageArtworkSrc = async ({ id, + artworkId, + ownerId, }: { id: IAssetImage['id'] + artworkId: IAssetImage['artworkId'] + ownerId: IAssetImage['ownerId'] }): Promise => { const image = await prisma.asset.findUnique({ where: { id, + ownerId, + artworkId, type: AssetTypeEnum.IMAGE, }, select: { diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx index f1c423aa..586c3b2e 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx @@ -19,7 +19,7 @@ import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.im import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' import { sizeInMB } from '#app/utils/asset/image' -import { getArtworkImgSrc } from '#app/utils/misc' +import { getArtworkAssetImgSrc } from '#app/utils/misc' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { return @@ -44,6 +44,7 @@ export const ImageListItem = memo( ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { const { id, name, attributes } = image const { altText, height, width, size } = attributes + const imgSrc = getArtworkAssetImgSrc({ imageId: id, artworkId: artwork.id }) return ( @@ -53,14 +54,14 @@ export const ImageListItem = memo( - + {name} {altText} - + diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 2c5f7bb7..3a491fd7 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -36,7 +36,7 @@ export const PanelArtworkVersionImages = ({}: {}) => { type: AssetTypeEnum.IMAGE, }) - const artworkImageCreate = useCallback( + const imageCreate = useCallback( () => , [artwork], ) @@ -57,7 +57,7 @@ export const PanelArtworkVersionImages = ({}: {}) => { - {artworkImageCreate()} + {imageCreate()} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx index 7e22e428..77e0eb32 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx @@ -19,6 +19,7 @@ import { import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkUpdateService } from '#app/services/asset.image.artwork.update.service' import { requireUserId } from '#app/utils/auth.server' +import { getArtworkAssetImgSrc } from '#app/utils/misc' import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components @@ -81,6 +82,7 @@ export const AssetImageArtworkUpdate = ({ const imageId = image.id const artworkId = artwork.id const formId = `asset-image-${imageId}-artwork-${artworkId}-update` + const imgSrc = getArtworkAssetImgSrc({ imageId, artworkId }) const fetcher = useFetcher() let isHydrated = useHydrated() @@ -92,6 +94,7 @@ export const AssetImageArtworkUpdate = ({ schema={schema} formId={formId} image={image} + imgSrc={imgSrc} icon="pencil-1" iconText="Edit Image" tooltipText="Edit image..." diff --git a/app/routes/resources+/artwork-images.$imageId.tsx b/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx similarity index 62% rename from app/routes/resources+/artwork-images.$imageId.tsx rename to app/routes/resources+/artwork.$artworkId.images.$imageId.tsx index 249b79a4..1871bbf5 100644 --- a/app/routes/resources+/artwork-images.$imageId.tsx +++ b/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx @@ -1,10 +1,17 @@ import { invariantResponse } from '@epic-web/invariant' import { type LoaderFunctionArgs } from '@remix-run/node' import { getAssetImageArtworkSrc } from '#app/models/asset/image/image.get.server' +import { requireUserId } from '#app/utils/auth.server' -export async function loader({ params }: LoaderFunctionArgs) { +export async function loader({ params, request }: LoaderFunctionArgs) { + const userId = await requireUserId(request) + invariantResponse(params.artworkId, 'Artwork ID is required', { status: 400 }) invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) - const image = await getAssetImageArtworkSrc({ id: params.imageId }) + const image = await getAssetImageArtworkSrc({ + id: params.imageId, + artworkId: params.artworkId, + ownerId: userId, + }) invariantResponse(image, 'Not found', { status: 404 }) diff --git a/app/utils/misc.tsx b/app/utils/misc.tsx index 9ea71cb7..98ce13dd 100644 --- a/app/utils/misc.tsx +++ b/app/utils/misc.tsx @@ -13,8 +13,14 @@ export function getNoteImgSrc(imageId: string) { return `/resources/note-images/${imageId}` } -export function getArtworkImgSrc(imageId: string) { - return `/resources/artwork-images/${imageId}` +export function getArtworkAssetImgSrc({ + artworkId, + imageId, +}: { + artworkId: string + imageId: string +}) { + return `/resources/artwork/${artworkId}/images/${imageId}` } export function getErrorMessage(error: unknown) { From 5aa5d2c61a030f076e428813942bdaa5ef1709b0 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 14:59:09 -0400 Subject: [PATCH 23/54] version with children interface --- .../artwork-version.get.server.ts | 22 ++++-- .../artwork-version/artwork-version.server.ts | 8 ++ app/models/asset/asset.server.ts | 7 +- app/models/design/design.server.ts | 2 + .../$branchSlug.$versionSlug.tsx | 4 +- ...ebars.panel.artwork-version.background.tsx | 4 +- .../sidebars.panel.artwork-version.frame.tsx | 4 +- .../sidebars.panel.artwork-version.layers.tsx | 4 +- .../sidebars.panel.artwork-version.tsx | 6 +- ...debars.panel.artwork-version.watermark.tsx | 6 +- .../sidebars.panel.assets.artwork-version.tsx | 26 +++++++ .../__components/sidebars.panel.assets.tsx | 76 +++++++++++++++++++ ...sidebars.panel.designs.artwork-version.tsx | 7 +- .../$artworkSlug+/__components/sidebars.tsx | 6 +- app/schema/design.ts | 6 +- 15 files changed, 159 insertions(+), 29 deletions(-) create mode 100644 app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx create mode 100644 app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx diff --git a/app/models/artwork-version/artwork-version.get.server.ts b/app/models/artwork-version/artwork-version.get.server.ts index 262c821b..4687a1c6 100644 --- a/app/models/artwork-version/artwork-version.get.server.ts +++ b/app/models/artwork-version/artwork-version.get.server.ts @@ -1,9 +1,12 @@ +import { invariant } from '@epic-web/invariant' import { z } from 'zod' import { zodStringOrNull } from '#app/schema/zod-helpers' +import { deserializeAssets } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' import { type IArtworkVersionWithDesignsAndLayers, type IArtworkVersion, + type IArtworkVersionWithChildren, } from './artwork-version.server' export type queryArtworkVersionWhereArgsType = z.infer @@ -29,12 +32,14 @@ const includeDesigns = { } // no ordering for now since these are linked lists -const includeDesignsAndLayers = { +const artworkVersionChildren = { + assets: true, designs: { include: includeDesigns, }, layers: { include: { + assets: true, designs: { include: includeDesigns, }, @@ -89,17 +94,20 @@ export const getArtworkVersion = async ({ return artworkVersion } -export const getArtworkVersionWithDesignsAndLayers = async ({ +export const getArtworkVersionWithChildren = async ({ where, }: { where: queryArtworkVersionWhereArgsType -}): Promise => { +}): Promise => { validateQueryWhereArgsPresent(where) const artworkVersion = await prisma.artworkVersion.findFirst({ where, - include: includeDesignsAndLayers, + include: artworkVersionChildren, }) - return artworkVersion + invariant(artworkVersion, 'Artwork Version not found') + + const validatedAssets = deserializeAssets({ assets: artworkVersion.assets }) + return { ...artworkVersion, assets: validatedAssets } } export const getStarredArtworkVersionsByArtworkId = async ({ @@ -115,7 +123,7 @@ export const getStarredArtworkVersionsByArtworkId = async ({ starred: true, }, include: { - ...includeDesignsAndLayers, + ...artworkVersionChildren, branch: true, }, orderBy: { @@ -132,7 +140,7 @@ export const getAllPublishedArtworkVersions = async (): Promise< where: { published: true, }, - include: includeDesignsAndLayers, + include: artworkVersionChildren, orderBy: { publishedAt: 'desc', }, diff --git a/app/models/artwork-version/artwork-version.server.ts b/app/models/artwork-version/artwork-version.server.ts index 4dcc3947..c2af6987 100644 --- a/app/models/artwork-version/artwork-version.server.ts +++ b/app/models/artwork-version/artwork-version.server.ts @@ -2,6 +2,7 @@ import { type ArtworkVersion } from '@prisma/client' import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { type DateOrString } from '#app/definitions/prisma-helper' import { type IArtworkBranch } from '../artwork-branch/artwork-branch.server' +import { type IAssetParsed } from '../asset/asset.server' import { type IDesignWithType } from '../design/design.server' import { type ILayerWithDesigns } from '../layer/layer.server' @@ -24,6 +25,13 @@ export interface IArtworkVersionWithDesignsAndLayers extends IArtworkVersion { branch?: IArtworkBranch } +export interface IArtworkVersionWithChildren extends IArtworkVersion { + designs: IDesignWithType[] + layers: ILayerWithDesigns[] + assets: IAssetParsed[] + branch?: IArtworkBranch +} + // created this for profile artwork view to review starred versions // now wanting to display canvas from dialog // canvas requires designs and layers to compute diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index 39e48af1..7b4284b4 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -11,7 +11,12 @@ import { // prisma query returns a string for these fields // omit type string to ensure type safety with assetTypeEnum // omit attributes string so that extended asset types can insert their own attributes -type BaseAsset = Omit +// omit blob to speed up queries and reduce memory usage +// asset image blobs are requested via resource routes for example +type BaseAsset = Omit< + Asset, + 'type' | 'attributes' | 'blob' | 'createdAt' | 'updatedAt' +> export interface IAsset extends BaseAsset { type: string diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index dcbb0b73..c2bcb7c5 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -8,6 +8,7 @@ import { prisma } from '#app/utils/db.server' import { type IArtworkVersionWithDesignsAndLayers, type IArtworkVersion, + type IArtworkVersionWithChildren, } from '../artwork-version/artwork-version.server' import { type IFillCreateOverrides } from '../design-type/fill/fill.create.server' import { type IFill } from '../design-type/fill/fill.server' @@ -34,6 +35,7 @@ export type IDesignEntityId = | IDesign['id'] | IArtworkVersion['id'] | IArtworkVersionWithDesignsAndLayers['id'] + | IArtworkVersionWithChildren['id'] export type IDesignEntityIdOrNull = IDesignEntityId | null | undefined export interface IDesignCreateOverrides { diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx index d82a7623..b23ee709 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/$branchSlug.$versionSlug.tsx @@ -14,7 +14,7 @@ import { } from '#app/components/layout' import { getArtworkWithAssets } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' -import { getArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.get.server' +import { getArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.get.server' import { getUserBasic } from '#app/models/user/user.get.server' import { artworkVersionGeneratorBuildService } from '#app/services/artwork/version/generator/build.service' import { requireUserId } from '#app/utils/auth.server' @@ -50,7 +50,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { ? { ownerId: owner.id, branchId: branch.id, nextId: null } : { ownerId: owner.id, branchId: branch.id, slug: versionSlug } - const version = await getArtworkVersionWithDesignsAndLayers({ where }) + const version = await getArtworkVersionWithChildren({ where }) invariantResponse(version, 'Artwork Version not found', { status: 404 }) const selectedLayer = version.layers.find(layer => layer.selected) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.background.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.background.tsx index 393f2896..e90cf020 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.background.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.background.tsx @@ -5,13 +5,13 @@ import { SidebarPanelRowContainer, SidebarPanelRowValuesContainer, } from '#app/components/templates' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { ArtworkVersionBackground } from '#app/routes/resources+/api.v1+/artwork-version.update.background' export const PanelArtworkVersionBackground = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { return ( diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.frame.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.frame.tsx index 31bf73c6..2d1a54ee 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.frame.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.frame.tsx @@ -5,14 +5,14 @@ import { SidebarPanelRowContainer, SidebarPanelRowValuesContainer, } from '#app/components/templates' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { ArtworkVersionHeight } from '#app/routes/resources+/api.v1+/artwork-version.update.height' import { ArtworkVersionWidth } from '#app/routes/resources+/api.v1+/artwork-version.update.width' export const PanelArtworkVersionFrame = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { return ( diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.layers.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.layers.tsx index 18f9d420..fc6c903e 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.layers.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.layers.tsx @@ -1,5 +1,5 @@ import { DashboardEntityPanel } from '#app/components/templates/panel/dashboard-entity-panel' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { type ILayerWithDesigns } from '#app/models/layer/layer.server' import { DashboardPanelCreateArtworkVersionLayerStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' import { DashboardPanelArtworkVersionLayerActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' @@ -9,7 +9,7 @@ import { orderLinkedItems } from '#app/utils/linked-list.utils' export const PanelArtworkVersionLayers = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { const orderedLayers = orderLinkedItems(version.layers) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx index 7750f68a..a168456d 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx @@ -1,13 +1,14 @@ -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { PanelArtworkVersionBackground } from './sidebars.panel.artwork-version.background' import { PanelArtworkVersionFrame } from './sidebars.panel.artwork-version.frame' import { PanelArtworkVersionWatermark } from './sidebars.panel.artwork-version.watermark' +import { PanelArtworkVersionAssets } from './sidebars.panel.assets.artwork-version' import { PanelArtworkVersionDesigns } from './sidebars.panel.designs.artwork-version' export const PanelArtworkVersion = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { return (
@@ -15,6 +16,7 @@ export const PanelArtworkVersion = ({ +
) } diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.watermark.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.watermark.tsx index edd5ea97..c0506fab 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.watermark.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.watermark.tsx @@ -7,12 +7,12 @@ import { SidebarPanelRowContainer, SidebarPanelRowValuesContainer, } from '#app/components/templates' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { ArtworkVersionWatermark } from '#app/routes/resources+/api.v1+/artwork-version.update.watermark' import { ArtworkVersionWatermarkColor } from '#app/routes/resources+/api.v1+/artwork-version.update.watermark-color' const WatermarkToggle = memo( - ({ version }: { version: IArtworkVersionWithDesignsAndLayers }) => { + ({ version }: { version: IArtworkVersionWithChildren }) => { return }, ) @@ -21,7 +21,7 @@ WatermarkToggle.displayName = 'WatermarkToggle' export const PanelArtworkVersionWatermark = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { const artworkVersionWatermarkToggle = useCallback( () => , diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx new file mode 100644 index 00000000..fe6fdf0a --- /dev/null +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx @@ -0,0 +1,26 @@ +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { DashboardPanelCreateArtworkVersionDesignTypeStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' +import { DashboardPanelArtworkVersionDesignActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' +import { DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' +import { PanelAssets } from './sidebars.panel.assets' + +export const PanelArtworkVersionAssets = ({ + version, +}: { + version: IArtworkVersionWithChildren +}) => { + const strategyEntityNew = + new DashboardPanelCreateArtworkVersionDesignTypeStrategy() + const strategyReorder = + new DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy() + const strategyActions = new DashboardPanelArtworkVersionDesignActionStrategy() + + return ( + + ) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx new file mode 100644 index 00000000..8bf509a9 --- /dev/null +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx @@ -0,0 +1,76 @@ +import { DashboardEntityPanel } from '#app/components/templates/panel/dashboard-entity-panel' +import { type IDesignWithType } from '#app/models/design/design.server' +import { type designTypeEnum, type DesignParentType } from '#app/schema/design' +import { type IDashboardPanelCreateEntityStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' +import { type IDashboardPanelEntityActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' +import { type IDashboardPanelUpdateEntityOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' +import { + designsByTypeToPanelArray, + filterAndOrderDesignsByType, +} from '#app/utils/design' + +export const PanelAssets = ({ + parent, + strategyEntityNew, + strategyReorder, + strategyActions, +}: { + parent: DesignParentType + strategyEntityNew: IDashboardPanelCreateEntityStrategy + strategyReorder: IDashboardPanelUpdateEntityOrderStrategy + strategyActions: IDashboardPanelEntityActionStrategy +}) => { + const orderedDesigns = filterAndOrderDesignsByType({ + designs: parent.designs, + }) + const designTypePanels = designsByTypeToPanelArray({ + designs: orderedDesigns, + }) + + return ( +
+ {designTypePanels.map(designTypePanel => { + return ( + + ) + })} +
+ ) +} + +export const PanelDesign = ({ + parent, + designTypePanel, + strategyEntityNew, + strategyReorder, + strategyActions, +}: { + parent: DesignParentType + designTypePanel: { + type: designTypeEnum + designs: IDesignWithType[] + } + strategyEntityNew: IDashboardPanelCreateEntityStrategy + strategyReorder: IDashboardPanelUpdateEntityOrderStrategy + strategyActions: IDashboardPanelEntityActionStrategy +}) => { + const { type, designs } = designTypePanel + return ( + + ) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.artwork-version.tsx index ad75fc65..4905a1d9 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.artwork-version.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.artwork-version.tsx @@ -1,4 +1,4 @@ -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { DashboardPanelCreateArtworkVersionDesignTypeStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' import { DashboardPanelArtworkVersionDesignActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' import { DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' @@ -7,14 +7,13 @@ import { PanelDesigns } from './sidebars.panel.designs' export const PanelArtworkVersionDesigns = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { const strategyEntityNew = new DashboardPanelCreateArtworkVersionDesignTypeStrategy() const strategyReorder = new DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy() - const strategyActions = - new DashboardPanelArtworkVersionDesignActionStrategy() + const strategyActions = new DashboardPanelArtworkVersionDesignActionStrategy() return ( { return ( @@ -30,7 +30,7 @@ export const SidebarRight = ({ version, selectedLayer, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren selectedLayer: ILayerWithDesigns | undefined }) => { return ( diff --git a/app/schema/design.ts b/app/schema/design.ts index b5409407..e67ff6ac 100644 --- a/app/schema/design.ts +++ b/app/schema/design.ts @@ -1,5 +1,8 @@ import { z } from 'zod' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { + type IArtworkVersionWithChildren, + type IArtworkVersionWithDesignsAndLayers, +} from '#app/models/artwork-version/artwork-version.server' import { type ILayerWithDesigns } from '#app/models/layer/layer.server' import { type ObjectValues } from '#app/utils/typescript-helpers' import { @@ -29,6 +32,7 @@ export const DesignTypeEnum = { export type designTypeEnum = ObjectValues export type DesignParentType = + | IArtworkVersionWithChildren | IArtworkVersionWithDesignsAndLayers | ILayerWithDesigns From 3dc7f7e1f5be4f1246cbbd66d5b0550990b7687e Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 15:06:24 -0400 Subject: [PATCH 24/54] model layer server cleanup; preparing for assets --- app/models/layer/layer.server.ts | 60 ++++++++------------------------ 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/app/models/layer/layer.server.ts b/app/models/layer/layer.server.ts index d86adbf5..05a96117 100644 --- a/app/models/layer/layer.server.ts +++ b/app/models/layer/layer.server.ts @@ -1,30 +1,19 @@ import { type Layer } from '@prisma/client' -import { - type findLayerArgsType, - type selectArgsType, - type whereArgsType, -} from '#app/schema/layer' +import { type DateOrString } from '#app/definitions/prisma-helper' +import { type findLayerArgsType } from '#app/schema/layer' import { prisma } from '#app/utils/db.server' import { type IArtwork } from '../artwork/artwork.server' import { type IArtworkVersion } from '../artwork-version/artwork-version.server' +import { type IAssetParsed } from '../asset/asset.server' import { type IDesignWithType } from '../design/design.server' -export interface ILayer { - id: string - name: string - description: string | null - slug: string | null - visible: boolean - selected: boolean - createdAt: Date | string - updatedAt: Date | string - ownerId: string - artworkVersionId: string | null - nextId: string | null - prevId: string | null - parentId: string | null - // children: ILayer[] - // designs: IDesignWithType[] +// Omitting 'createdAt' and 'updatedAt' from the Layer interface +// prisma query returns a string for these fields +type BaseLayer = Omit + +export interface ILayer extends BaseLayer { + createdAt: DateOrString + updatedAt: DateOrString } export type ILayerEntityId = IArtwork['id'] | IArtworkVersion['id'] @@ -32,6 +21,11 @@ export interface ILayerWithDesigns extends ILayer { designs: IDesignWithType[] } +export interface ILayerWithChildren extends ILayer { + assets: IAssetParsed[] + designs: IDesignWithType[] +} + export interface ILayerCreateOverrides { name?: string description?: string @@ -39,17 +33,6 @@ export interface ILayerCreateOverrides { visible?: boolean } -export const findManyLayers = async ({ where }: { where: whereArgsType }) => { - const layers = await prisma.layer.findMany({ - where, - // include: { - // designs: true, - // children: true, - // }, - }) - return layers -} - export const findFirstLayer = async ({ where, select, @@ -60,19 +43,6 @@ export const findFirstLayer = async ({ }) } -export const findLayerByIdAndOwner = async ({ - id, - ownerId, - select, -}: { - id: whereArgsType['id'] - ownerId: whereArgsType['ownerId'] - select?: selectArgsType -}): Promise => { - const where = { id, ownerId } - return await findFirstLayer({ where, select }) -} - export const connectPrevAndNextLayers = ({ prevId, nextId, From 9e245bd006211e887176fe0ab5f886881f5bc08b Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 16:04:51 -0400 Subject: [PATCH 25/54] IArtworkVersionWithChildren is more scalable than IArtworkVersionWithDesignsAndLayers; removing asset blob from queries for speed; parsing assets in nested children --- .../artwork-version.get.server.ts | 63 ++++++++++++++++--- .../artwork-version/artwork-version.server.ts | 8 +-- app/models/artwork/artwork.get.server.ts | 5 +- app/models/asset/asset.get.server.ts | 18 ++++++ app/models/design/design.server.ts | 2 - app/routes/_marketing+/index.tsx | 4 +- .../artworks+/$artworkId/_index/route.tsx | 4 +- app/schema/design.ts | 10 +-- .../version/generator/build.service.ts | 18 +++--- 9 files changed, 91 insertions(+), 41 deletions(-) diff --git a/app/models/artwork-version/artwork-version.get.server.ts b/app/models/artwork-version/artwork-version.get.server.ts index 4687a1c6..568acb3a 100644 --- a/app/models/artwork-version/artwork-version.get.server.ts +++ b/app/models/artwork-version/artwork-version.get.server.ts @@ -3,8 +3,8 @@ import { z } from 'zod' import { zodStringOrNull } from '#app/schema/zod-helpers' import { deserializeAssets } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' +import { assetSelect } from '../asset/asset.get.server' import { - type IArtworkVersionWithDesignsAndLayers, type IArtworkVersion, type IArtworkVersionWithChildren, } from './artwork-version.server' @@ -33,13 +33,17 @@ const includeDesigns = { // no ordering for now since these are linked lists const artworkVersionChildren = { - assets: true, + assets: { + select: assetSelect, + }, designs: { include: includeDesigns, }, layers: { include: { - assets: true, + assets: { + select: assetSelect, + }, designs: { include: includeDesigns, }, @@ -107,18 +111,26 @@ export const getArtworkVersionWithChildren = async ({ invariant(artworkVersion, 'Artwork Version not found') const validatedAssets = deserializeAssets({ assets: artworkVersion.assets }) - return { ...artworkVersion, assets: validatedAssets } + const layersWithValidatedAssets = artworkVersion.layers.map(layer => { + const validatedAssets = deserializeAssets({ assets: layer.assets }) + return { ...layer, assets: validatedAssets } + }) + return { + ...artworkVersion, + assets: validatedAssets, + layers: layersWithValidatedAssets, + } } export const getStarredArtworkVersionsByArtworkId = async ({ artworkId, }: { artworkId: string -}): Promise => { +}): Promise => { const starredVersions = await prisma.artworkVersion.findMany({ where: { branch: { - artworkId: artworkId, + artworkId, }, starred: true, }, @@ -130,13 +142,29 @@ export const getStarredArtworkVersionsByArtworkId = async ({ updatedAt: 'desc', }, }) - return starredVersions + + const validatedStarredVersions = starredVersions.map(artworkVersion => { + const validatedArtboardVersionAssets = deserializeAssets({ + assets: artworkVersion.assets, + }) + const layersWithValidatedAssets = artworkVersion.layers.map(layer => { + const validatedLayerAssets = deserializeAssets({ assets: layer.assets }) + return { ...layer, assets: validatedLayerAssets } + }) + return { + ...artworkVersion, + assets: validatedArtboardVersionAssets, + layers: layersWithValidatedAssets, + } + }) + + return validatedStarredVersions } export const getAllPublishedArtworkVersions = async (): Promise< - IArtworkVersionWithDesignsAndLayers[] + IArtworkVersionWithChildren[] > => { - const starredVersions = await prisma.artworkVersion.findMany({ + const publishedVersions = await prisma.artworkVersion.findMany({ where: { published: true, }, @@ -145,5 +173,20 @@ export const getAllPublishedArtworkVersions = async (): Promise< publishedAt: 'desc', }, }) - return starredVersions + + const validatedPublishedVersions = publishedVersions.map(artworkVersion => { + const validatedArtboardVersionAssets = deserializeAssets({ + assets: artworkVersion.assets, + }) + const layersWithValidatedAssets = artworkVersion.layers.map(layer => { + const validatedLayerAssets = deserializeAssets({ assets: layer.assets }) + return { ...layer, assets: validatedLayerAssets } + }) + return { + ...artworkVersion, + assets: validatedArtboardVersionAssets, + layers: layersWithValidatedAssets, + } + }) + return validatedPublishedVersions } diff --git a/app/models/artwork-version/artwork-version.server.ts b/app/models/artwork-version/artwork-version.server.ts index c2af6987..bd58b0b5 100644 --- a/app/models/artwork-version/artwork-version.server.ts +++ b/app/models/artwork-version/artwork-version.server.ts @@ -19,12 +19,6 @@ export interface IArtworkVersion extends BaseArtworkVersion { publishedAt: DateOrString | null } -export interface IArtworkVersionWithDesignsAndLayers extends IArtworkVersion { - designs: IDesignWithType[] - layers: ILayerWithDesigns[] - branch?: IArtworkBranch -} - export interface IArtworkVersionWithChildren extends IArtworkVersion { designs: IDesignWithType[] layers: ILayerWithDesigns[] @@ -41,6 +35,6 @@ export interface IArtworkVersionWithBranch extends IArtworkVersion { } export interface IArtworkVersionWithGenerator - extends IArtworkVersionWithDesignsAndLayers { + extends IArtworkVersionWithChildren { generator: IArtworkVersionGenerator } diff --git a/app/models/artwork/artwork.get.server.ts b/app/models/artwork/artwork.get.server.ts index df74b718..4dd40585 100644 --- a/app/models/artwork/artwork.get.server.ts +++ b/app/models/artwork/artwork.get.server.ts @@ -8,6 +8,7 @@ import { type IArtworkWithBranchesAndVersions, type IArtworkWithAssets, } from '../artwork/artwork.server' +import { assetSelect } from '../asset/asset.get.server' export type queryArtworkWhereArgsType = z.infer const whereArgs = z.object({ @@ -86,7 +87,9 @@ export const getArtworkWithAssets = async ({ const artwork = await prisma.artwork.findFirst({ where, include: { - assets: true, + assets: { + select: assetSelect, + }, }, }) invariant(artwork, 'Artwork not found') diff --git a/app/models/asset/asset.get.server.ts b/app/models/asset/asset.get.server.ts index 9c7b5a29..e66866b4 100644 --- a/app/models/asset/asset.get.server.ts +++ b/app/models/asset/asset.get.server.ts @@ -2,6 +2,24 @@ import { z } from 'zod' import { prisma } from '#app/utils/db.server' import { type IAsset } from './asset.server' +// include all fields except blob +// when including asset in parent queries +export const assetSelect = { + id: true, + name: true, + description: true, + type: true, + attributes: true, + // no blob, too much memory on query + createdAt: true, + updatedAt: true, + ownerId: true, + projectId: true, + artworkId: true, + artworkVersionId: true, + layerId: true, +} + export type queryAssetWhereArgsType = z.infer const whereArgs = z.object({ id: z.string().optional(), diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index c2bcb7c5..f26a3474 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -6,7 +6,6 @@ import { } from '#app/schema/design' import { prisma } from '#app/utils/db.server' import { - type IArtworkVersionWithDesignsAndLayers, type IArtworkVersion, type IArtworkVersionWithChildren, } from '../artwork-version/artwork-version.server' @@ -34,7 +33,6 @@ export type IDesignIdOrNull = IDesign['id'] | null | undefined export type IDesignEntityId = | IDesign['id'] | IArtworkVersion['id'] - | IArtworkVersionWithDesignsAndLayers['id'] | IArtworkVersionWithChildren['id'] export type IDesignEntityIdOrNull = IDesignEntityId | null | undefined diff --git a/app/routes/_marketing+/index.tsx b/app/routes/_marketing+/index.tsx index ca4f1e79..533e53f5 100644 --- a/app/routes/_marketing+/index.tsx +++ b/app/routes/_marketing+/index.tsx @@ -12,7 +12,7 @@ import { import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator.ts' import { getAllPublishedArtworkVersions } from '#app/models/artwork-version/artwork-version.get.server.ts' import { - type IArtworkVersionWithDesignsAndLayers, + type IArtworkVersionWithChildren, type IArtworkVersionWithGenerator, } from '#app/models/artwork-version/artwork-version.server.ts' import { artworkVersionGeneratorBuildService } from '#app/services/artwork/version/generator/build.service.ts' @@ -44,7 +44,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { // get all starred versions // will eventually want to limit by user, but just one for now - const publishedVersions: IArtworkVersionWithDesignsAndLayers[] = + const publishedVersions: IArtworkVersionWithChildren[] = await getAllPublishedArtworkVersions() // get all generators for these versions diff --git a/app/routes/users+/$username_+/artworks+/$artworkId/_index/route.tsx b/app/routes/users+/$username_+/artworks+/$artworkId/_index/route.tsx index 11a6d6d2..ab246912 100644 --- a/app/routes/users+/$username_+/artworks+/$artworkId/_index/route.tsx +++ b/app/routes/users+/$username_+/artworks+/$artworkId/_index/route.tsx @@ -15,7 +15,7 @@ import { getArtworkWithProject } from '#app/models/artwork/artwork.get.server.ts import { getStarredArtworkVersionsByArtworkId } from '#app/models/artwork-version/artwork-version.get.server.ts' import { type IArtworkVersionWithGenerator, - type IArtworkVersionWithDesignsAndLayers, + type IArtworkVersionWithChildren, } from '#app/models/artwork-version/artwork-version.server.ts' import { artworkVersionGeneratorBuildService } from '#app/services/artwork/version/generator/build.service.ts' import { requireUserId } from '#app/utils/auth.server' @@ -39,7 +39,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { invariantResponse(artwork, 'Not found', { status: 404 }) // get all starred versions for this artwork - const starredVersions: IArtworkVersionWithDesignsAndLayers[] = + const starredVersions: IArtworkVersionWithChildren[] = await getStarredArtworkVersionsByArtworkId({ artworkId: artwork.id, }) diff --git a/app/schema/design.ts b/app/schema/design.ts index e67ff6ac..c7b9439e 100644 --- a/app/schema/design.ts +++ b/app/schema/design.ts @@ -1,8 +1,5 @@ import { z } from 'zod' -import { - type IArtworkVersionWithChildren, - type IArtworkVersionWithDesignsAndLayers, -} from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { type ILayerWithDesigns } from '#app/models/layer/layer.server' import { type ObjectValues } from '#app/utils/typescript-helpers' import { @@ -31,10 +28,7 @@ export const DesignTypeEnum = { } as const export type designTypeEnum = ObjectValues -export type DesignParentType = - | IArtworkVersionWithChildren - | IArtworkVersionWithDesignsAndLayers - | ILayerWithDesigns +export type DesignParentType = IArtworkVersionWithChildren | ILayerWithDesigns export const DesignCloneSourceTypeEnum = { ARTWORK_VERSION: 'artworkVersion', diff --git a/app/services/artwork/version/generator/build.service.ts b/app/services/artwork/version/generator/build.service.ts index ce99a11e..914ddd54 100644 --- a/app/services/artwork/version/generator/build.service.ts +++ b/app/services/artwork/version/generator/build.service.ts @@ -6,7 +6,7 @@ import { type IGeneratorWatermark, type IArtworkVersionGeneratorMetadata, } from '#app/definitions/artwork-generator' -import { type IArtworkVersionWithDesignsAndLayers } from '#app/models/artwork-version/artwork-version.server' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { findManyDesignsWithType, type IDesignWithType, @@ -40,7 +40,7 @@ import { isArrayRotateBasisType } from '#app/utils/rotate' export const artworkVersionGeneratorBuildService = async ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }): Promise => { try { // Step 1: verify version selected designs are all present @@ -119,7 +119,7 @@ export const artworkVersionGeneratorBuildService = async ({ const verifyDefaultGeneratorDesigns = async ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }): Promise<{ defaultGeneratorDesigns: IGeneratorDesigns | null message: string @@ -165,7 +165,7 @@ const verifyDefaultGeneratorDesigns = async ({ const getVersionSelectedDesigns = async ({ artworkVersionId, }: { - artworkVersionId: IArtworkVersionWithDesignsAndLayers['id'] + artworkVersionId: IArtworkVersionWithChildren['id'] }): Promise => { return await findManyDesignsWithType({ where: { artworkVersionId, selected: true }, @@ -178,7 +178,7 @@ const buildDefaultGeneratorLayer = async ({ version, defaultGeneratorDesigns, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren defaultGeneratorDesigns: IGeneratorDesigns }): Promise => { const artworkVersionId = version.id @@ -208,7 +208,7 @@ const buildDefaultGeneratorLayer = async ({ const getArtworkVersionContainer = ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }) => { const { width, height } = version return { @@ -319,7 +319,7 @@ const getRotates = async ({ layerId, rotate, }: { - artworkVersionId?: IArtworkVersionWithDesignsAndLayers['id'] + artworkVersionId?: IArtworkVersionWithChildren['id'] layerId?: ILayer['id'] rotate: IRotate }) => { @@ -338,7 +338,7 @@ const getRotates = async ({ const buildGeneratorWatermark = async ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }): Promise => { if (!version.watermark) return null @@ -366,7 +366,7 @@ const buildGeneratorWatermark = async ({ const buildGeneratorMetadata = async ({ version, }: { - version: IArtworkVersionWithDesignsAndLayers + version: IArtworkVersionWithChildren }): Promise => { const branch = await prisma.artworkBranch.findUnique({ where: { id: version.branchId }, From 37dc5f213aa8c3cc740a3cb173a1d70974b39881 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 16:46:06 -0400 Subject: [PATCH 26/54] asset panels started with image --- app/models/asset/asset.server.ts | 7 ++++ app/models/design/design.server.ts | 5 +++ .../sidebars.panel.artwork-version.tsx | 2 +- .../__components/sidebars.panel.assets.tsx | 42 ++++++++----------- .../__components/sidebars.panel.designs.tsx | 9 ++-- app/schema/asset.ts | 8 ++-- app/schema/entity.ts | 5 ++- app/utils/asset.ts | 33 +++++++++++++++ app/utils/design.ts | 6 +-- 9 files changed, 76 insertions(+), 41 deletions(-) diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index 7b4284b4..40dee5ae 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -38,6 +38,13 @@ export interface IAssetParsed extends BaseAsset { } export type IAssetType = IAssetImage +export type IAssetByType = { + assetImages: IAssetImage[] +} +export interface IAssetsByTypeWithType { + type: assetTypeEnum + assets: IAssetType[] +} interface IAssetData { name: string diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index f26a3474..8247fd59 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -3,6 +3,7 @@ import { type selectArgsType, type findDesignArgsType, type whereArgsType, + type designTypeEnum, } from '#app/schema/design' import { prisma } from '#app/utils/db.server' import { @@ -81,6 +82,10 @@ export interface IDesignsByType { designLayouts: IDesignWithLayout[] designTemplates: IDesignWithTemplate[] } +export interface IDesignsByTypeWithType { + type: designTypeEnum + designs: IDesignWithType[] +} export interface IDesignWithPalette extends IDesignWithType { palette: IPalette diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx index a168456d..a4c700ac 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.tsx @@ -15,8 +15,8 @@ export const PanelArtworkVersion = ({ - + ) } diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx index 8bf509a9..d14e23e0 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx @@ -1,13 +1,10 @@ import { DashboardEntityPanel } from '#app/components/templates/panel/dashboard-entity-panel' -import { type IDesignWithType } from '#app/models/design/design.server' -import { type designTypeEnum, type DesignParentType } from '#app/schema/design' +import { type IAssetsByTypeWithType } from '#app/models/asset/asset.server' +import { type AssetParentType } from '#app/schema/asset' import { type IDashboardPanelCreateEntityStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' import { type IDashboardPanelEntityActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' import { type IDashboardPanelUpdateEntityOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' -import { - designsByTypeToPanelArray, - filterAndOrderDesignsByType, -} from '#app/utils/design' +import { assetsByTypeToPanelArray, filterAssetsByType } from '#app/utils/asset' export const PanelAssets = ({ parent, @@ -15,26 +12,26 @@ export const PanelAssets = ({ strategyReorder, strategyActions, }: { - parent: DesignParentType + parent: AssetParentType strategyEntityNew: IDashboardPanelCreateEntityStrategy strategyReorder: IDashboardPanelUpdateEntityOrderStrategy strategyActions: IDashboardPanelEntityActionStrategy }) => { - const orderedDesigns = filterAndOrderDesignsByType({ - designs: parent.designs, + const assetsByType = filterAssetsByType({ + assets: parent.assets, }) - const designTypePanels = designsByTypeToPanelArray({ - designs: orderedDesigns, + const assetTypePanels = assetsByTypeToPanelArray({ + assets: assetsByType, }) return (
- {designTypePanels.map(designTypePanel => { + {assetTypePanels.map(assetTypePanel => { return ( - { - const { type, designs } = designTypePanel + const { type, assets } = assetTypePanel return ( -export type AssetParentType = IProject | IArtwork | IArtworkVersion | ILayer +export type AssetParentType = IArtworkVersionWithChildren | ILayerWithChildren // Dummy schema for demonstration // ! remove this when adding more asset types diff --git a/app/schema/entity.ts b/app/schema/entity.ts index 74cd36d4..23ccb0ea 100644 --- a/app/schema/entity.ts +++ b/app/schema/entity.ts @@ -1,6 +1,7 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetType } from '#app/models/asset/asset.server' import { type IDesignWithType, type IDesign, @@ -16,6 +17,7 @@ import { type ITemplate } from '#app/models/design-type/template/template.server import { type ILayer } from '#app/models/layer/layer.server' import { type IProject } from '#app/models/project/project.server' import { type ObjectValues } from '#app/utils/typescript-helpers' +import { type assetTypeEnum } from './asset' import { type ReorderDesignSchemaType, type NewDesignSchemaType, @@ -45,6 +47,7 @@ export type IEntity = | IRotate | ILayout | ITemplate + | IAssetType export type IEntityVisible = IDesign | IDesignWithType | ILayer export type IEntitySelectable = ILayer @@ -68,7 +71,7 @@ export type IEntityId = | ILayout['id'] | ITemplate['id'] -export type IEntityType = designTypeEnum | 'layer' +export type IEntityType = designTypeEnum | assetTypeEnum | 'layer' export type IEntityParentType = | IDesignWithType diff --git a/app/utils/asset.ts b/app/utils/asset.ts index 605ae28a..b8fa56e7 100644 --- a/app/utils/asset.ts +++ b/app/utils/asset.ts @@ -3,7 +3,10 @@ import { type IAssetParsed, type IAsset, type IAssetType, + type IAssetByType, + type IAssetsByTypeWithType, } from '#app/models/asset/asset.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { AssetTypeEnum, type assetTypeEnum } from '#app/schema/asset' import { parseAssetImageAttributes } from './asset/image' @@ -71,3 +74,33 @@ export const filterAssetType = ({ }): IAssetType[] => { return assets.filter(asset => asset.type === type) } + +export const filterAssetsByType = ({ + assets, +}: { + assets: IAssetParsed[] +}): IAssetByType => { + const assetImages = filterAssetType({ + assets, + type: AssetTypeEnum.IMAGE, + }) as IAssetImage[] + + return { + assetImages, + } +} + +export const assetsByTypeToPanelArray = ({ + assets, +}: { + assets: IAssetByType +}): IAssetsByTypeWithType[] => { + const { assetImages } = assets + + return [ + { + type: AssetTypeEnum.IMAGE, + assets: assetImages, + }, + ] +} diff --git a/app/utils/design.ts b/app/utils/design.ts index e00856ac..236b4ba8 100644 --- a/app/utils/design.ts +++ b/app/utils/design.ts @@ -12,6 +12,7 @@ import { type IDesignsByType, type ISelectedDesigns, type ISelectedDesignsFiltered, + type IDesignsByTypeWithType, } from '#app/models/design/design.server' import { findFirstFillInDesignArray } from '#app/models/design-type/fill/fill.util' import { findFirstLayoutInDesignArray } from '#app/models/design-type/layout/layout.util' @@ -128,10 +129,7 @@ export const designsByTypeToPanelArray = ({ designs, }: { designs: IDesignsByType -}): { - type: designTypeEnum - designs: IDesignWithType[] -}[] => { +}): IDesignsByTypeWithType[] => { const { designPalettes, designSizes, From 7af36d2119cd9ee970e18c1f741c9bdd5e068687 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 17:04:23 -0400 Subject: [PATCH 27/54] preparing add image --- .../panel/dashboard-entity-panel.header.tsx | 2 + .../sidebars.panel.assets.artwork-version.tsx | 4 +- app/schema/entity.ts | 38 +------------------ .../dashboard-panel/create-entity.strategy.ts | 7 ++++ 4 files changed, 12 insertions(+), 39 deletions(-) diff --git a/app/components/templates/panel/dashboard-entity-panel.header.tsx b/app/components/templates/panel/dashboard-entity-panel.header.tsx index 7622c070..2527e1df 100644 --- a/app/components/templates/panel/dashboard-entity-panel.header.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.header.tsx @@ -31,6 +31,8 @@ interface CreateChildEntityFormProps { const ArtworkVersionCreateChildEntityForm = memo( ({ entityType, type, parent }: CreateChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return
+ image
case EntityType.DESIGN: return ( { const strategyEntityNew = - new DashboardPanelCreateArtworkVersionDesignTypeStrategy() + new DashboardPanelCreateArtworkVersionAssetTypeStrategy() const strategyReorder = new DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy() const strategyActions = new DashboardPanelArtworkVersionDesignActionStrategy() diff --git a/app/schema/entity.ts b/app/schema/entity.ts index 23ccb0ea..22ffa131 100644 --- a/app/schema/entity.ts +++ b/app/schema/entity.ts @@ -19,16 +19,12 @@ import { type IProject } from '#app/models/project/project.server' import { type ObjectValues } from '#app/utils/typescript-helpers' import { type assetTypeEnum } from './asset' import { - type ReorderDesignSchemaType, - type NewDesignSchemaType, type designTypeEnum, type ToggleVisibleDesignSchemaType, type DeleteDesignSchemaType, type DesignParentType, } from './design' import { - type ReorderLayerSchemaType, - type NewLayerSchemaType, type ToggleVisibleLayerSchemaType, type DeleteLayerSchemaType, type SelectLayerSchemaType, @@ -81,20 +77,15 @@ export type IEntityParentType = export type IEntityParentId = IDesignWithType['id'] | IArtworkVersion['id'] export const EntityType = { + ASSET: 'asset', DESIGN: 'design', - ARtwork: 'artwork', - ARTWORK_BRANCH: 'artworkBranch', - ARTWORK_VERSION: 'artworkVersion', LAYER: 'layer', // add more parent id types here } as const export type entityTypeEnum = ObjectValues export const EntityParentType = { - PARENT: 'parent', DESIGN: 'design', - ARtwork: 'artwork', - ARTWORK_BRANCH: 'artworkBranch', ARTWORK_VERSION: 'artworkVersion', LAYER: 'layer', // add more parent types here @@ -102,40 +93,13 @@ export const EntityParentType = { export type entityParentTypeEnum = ObjectValues export const EntityParentIdType = { - PARENT_ID: 'parentId', DESIGN_ID: 'designId', - ARTWORK_ID: 'artworkId', - ARTWORK_BRANCH_ID: 'artworkBranchId', ARTWORK_VERSION_ID: 'artworkVersionId', LAYER_ID: 'layerId', // add more parent id types here } as const export type entityParentIdTypeEnum = ObjectValues -export const EntityFormType = { - HEX: 'hex', - TEXT: 'text', - NUMBER: 'number', - ICON: 'icon', - MOVE_ICON: 'move-icon', - SELECT: 'select', - TEXTAREA: 'textarea', - BUTTON: 'button', - MULTIPLE: 'multiple', - // add more form types here -} as const -export type entityFormTypeEnum = ObjectValues - -export type IEntityEnumSelectOption = { - [x: string]: string -} - -export type NewEntitySchemaType = NewDesignSchemaType | NewLayerSchemaType - -export type ReorderEntitySchemaType = - | ReorderDesignSchemaType - | ReorderLayerSchemaType - // actions that go at end of the panel export const EntityActionType = { DELETE: 'delete', diff --git a/app/strategies/component/dashboard-panel/create-entity.strategy.ts b/app/strategies/component/dashboard-panel/create-entity.strategy.ts index 12472286..178b0b75 100644 --- a/app/strategies/component/dashboard-panel/create-entity.strategy.ts +++ b/app/strategies/component/dashboard-panel/create-entity.strategy.ts @@ -10,6 +10,13 @@ export interface IDashboardPanelCreateEntityStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelCreateArtworkVersionAssetTypeStrategy + implements IDashboardPanelCreateEntityStrategy +{ + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelCreateArtworkVersionDesignTypeStrategy implements IDashboardPanelCreateEntityStrategy { From 4d1700175ee507248e95e1b2f92b67ecf3ab09b0 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 17:51:07 -0400 Subject: [PATCH 28/54] useAssetImagesArtwork hook works great for loading data from matches; these images will be selectable from the dialog from artwork to version --- .../panel/dashboard-entity-panel.header.tsx | 6 +- app/models/artwork/hooks.ts | 13 +++ app/models/asset/image/hooks.ts | 14 +++ app/models/asset/image/image.create.server.ts | 2 +- app/models/asset/image/image.get.server.ts | 2 +- app/models/asset/image/image.update.server.ts | 2 +- .../image.ts => models/asset/image/utils.ts} | 0 ...ars.panel.artwork-version.images.image.tsx | 2 +- .../sidebars.panel.artwork-version.images.tsx | 19 +--- .../asset.image.artwork-version.create.tsx | 105 ++++++++++++++++++ app/utils/asset.ts | 2 +- 11 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 app/models/artwork/hooks.ts create mode 100644 app/models/asset/image/hooks.ts rename app/{utils/asset/image.ts => models/asset/image/utils.ts} (100%) create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx diff --git a/app/components/templates/panel/dashboard-entity-panel.header.tsx b/app/components/templates/panel/dashboard-entity-panel.header.tsx index 2527e1df..73165a35 100644 --- a/app/components/templates/panel/dashboard-entity-panel.header.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.header.tsx @@ -1,6 +1,8 @@ import { memo, useCallback } from 'react' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' import { ArtworkVersionDesignCreate } from '#app/routes/resources+/api.v1+/artwork-version.design.create' import { ArtworkVersionLayerCreate } from '#app/routes/resources+/api.v1+/artwork-version.layer.create' +import { AssetImageArtworkVersionCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.create' import { LayerDesignCreate } from '#app/routes/resources+/api.v1+/layer.design.create' import { type designTypeEnum } from '#app/schema/design' import { @@ -32,7 +34,9 @@ const ArtworkVersionCreateChildEntityForm = memo( ({ entityType, type, parent }: CreateChildEntityFormProps) => { switch (entityType) { case EntityType.ASSET: - return
+ image
+ return ( + + ) case EntityType.DESIGN: return ( const whereArgs = z.object({ diff --git a/app/models/asset/image/image.update.server.ts b/app/models/asset/image/image.update.server.ts index 3956a211..9e4992f5 100644 --- a/app/models/asset/image/image.update.server.ts +++ b/app/models/asset/image/image.update.server.ts @@ -2,7 +2,6 @@ import { type IntentActionArgs } from '#app/definitions/intent-action-args' import { type IArtwork } from '#app/models/artwork/artwork.server' import { EditAssetImageArtworkSchema } from '#app/schema/asset/image' import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { stringifyAssetImageAttributes } from '#app/utils/asset/image' import { validateEntityImageSubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' import { type IAssetUpdateData, type IAsset } from '../asset.server' @@ -11,6 +10,7 @@ import { type IAssetAttributesImage, type IAssetImage, } from './image.server' +import { stringifyAssetImageAttributes } from './utils' export interface IAssetImageUpdatedResponse { success: boolean diff --git a/app/utils/asset/image.ts b/app/models/asset/image/utils.ts similarity index 100% rename from app/utils/asset/image.ts rename to app/models/asset/image/utils.ts diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx index 586c3b2e..82500106 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx @@ -15,10 +15,10 @@ import { } from '#app/components/ui/dialog' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { sizeInMB } from '#app/models/asset/image/utils' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' -import { sizeInMB } from '#app/utils/asset/image' import { getArtworkAssetImgSrc } from '#app/utils/misc' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx index 3a491fd7..5c1aad9d 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.tsx @@ -1,4 +1,3 @@ -import { useMatches } from '@remix-run/react' import { memo, useCallback } from 'react' import { ImageSidebar, @@ -11,12 +10,10 @@ import { SidebarPanelRowActionsContainer, } from '#app/components/templates' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' +import { useArtworkFromVersion } from '#app/models/artwork/hooks' +import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' import { type IAssetImage } from '#app/models/asset/image/image.server' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' -import { AssetTypeEnum } from '#app/schema/asset' -import { filterAssetType } from '#app/utils/asset' -import { useRouteLoaderMatchData } from '#app/utils/matches' -import { artworkVersionLoaderRoute } from '../$branchSlug.$versionSlug' import { ImageListItem } from './sidebars.panel.artwork-version.images.image' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { @@ -25,16 +22,8 @@ const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { ImageCreate.displayName = 'ImageCreate' export const PanelArtworkVersionImages = ({}: {}) => { - const matches = useMatches() - const { artwork } = useRouteLoaderMatchData( - matches, - artworkVersionLoaderRoute, - ) - const { assets } = artwork as IArtworkWithAssets - const images: IAssetImage[] = filterAssetType({ - assets, - type: AssetTypeEnum.IMAGE, - }) + const artwork = useArtworkFromVersion() + const images = useAssetImagesArtwork() const imageCreate = useCallback( () => , diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx new file mode 100644 index 00000000..1da0afd1 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx @@ -0,0 +1,105 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' +import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.server' +import { + MAX_UPLOAD_SIZE, + NewAssetImageArtworkSchema, +} from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkCreateService } from '#app/services/asset.image.artwork.create.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK.CREATE +const schema = NewAssetImageArtworkSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateNewAssetImageArtworkSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkCreateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionCreate = ({ + version, +}: { + version: IArtworkVersion +}) => { + const images = useAssetImagesArtwork() + console.log('images', images) + const artworkId = version.id + const formId = `asset-image-artwork-${artworkId}-create` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ +
+
+ ) +} diff --git a/app/utils/asset.ts b/app/utils/asset.ts index b8fa56e7..923a9c36 100644 --- a/app/utils/asset.ts +++ b/app/utils/asset.ts @@ -7,8 +7,8 @@ import { type IAssetsByTypeWithType, } from '#app/models/asset/asset.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { parseAssetImageAttributes } from '#app/models/asset/image/utils' import { AssetTypeEnum, type assetTypeEnum } from '#app/schema/asset' -import { parseAssetImageAttributes } from './asset/image' export const deserializeAssets = ({ assets, From ac5b4d11c8133af7dfc639940e3dc4ca649bb34a Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 19:27:06 -0400 Subject: [PATCH 29/54] splitting apart asset image artwork into separate files -- might make it easier for modularity and separation to make sure things are where they need to be --- .../templates/form/fetcher-image-select.tsx | 220 ++++++++++++++++++ .../image.create.artwork-version.server.ts | 49 ++++ .../image/image.create.artwork.server.ts | 49 ++++ app/models/asset/image/image.create.server.ts | 47 +--- .../image/image.delete.artwork.server.ts | 20 ++ app/models/asset/image/image.delete.server.ts | 20 -- .../image/image.update.artwork.server.ts | 53 +++++ app/models/asset/image/image.update.server.ts | 50 +--- .../asset.image.artwork-version.create.tsx | 8 +- .../api.v1+/asset.image.artwork.create.tsx | 8 +- .../api.v1+/asset.image.artwork.delete.tsx | 4 +- .../api.v1+/asset.image.artwork.update.tsx | 8 +- app/schema/asset/image.artwork.ts | 23 ++ app/schema/asset/image.ts | 18 -- .../asset.image.artwork.create.service.ts | 8 +- .../asset.image.artwork.update.service.ts | 10 +- 16 files changed, 436 insertions(+), 159 deletions(-) create mode 100644 app/components/templates/form/fetcher-image-select.tsx create mode 100644 app/models/asset/image/image.create.artwork-version.server.ts create mode 100644 app/models/asset/image/image.create.artwork.server.ts create mode 100644 app/models/asset/image/image.delete.artwork.server.ts create mode 100644 app/models/asset/image/image.update.artwork.server.ts create mode 100644 app/schema/asset/image.artwork.ts diff --git a/app/components/templates/form/fetcher-image-select.tsx b/app/components/templates/form/fetcher-image-select.tsx new file mode 100644 index 00000000..acf0deb8 --- /dev/null +++ b/app/components/templates/form/fetcher-image-select.tsx @@ -0,0 +1,220 @@ +import { useForm, conform } from '@conform-to/react' +import { getFieldsetConstraint } from '@conform-to/zod' +import { type FetcherWithComponents } from '@remix-run/react' +import { useEffect, useState } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { type z } from 'zod' +import { ErrorList, Field, TextareaField } from '#app/components/forms' +import { + ImagePreview, + ImagePreviewContainer, + ImagePreviewLabel, + ImagePreviewSkeleton, + ImagePreviewWrapper, + ImageUploadInput, + noImagePreviewClassName, +} from '#app/components/image' +import { FlexColumn } from '#app/components/layout' +import { + DialogContentGrid, + DialogFormsContainer, +} from '#app/components/layout/dialog' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '#app/components/ui/dialog' +import { Icon, type IconName } from '#app/components/ui/icon' +import { Label } from '#app/components/ui/label' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { StatusButton } from '#app/components/ui/status-button' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { useIsPending } from '#app/utils/misc' +import { TooltipHydrated } from '../tooltip' + +export const FetcherImageSelect = ({ + fetcher, + route, + schema, + formId, + image, + imgSrc, + icon, + iconText, + tooltipText, + dialogTitle, + dialogDescription, + isHydrated, + children, +}: { + fetcher: FetcherWithComponents + route: string + schema: z.ZodSchema + formId: string + image?: IAssetImage + imgSrc?: string + icon: IconName + iconText: string + tooltipText: string + dialogTitle: string + dialogDescription: string + isHydrated: boolean + children: JSX.Element +}) => { + const [open, setOpen] = useState(false) + const [name, setName] = useState(image?.name ?? '') + const [altText, setAltText] = useState(image?.attributes.altText ?? '') + + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + const [form, fields] = useForm({ + id: formId, + constraint: getFieldsetConstraint(schema), + lastSubmission, + defaultValue: { + id: image?.id ?? '', + name, + description: image?.description ?? '', + altText, + }, + }) + + const [previewImage, setPreviewImage] = useState( + imgSrc ?? null, + ) + + // close after successful submission + useEffect(() => { + if (fetcher.state === 'idle' && fetcher.data?.status === 'success') { + setOpen(false) + } + }, [fetcher]) + + return ( + + + + + + + + + {dialogTitle} + {dialogDescription} + + + + + + {children} + + + + + + + + {previewImage ? ( +
+ +
+ ) : ( + + + + )} + , + ) => { + const file = event.target.files?.[0] + + if (file) { + const reader = new FileReader() + reader.onloadend = () => { + setPreviewImage(reader.result as string) + } + reader.readAsDataURL(file) + setName(file.name) + } else { + setPreviewImage(null) + setName('') + } + }} + accept="image/*" + {...conform.input(fields.file, { + type: 'file', + ariaAttributes: true, + })} + /> +
+
+
+ +
+
+ setName(e.currentTarget.value), + }} + errors={fields.name.errors} + /> + + setAltText(e.currentTarget.value), + }} + errors={fields.altText.errors} + /> +
+
+
+
+ + + Submit + + +
+
+ ) +} diff --git a/app/models/asset/image/image.create.artwork-version.server.ts b/app/models/asset/image/image.create.artwork-version.server.ts new file mode 100644 index 00000000..83287890 --- /dev/null +++ b/app/models/asset/image/image.create.artwork-version.server.ts @@ -0,0 +1,49 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { ValidateArtworkVersionParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IAssetImageCreateData, + type IAssetImageCreateSubmission, +} from './image.create.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateNewAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkVersionParentSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: NewAssetImageArtworkSchema, + strategy, + }) +} + +export interface IAssetImageArtworkCreateSubmission + extends IAssetImageCreateSubmission { + artworkId: IArtwork['id'] +} + +interface IAssetImageArtworkCreateData extends IAssetImageCreateData { + artworkId: IArtwork['id'] +} + +export const createAssetImageArtwork = ({ + data, +}: { + data: IAssetImageArtworkCreateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.create({ + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.create.artwork.server.ts b/app/models/asset/image/image.create.artwork.server.ts new file mode 100644 index 00000000..3e1eb439 --- /dev/null +++ b/app/models/asset/image/image.create.artwork.server.ts @@ -0,0 +1,49 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IAssetImageCreateData, + type IAssetImageCreateSubmission, +} from './image.create.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateNewAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateArtworkParentSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: NewAssetImageArtworkSchema, + strategy, + }) +} + +export interface IAssetImageArtworkCreateSubmission + extends IAssetImageCreateSubmission { + artworkId: IArtwork['id'] +} + +interface IAssetImageArtworkCreateData extends IAssetImageCreateData { + artworkId: IArtwork['id'] +} + +export const createAssetImageArtwork = ({ + data, +}: { + data: IAssetImageArtworkCreateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.create({ + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.create.server.ts b/app/models/asset/image/image.create.server.ts index 5e583518..5cc9c472 100644 --- a/app/models/asset/image/image.create.server.ts +++ b/app/models/asset/image/image.create.server.ts @@ -1,16 +1,9 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { type IArtwork } from '#app/models/artwork/artwork.server' import { type AssetTypeEnum } from '#app/schema/asset' -import { NewAssetImageArtworkSchema } from '#app/schema/asset/image' -import { ValidateArtworkParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntityImageSubmission } from '#app/utils/conform-utils' -import { prisma } from '#app/utils/db.server' import { type IAssetCreateData, type IAsset } from '../asset.server' import { type IAssetImageSubmission, type IAssetAttributesImage, } from './image.server' -import { stringifyAssetImageAttributes } from './utils' export interface IAssetImageCreatedResponse { success: boolean @@ -18,50 +11,12 @@ export interface IAssetImageCreatedResponse { createdAssetImage?: IAsset } -export const validateNewAssetImageArtworkSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateArtworkParentSubmissionStrategy() - - return await validateEntityImageSubmission({ - userId, - formData, - schema: NewAssetImageArtworkSchema, - strategy, - }) -} - export interface IAssetImageCreateSubmission extends IAssetImageSubmission { blob: Buffer } -export interface IAssetImageArtworkCreateSubmission - extends IAssetImageCreateSubmission { - artworkId: IArtwork['id'] -} - -interface IAssetImageCreateData extends IAssetCreateData { +export interface IAssetImageCreateData extends IAssetCreateData { type: typeof AssetTypeEnum.IMAGE attributes: IAssetAttributesImage blob: Buffer } - -interface IAssetImageArtworkCreateData extends IAssetImageCreateData { - artworkId: IArtwork['id'] -} - -export const createAssetImageArtwork = ({ - data, -}: { - data: IAssetImageArtworkCreateData -}) => { - const { attributes, ...rest } = data - const jsonAttributes = stringifyAssetImageAttributes(attributes) - return prisma.asset.create({ - data: { - ...rest, - attributes: jsonAttributes, - }, - }) -} diff --git a/app/models/asset/image/image.delete.artwork.server.ts b/app/models/asset/image/image.delete.artwork.server.ts new file mode 100644 index 00000000..4422a24c --- /dev/null +++ b/app/models/asset/image/image.delete.artwork.server.ts @@ -0,0 +1,20 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' + +export const validateDeleteAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + // not validateEntityImageSubmission + // there is no image file to parse and transform + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteAssetImageArtworkSchema, + strategy, + }) +} diff --git a/app/models/asset/image/image.delete.server.ts b/app/models/asset/image/image.delete.server.ts index eb95f802..39263081 100644 --- a/app/models/asset/image/image.delete.server.ts +++ b/app/models/asset/image/image.delete.server.ts @@ -1,7 +1,3 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image' -import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntitySubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' import { type IAssetImage } from './image.server' @@ -10,22 +6,6 @@ export interface IAssetImageDeletedResponse { message?: string } -export const validateDeleteAssetImageArtworkSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateAssetSubmissionStrategy() - - // not validateEntityImageSubmission - // there is no image file to parse and transform - return await validateEntitySubmission({ - userId, - formData, - schema: DeleteAssetImageArtworkSchema, - strategy, - }) -} - export const deleteAssetImage = ({ id }: { id: IAssetImage['id'] }) => { return prisma.asset.delete({ where: { id }, diff --git a/app/models/asset/image/image.update.artwork.server.ts b/app/models/asset/image/image.update.artwork.server.ts new file mode 100644 index 00000000..bee13a85 --- /dev/null +++ b/app/models/asset/image/image.update.artwork.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtwork } from '#app/models/artwork/artwork.server' +import { EditAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntityImageSubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' +import { + type IAssetImageUpdateData, + type IAssetImageUpdateSubmission, +} from './image.update.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditAssetImageArtworkSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: EditAssetImageArtworkSchema, + strategy, + }) +} + +export interface IAssetImageArtworkUpdateSubmission + extends IAssetImageUpdateSubmission { + artworkId: IArtwork['id'] +} + +interface IAssetImageArtworkUpdateData extends IAssetImageUpdateData { + artworkId: IArtwork['id'] +} + +export const updateAssetImageArtwork = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageArtworkUpdateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.update.server.ts b/app/models/asset/image/image.update.server.ts index 9e4992f5..73bc229a 100644 --- a/app/models/asset/image/image.update.server.ts +++ b/app/models/asset/image/image.update.server.ts @@ -1,16 +1,9 @@ -import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { EditAssetImageArtworkSchema } from '#app/schema/asset/image' -import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' -import { validateEntityImageSubmission } from '#app/utils/conform-utils' -import { prisma } from '#app/utils/db.server' import { type IAssetUpdateData, type IAsset } from '../asset.server' import { type IAssetImageSubmission, type IAssetAttributesImage, type IAssetImage, } from './image.server' -import { stringifyAssetImageAttributes } from './utils' export interface IAssetImageUpdatedResponse { success: boolean @@ -18,52 +11,11 @@ export interface IAssetImageUpdatedResponse { updatedAssetImage?: IAsset } -export const validateEditAssetImageArtworkSubmission = async ({ - userId, - formData, -}: IntentActionArgs) => { - const strategy = new ValidateAssetSubmissionStrategy() - - return await validateEntityImageSubmission({ - userId, - formData, - schema: EditAssetImageArtworkSchema, - strategy, - }) -} - export interface IAssetImageUpdateSubmission extends IAssetImageSubmission { id: IAssetImage['id'] blob?: Buffer } -export interface IAssetImageArtworkUpdateSubmission - extends IAssetImageUpdateSubmission { - artworkId: IArtwork['id'] -} - -interface IAssetImageUpdateData extends IAssetUpdateData { +export interface IAssetImageUpdateData extends IAssetUpdateData { attributes: IAssetAttributesImage } - -interface IAssetImageArtworkUpdateData extends IAssetImageUpdateData { - artworkId: IArtwork['id'] -} - -export const updateAssetImageArtwork = ({ - id, - data, -}: { - id: IAssetImage['id'] - data: IAssetImageArtworkUpdateData -}) => { - const { attributes, ...rest } = data - const jsonAttributes = stringifyAssetImageAttributes(attributes) - return prisma.asset.update({ - where: { id }, - data: { - ...rest, - attributes: jsonAttributes, - }, - }) -} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx index 1da0afd1..3639d6f5 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx @@ -11,11 +11,9 @@ import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' -import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.server' -import { - MAX_UPLOAD_SIZE, - NewAssetImageArtworkSchema, -} from '#app/schema/asset/image' +import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.artwork.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkCreateService } from '#app/services/asset.image.artwork.create.service' import { requireUserId } from '#app/utils/auth.server' diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx index bd20bccd..466ba135 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.create.tsx @@ -10,11 +10,9 @@ import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtwork } from '#app/models/artwork/artwork.server' -import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.server' -import { - MAX_UPLOAD_SIZE, - NewAssetImageArtworkSchema, -} from '#app/schema/asset/image' +import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.artwork.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkCreateService } from '#app/services/asset.image.artwork.create.service' import { requireUserId } from '#app/utils/auth.server' diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx index 5cd07fba..09b6630e 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.delete.tsx @@ -8,9 +8,9 @@ import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' import { type IArtwork } from '#app/models/artwork/artwork.server' -import { validateDeleteAssetImageArtworkSubmission } from '#app/models/asset/image/image.delete.server' +import { validateDeleteAssetImageArtworkSubmission } from '#app/models/asset/image/image.delete.artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image' +import { DeleteAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkDeleteService } from '#app/services/asset.image.artwork.delete.service' import { requireUserId } from '#app/utils/auth.server' diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx index 77e0eb32..0398bded 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx @@ -11,11 +11,9 @@ import { useHydrated } from 'remix-utils/use-hydrated' import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { validateEditAssetImageArtworkSubmission } from '#app/models/asset/image/image.update.server' -import { - EditAssetImageArtworkSchema, - MAX_UPLOAD_SIZE, -} from '#app/schema/asset/image' +import { validateEditAssetImageArtworkSubmission } from '#app/models/asset/image/image.update.artwork.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { EditAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkUpdateService } from '#app/services/asset.image.artwork.update.service' import { requireUserId } from '#app/utils/auth.server' diff --git a/app/schema/asset/image.artwork.ts b/app/schema/asset/image.artwork.ts new file mode 100644 index 00000000..b122e437 --- /dev/null +++ b/app/schema/asset/image.artwork.ts @@ -0,0 +1,23 @@ +import { z } from 'zod' +import { + AssetImageDataSchema, + DeleteAssetImageSchema, + EditAssetImageSchema, + NewAssetImageSchema, +} from './image' + +const ArtworkParentSchema = z.object({ + artworkId: z.string(), +}) + +export const AssetImageArtworkDataSchema = + AssetImageDataSchema.merge(ArtworkParentSchema) + +export const NewAssetImageArtworkSchema = + NewAssetImageSchema.merge(ArtworkParentSchema) + +export const EditAssetImageArtworkSchema = + EditAssetImageSchema.merge(ArtworkParentSchema) + +export const DeleteAssetImageArtworkSchema = + DeleteAssetImageSchema.merge(ArtworkParentSchema) diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index bfc0cbe7..7867cce5 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -74,21 +74,3 @@ export const EditAssetImageSchema = z.object({ export const DeleteAssetImageSchema = z.object({ id: z.string(), }) - -// parent is artwork - -const ArtworkParentSchema = z.object({ - artworkId: z.string(), -}) - -export const AssetImageArtworkDataSchema = - AssetImageDataSchema.merge(ArtworkParentSchema) - -export const NewAssetImageArtworkSchema = - NewAssetImageSchema.merge(ArtworkParentSchema) - -export const EditAssetImageArtworkSchema = - EditAssetImageSchema.merge(ArtworkParentSchema) - -export const DeleteAssetImageArtworkSchema = - DeleteAssetImageSchema.merge(ArtworkParentSchema) diff --git a/app/services/asset.image.artwork.create.service.ts b/app/services/asset.image.artwork.create.service.ts index c110919f..8821a9d9 100644 --- a/app/services/asset.image.artwork.create.service.ts +++ b/app/services/asset.image.artwork.create.service.ts @@ -1,12 +1,12 @@ import { invariant } from '@epic-web/invariant' import { getArtwork } from '#app/models/artwork/artwork.get.server' import { - type IAssetImageCreatedResponse, - createAssetImageArtwork, type IAssetImageArtworkCreateSubmission, -} from '#app/models/asset/image/image.create.server' + createAssetImageArtwork, +} from '#app/models/asset/image/image.create.artwork.server' +import { type IAssetImageCreatedResponse } from '#app/models/asset/image/image.create.server' import { AssetTypeEnum } from '#app/schema/asset' -import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' +import { AssetImageArtworkDataSchema } from '#app/schema/asset/image.artwork' import { prisma } from '#app/utils/db.server' export const assetImageArtworkCreateService = async ({ diff --git a/app/services/asset.image.artwork.update.service.ts b/app/services/asset.image.artwork.update.service.ts index 6dbaa39e..05b45e10 100644 --- a/app/services/asset.image.artwork.update.service.ts +++ b/app/services/asset.image.artwork.update.service.ts @@ -1,14 +1,14 @@ import { invariant } from '@epic-web/invariant' -import { createAssetImageArtwork } from '#app/models/asset/image/image.create.server' +import { createAssetImageArtwork } from '#app/models/asset/image/image.create.artwork.server' import { deleteAssetImage } from '#app/models/asset/image/image.delete.server' import { getAssetImage } from '#app/models/asset/image/image.get.server' import { - type IAssetImageUpdatedResponse, - updateAssetImageArtwork, type IAssetImageArtworkUpdateSubmission, -} from '#app/models/asset/image/image.update.server' + updateAssetImageArtwork, +} from '#app/models/asset/image/image.update.artwork.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' import { AssetTypeEnum } from '#app/schema/asset' -import { AssetImageArtworkDataSchema } from '#app/schema/asset/image' +import { AssetImageArtworkDataSchema } from '#app/schema/asset/image.artwork' import { prisma } from '#app/utils/db.server' export const assetImageArtworkUpdateService = async ({ From ba595e765e3930d9a5b00bc11224f4d5c0da035a Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Wed, 12 Jun 2024 20:54:40 -0400 Subject: [PATCH 30/54] displaying artwork asset images via strategy after dialog opens --- .../templates/form/fetcher-image-select.tsx | 157 +++++++----------- app/models/asset/asset.server.ts | 8 + .../image.create.artwork-version.server.ts | 20 +-- .../__components/sidebars.panel.assets.tsx | 10 +- .../asset.image.artwork-version.create.tsx | 45 ++--- app/schema/asset.ts | 11 +- app/schema/asset/image.artwork-version.ts | 26 +++ app/schema/entity.ts | 6 +- ...et.image.artwork-version.create.service.ts | 75 +++++++++ app/strategies/asset.image.src.strategy.ts | 17 ++ app/utils/routes.const.ts | 5 + 11 files changed, 242 insertions(+), 138 deletions(-) create mode 100644 app/schema/asset/image.artwork-version.ts create mode 100644 app/services/asset.image.artwork-version.create.service.ts create mode 100644 app/strategies/asset.image.src.strategy.ts diff --git a/app/components/templates/form/fetcher-image-select.tsx b/app/components/templates/form/fetcher-image-select.tsx index acf0deb8..2ac7494c 100644 --- a/app/components/templates/form/fetcher-image-select.tsx +++ b/app/components/templates/form/fetcher-image-select.tsx @@ -1,18 +1,15 @@ -import { useForm, conform } from '@conform-to/react' +import { conform, useForm } from '@conform-to/react' import { getFieldsetConstraint } from '@conform-to/zod' import { type FetcherWithComponents } from '@remix-run/react' import { useEffect, useState } from 'react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' import { type z } from 'zod' -import { ErrorList, Field, TextareaField } from '#app/components/forms' import { ImagePreview, ImagePreviewContainer, ImagePreviewLabel, - ImagePreviewSkeleton, ImagePreviewWrapper, ImageUploadInput, - noImagePreviewClassName, } from '#app/components/image' import { FlexColumn } from '#app/components/layout' import { @@ -28,11 +25,13 @@ import { DialogTitle, DialogTrigger, } from '#app/components/ui/dialog' -import { Icon, type IconName } from '#app/components/ui/icon' +import { type IconName } from '#app/components/ui/icon' import { Label } from '#app/components/ui/label' import { PanelIconButton } from '#app/components/ui/panel-icon-button' import { StatusButton } from '#app/components/ui/status-button' +import { type IAssetParent } from '#app/models/asset/asset.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type IAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' import { useIsPending } from '#app/utils/misc' import { TooltipHydrated } from '../tooltip' @@ -41,8 +40,9 @@ export const FetcherImageSelect = ({ route, schema, formId, - image, - imgSrc, + images, + parent, + strategy, icon, iconText, tooltipText, @@ -55,8 +55,9 @@ export const FetcherImageSelect = ({ route: string schema: z.ZodSchema formId: string - image?: IAssetImage - imgSrc?: string + images: IAssetImage[] + parent: IAssetParent + strategy: IAssetImageSrcStrategy icon: IconName iconText: string tooltipText: string @@ -66,8 +67,6 @@ export const FetcherImageSelect = ({ children: JSX.Element }) => { const [open, setOpen] = useState(false) - const [name, setName] = useState(image?.name ?? '') - const [altText, setAltText] = useState(image?.attributes.altText ?? '') const lastSubmission = fetcher.data?.submission const isPending = useIsPending() @@ -75,18 +74,15 @@ export const FetcherImageSelect = ({ id: formId, constraint: getFieldsetConstraint(schema), lastSubmission, - defaultValue: { - id: image?.id ?? '', - name, - description: image?.description ?? '', - altText, + onSubmit: async (event, { formData }) => { + event.preventDefault() + fetcher.submit(formData, { + method: 'POST', + action: route, + }) }, }) - const [previewImage, setPreviewImage] = useState( - imgSrc ?? null, - ) - // close after successful submission useEffect(() => { if (fetcher.state === 'idle' && fetcher.data?.status === 'success') { @@ -118,89 +114,48 @@ export const FetcherImageSelect = ({ {children} - - - - - - {previewImage ? ( -
- -
- ) : ( - - - - )} - , - ) => { - const file = event.target.files?.[0] + + {images.map(image => { + const { id, name, description, attributes } = image + const { altText } = attributes + const imgSrc = strategy.getAssetSrc({ + parentId: parent.id, + assetId: id, + }) - if (file) { - const reader = new FileReader() - reader.onloadend = () => { - setPreviewImage(reader.result as string) - } - reader.readAsDataURL(file) - setName(file.name) - } else { - setPreviewImage(null) - setName('') - } - }} - accept="image/*" - {...conform.input(fields.file, { - type: 'file', - ariaAttributes: true, - })} - /> -
-
-
- + + + +
+ +
+
+
+
+ + + -
-
- setName(e.currentTarget.value), - }} - errors={fields.name.errors} - /> - - setAltText(e.currentTarget.value), - }} - errors={fields.altText.errors} - /> -
+ + ) + })}
diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index 40dee5ae..c7ed1666 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -1,6 +1,9 @@ import { type Asset } from '@prisma/client' import { type DateOrString } from '#app/definitions/prisma-helper' import { type assetTypeEnum } from '#app/schema/asset' +import { type IArtworkWithAssets } from '../artwork/artwork.server' +import { type IArtworkVersionWithChildren } from '../artwork-version/artwork-version.server' +import { type ILayerWithChildren } from '../layer/layer.server' import { type IUser } from '../user/user.server' import { type IAssetImage, @@ -46,6 +49,11 @@ export interface IAssetsByTypeWithType { assets: IAssetType[] } +export type IAssetParent = + | IArtworkWithAssets + | IArtworkVersionWithChildren + | ILayerWithChildren + interface IAssetData { name: string description?: string diff --git a/app/models/asset/image/image.create.artwork-version.server.ts b/app/models/asset/image/image.create.artwork-version.server.ts index 83287890..cc079481 100644 --- a/app/models/asset/image/image.create.artwork-version.server.ts +++ b/app/models/asset/image/image.create.artwork-version.server.ts @@ -1,6 +1,6 @@ import { type IntentActionArgs } from '#app/definitions/intent-action-args' -import { type IArtwork } from '#app/models/artwork/artwork.server' -import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { NewAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' import { ValidateArtworkVersionParentSubmissionStrategy } from '#app/strategies/validate-submission.strategy' import { validateEntityImageSubmission } from '#app/utils/conform-utils' import { prisma } from '#app/utils/db.server' @@ -10,7 +10,7 @@ import { } from './image.create.server' import { stringifyAssetImageAttributes } from './utils' -export const validateNewAssetImageArtworkSubmission = async ({ +export const validateNewAssetImageArtworkVersionSubmission = async ({ userId, formData, }: IntentActionArgs) => { @@ -19,24 +19,24 @@ export const validateNewAssetImageArtworkSubmission = async ({ return await validateEntityImageSubmission({ userId, formData, - schema: NewAssetImageArtworkSchema, + schema: NewAssetImageArtworkVersionSchema, strategy, }) } -export interface IAssetImageArtworkCreateSubmission +export interface IAssetImageArtworkVersionCreateSubmission extends IAssetImageCreateSubmission { - artworkId: IArtwork['id'] + artworkVersionId: IArtworkVersion['id'] } -interface IAssetImageArtworkCreateData extends IAssetImageCreateData { - artworkId: IArtwork['id'] +interface IAssetImageArtworkVersionCreateData extends IAssetImageCreateData { + artworkVersionId: IArtworkVersion['id'] } -export const createAssetImageArtwork = ({ +export const createAssetImageArtworkVersion = ({ data, }: { - data: IAssetImageArtworkCreateData + data: IAssetImageArtworkVersionCreateData }) => { const { attributes, ...rest } = data const jsonAttributes = stringifyAssetImageAttributes(attributes) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx index d14e23e0..275ec420 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx @@ -1,6 +1,8 @@ import { DashboardEntityPanel } from '#app/components/templates/panel/dashboard-entity-panel' -import { type IAssetsByTypeWithType } from '#app/models/asset/asset.server' -import { type AssetParentType } from '#app/schema/asset' +import { + type IAssetParent, + type IAssetsByTypeWithType, +} from '#app/models/asset/asset.server' import { type IDashboardPanelCreateEntityStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' import { type IDashboardPanelEntityActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' import { type IDashboardPanelUpdateEntityOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' @@ -12,7 +14,7 @@ export const PanelAssets = ({ strategyReorder, strategyActions, }: { - parent: AssetParentType + parent: IAssetParent strategyEntityNew: IDashboardPanelCreateEntityStrategy strategyReorder: IDashboardPanelUpdateEntityOrderStrategy strategyActions: IDashboardPanelEntityActionStrategy @@ -49,7 +51,7 @@ export const PanelAsset = ({ strategyReorder, strategyActions, }: { - parent: AssetParentType + parent: IAssetParent assetTypePanel: IAssetsByTypeWithType strategyEntityNew: IDashboardPanelCreateEntityStrategy strategyReorder: IDashboardPanelUpdateEntityOrderStrategy diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx index 3639d6f5..02f0ea48 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx @@ -8,21 +8,23 @@ import { import { useFetcher } from '@remix-run/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { FetcherImageSelect } from '#app/components/templates/form/fetcher-image-select' +import { useArtworkFromVersion } from '#app/models/artwork/hooks' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' -import { validateNewAssetImageArtworkSubmission } from '#app/models/asset/image/image.create.artwork.server' +import { validateNewAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.create.artwork-version.server' import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' -import { NewAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' +import { NewAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' import { validateNoJS } from '#app/schema/form-data' -import { assetImageArtworkCreateService } from '#app/services/asset.image.artwork.create.service' +import { assetImageArtworkVersionCreateService } from '#app/services/asset.image.artwork-version.create.service' +import { ArtworkAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' import { requireUserId } from '#app/utils/auth.server' import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components -const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK.CREATE -const schema = NewAssetImageArtworkSchema +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.CREATE +const schema = NewAssetImageArtworkVersionSchema // auth GET request to endpoint export async function loader({ request }: LoaderFunctionArgs) { @@ -40,13 +42,14 @@ export async function action({ request }: ActionFunctionArgs) { let createSuccess = false let errorMessage = '' - const { status, submission } = await validateNewAssetImageArtworkSubmission({ - userId, - formData, - }) + const { status, submission } = + await validateNewAssetImageArtworkVersionSubmission({ + userId, + formData, + }) if (status === 'success') { - const { success, message } = await assetImageArtworkCreateService({ + const { success, message } = await assetImageArtworkVersionCreateService({ userId, ...submission.value, }) @@ -74,30 +77,34 @@ export const AssetImageArtworkVersionCreate = ({ }: { version: IArtworkVersion }) => { + const artworkVersionId = version.id const images = useAssetImagesArtwork() - console.log('images', images) - const artworkId = version.id - const formId = `asset-image-artwork-${artworkId}-create` + const artwork = useArtworkFromVersion() + const strategy = new ArtworkAssetImageSrcStrategy() + const formId = `asset-image-artwork-${artworkVersionId}-create` const fetcher = useFetcher() let isHydrated = useHydrated() return ( -
- +
-
+ ) } diff --git a/app/schema/asset.ts b/app/schema/asset.ts index 2eebb628..bd30d8a3 100644 --- a/app/schema/asset.ts +++ b/app/schema/asset.ts @@ -1,6 +1,4 @@ import { z } from 'zod' -import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' -import { type ILayerWithChildren } from '#app/models/layer/layer.server' import { type ObjectValues } from '#app/utils/typescript-helpers' import { AssetAttributesImageSchema } from './asset/image' @@ -10,7 +8,14 @@ export const AssetTypeEnum = { } as const export type assetTypeEnum = ObjectValues -export type AssetParentType = IArtworkVersionWithChildren | ILayerWithChildren +export const AssetParentTypeEnum = { + PROJECT: 'project', + ARTWORK: 'artwork', + ARTWORK_VERSION: 'artworkVersion', + LAYER: 'layer', + // add more asset parent types here +} as const +export type assetParentTypeEnum = ObjectValues // Dummy schema for demonstration // ! remove this when adding more asset types diff --git a/app/schema/asset/image.artwork-version.ts b/app/schema/asset/image.artwork-version.ts new file mode 100644 index 00000000..07f6a539 --- /dev/null +++ b/app/schema/asset/image.artwork-version.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' +import { + AssetImageDataSchema, + DeleteAssetImageSchema, + EditAssetImageSchema, + NewAssetImageSchema, +} from './image' + +const ArtworkVersionParentSchema = z.object({ + artworkVersionId: z.string(), +}) + +export const AssetImageArtworkVersionDataSchema = AssetImageDataSchema.merge( + ArtworkVersionParentSchema, +) + +export const NewAssetImageArtworkVersionSchema = NewAssetImageSchema.merge( + ArtworkVersionParentSchema, +) + +export const EditAssetImageArtworkVersionSchema = EditAssetImageSchema.merge( + ArtworkVersionParentSchema, +) + +export const DeleteAssetImageArtworkVersionSchema = + DeleteAssetImageSchema.merge(ArtworkVersionParentSchema) diff --git a/app/schema/entity.ts b/app/schema/entity.ts index 22ffa131..af31ea12 100644 --- a/app/schema/entity.ts +++ b/app/schema/entity.ts @@ -1,7 +1,10 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' -import { type IAssetType } from '#app/models/asset/asset.server' +import { + type IAssetParent, + type IAssetType, +} from '#app/models/asset/asset.server' import { type IDesignWithType, type IDesign, @@ -70,6 +73,7 @@ export type IEntityId = export type IEntityType = designTypeEnum | assetTypeEnum | 'layer' export type IEntityParentType = + | IAssetParent | IDesignWithType | IArtworkVersion | DesignParentType diff --git a/app/services/asset.image.artwork-version.create.service.ts b/app/services/asset.image.artwork-version.create.service.ts new file mode 100644 index 00000000..2faf09ff --- /dev/null +++ b/app/services/asset.image.artwork-version.create.service.ts @@ -0,0 +1,75 @@ +import { invariant } from '@epic-web/invariant' +import { getArtworkVersion } from '#app/models/artwork-version/artwork-version.get.server' +import { + type IAssetImageArtworkVersionCreateSubmission, + createAssetImageArtworkVersion, +} from '#app/models/asset/image/image.create.artwork-version.server' +import { type IAssetImageCreatedResponse } from '#app/models/asset/image/image.create.server' +import { AssetTypeEnum } from '#app/schema/asset' +import { AssetImageArtworkVersionDataSchema } from '#app/schema/asset/image.artwork-version' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkVersionCreateService = async ({ + userId, + artworkVersionId, + name, + description, + blob, + altText, + contentType, + height, + width, + size, + lastModified, + filename, +}: IAssetImageArtworkVersionCreateSubmission): Promise => { + try { + // Step 1: verify the artworkVersion exists + const artworkVersion = await getArtworkVersion({ + where: { id: artworkVersionId, ownerId: userId }, + }) + invariant(artworkVersion, 'Artwork not found') + + // Step 2: validate asset image data + // zod schema for blob Buffer/File is not working + // pass in separately from validation + const data = { + name, + description, + type: AssetTypeEnum.IMAGE, + attributes: { + altText: altText || 'No alt text provided.', + contentType, + height, + width, + size, + lastModified, + filename, + }, + ownerId: userId, + artworkVersionId, + } + const assetImageData = AssetImageArtworkVersionDataSchema.parse(data) + + // Step 3: create the asset image via promise + const createAssetImagePromise = createAssetImageArtworkVersion({ + data: { ...assetImageData, blob }, + }) + + // Step 4: execute the transaction + const [createdAssetImage] = await prisma.$transaction([ + createAssetImagePromise, + ]) + + return { + createdAssetImage, + success: true, + } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkVersionCreateService', + } + } +} diff --git a/app/strategies/asset.image.src.strategy.ts b/app/strategies/asset.image.src.strategy.ts new file mode 100644 index 00000000..858cc565 --- /dev/null +++ b/app/strategies/asset.image.src.strategy.ts @@ -0,0 +1,17 @@ +import { getArtworkAssetImgSrc } from '#app/utils/misc' + +export interface IAssetImageSrcStrategy { + getAssetSrc(args: { assetId: string; parentId: string }): string +} + +export class ArtworkAssetImageSrcStrategy implements IAssetImageSrcStrategy { + getAssetSrc({ + assetId, + parentId, + }: { + assetId: string + parentId: string + }): string { + return getArtworkAssetImgSrc({ artworkId: parentId, imageId: assetId }) + } +} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 83c2a091..b7178ee8 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -43,6 +43,11 @@ export const Routes = { DELETE: `${pathBase}/asset/image/artwork/delete`, UPDATE: `${pathBase}/asset/image/artwork/update`, }, + ARTWORK_VERSION: { + CREATE: `${pathBase}/asset/image/artwork-version/create`, + DELETE: `${pathBase}/asset/image/artwork-version/delete`, + UPDATE: `${pathBase}/asset/image/artwork-version/update`, + }, }, }, DESIGN: { From d6ffdaea01f20b57822b8bc4d494e525632fa194 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Fri, 14 Jun 2024 14:50:26 -0400 Subject: [PATCH 31/54] asset image pannel can delete, toggle visible, name as main form (disabled), popover with name field --- .../templates/form/fetcher-image-select.tsx | 134 ++++++++++++------ .../templates/form/fetcher-text.tsx | 115 +++++++++++++++ .../dashboard-entity-panel.actions.delete.tsx | 10 ++ ...rd-entity-panel.actions.toggle-visible.tsx | 9 ++ .../panel/dashboard-entity-panel.tsx | 20 ++- ...hboard-entity-panel.values.asset.image.tsx | 58 ++++++++ .../panel/dashboard-entity-panel.values.tsx | 5 + app/components/ui/radio-group.tsx | 43 ++++++ app/models/asset/asset.get.server.ts | 1 + app/models/asset/asset.server.ts | 1 + .../image.delete.artwork-version.server.ts | 20 +++ app/models/asset/image/image.get.server.ts | 1 + .../image.update.artwork-version.server.ts | 73 ++++++++++ ...e.update.artwork-version.visible.server.ts | 45 ++++++ .../sidebars.panel.assets.artwork-version.tsx | 8 +- .../__components/sidebars.panel.assets.tsx | 1 + .../asset.image.artwork-version.clone.tsx | 115 +++++++++++++++ .../asset.image.artwork-version.create.tsx | 21 +-- .../asset.image.artwork-version.delete.tsx | 101 +++++++++++++ ...t.image.artwork-version.update.visible.tsx | 102 +++++++++++++ .../resources+/api.v1+/asset.update.name.tsx | 108 ++++++++++++++ app/schema/asset/__shared.ts | 7 + app/schema/asset/image.artwork-version.ts | 14 ++ app/schema/asset/image.ts | 19 +-- app/schema/entity.ts | 2 +- ...et.image.artwork-version.delete.service.ts | 31 ++++ ....artwork-version.update.visible.service.ts | 51 +++++++ .../dashboard-panel/delete-entity.strategy.ts | 8 ++ .../entity-action/entity-action.ts | 18 ++- .../update-entity-order.strategy.ts | 7 + .../update-entity-visible.strategy.ts | 8 ++ app/utils/routes.const.ts | 2 + package-lock.json | 51 +++++++ package.json | 1 + .../migration.sql | 30 ++++ prisma/schema.prisma | 1 + 36 files changed, 1156 insertions(+), 85 deletions(-) create mode 100644 app/components/templates/form/fetcher-text.tsx create mode 100644 app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx create mode 100644 app/components/ui/radio-group.tsx create mode 100644 app/models/asset/image/image.delete.artwork-version.server.ts create mode 100644 app/models/asset/image/image.update.artwork-version.server.ts create mode 100644 app/models/asset/image/image.update.artwork-version.visible.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx create mode 100644 app/routes/resources+/api.v1+/asset.update.name.tsx create mode 100644 app/services/asset.image.artwork-version.delete.service.ts create mode 100644 app/services/asset.image.artwork-version.update.visible.service.ts create mode 100644 prisma/migrations/20240613070911_add_visible_to_asset/migration.sql diff --git a/app/components/templates/form/fetcher-image-select.tsx b/app/components/templates/form/fetcher-image-select.tsx index 2ac7494c..21c1dd8d 100644 --- a/app/components/templates/form/fetcher-image-select.tsx +++ b/app/components/templates/form/fetcher-image-select.tsx @@ -1,5 +1,5 @@ import { conform, useForm } from '@conform-to/react' -import { getFieldsetConstraint } from '@conform-to/zod' +import { getFieldsetConstraint, parse } from '@conform-to/zod' import { type FetcherWithComponents } from '@remix-run/react' import { useEffect, useState } from 'react' import { AuthenticityTokenInput } from 'remix-utils/csrf/react' @@ -9,9 +9,8 @@ import { ImagePreviewContainer, ImagePreviewLabel, ImagePreviewWrapper, - ImageUploadInput, } from '#app/components/image' -import { FlexColumn } from '#app/components/layout' +import { FlexColumn, FlexRow } from '#app/components/layout' import { DialogContentGrid, DialogFormsContainer, @@ -26,13 +25,14 @@ import { DialogTrigger, } from '#app/components/ui/dialog' import { type IconName } from '#app/components/ui/icon' -import { Label } from '#app/components/ui/label' +import { Input } from '#app/components/ui/input' import { PanelIconButton } from '#app/components/ui/panel-icon-button' import { StatusButton } from '#app/components/ui/status-button' import { type IAssetParent } from '#app/models/asset/asset.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { sizeInMB } from '#app/models/asset/image/utils' import { type IAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' -import { useIsPending } from '#app/utils/misc' +import { cn, useIsPending } from '#app/utils/misc' import { TooltipHydrated } from '../tooltip' export const FetcherImageSelect = ({ @@ -67,13 +67,24 @@ export const FetcherImageSelect = ({ children: JSX.Element }) => { const [open, setOpen] = useState(false) + const [selectedImageId, setSelectedImageId] = useState< + IAssetImage['id'] | null + >(null) const lastSubmission = fetcher.data?.submission const isPending = useIsPending() - const [form, fields] = useForm({ + const [form, { assetImageId }] = useForm({ id: formId, constraint: getFieldsetConstraint(schema), lastSubmission, + shouldValidate: 'onInput', + onValidate({ formData }) { + const parsed = parse(formData, { schema }) + setSelectedImageId( + (parsed.payload.assetImageId as IAssetImage['id']) ?? null, + ) + return parsed + }, onSubmit: async (event, { formData }) => { event.preventDefault() fetcher.submit(formData, { @@ -114,48 +125,75 @@ export const FetcherImageSelect = ({ {children} - - {images.map(image => { - const { id, name, description, attributes } = image - const { altText } = attributes - const imgSrc = strategy.getAssetSrc({ - parentId: parent.id, - assetId: id, - }) +
+ + {conform + .collection(assetImageId, { + type: 'radio', + options: images.map(image => image.id), + }) + .map((props, index) => { + const image = images.find( + image => image.id === props.value, + ) as IAssetImage + const isSelectedImage = selectedImageId === image.id + + const { id, name, attributes } = image + const { altText, height, width, size } = attributes + const imgSrc = strategy.getAssetSrc({ + parentId: parent.id, + assetId: id, + }) - return ( - - - - -
- -
-
-
-
- - - -
- ) - })} + return ( + + + + +
+
+ +
+
+ +
+
+
+ + + + + {name} + + + {width}x{height} + + +
{sizeInMB(size)} MB
+
+
+
+
+
+ ) + })} +
+
@@ -163,6 +201,8 @@ export const FetcherImageSelect = ({ diff --git a/app/components/templates/form/fetcher-text.tsx b/app/components/templates/form/fetcher-text.tsx new file mode 100644 index 00000000..b958aa95 --- /dev/null +++ b/app/components/templates/form/fetcher-text.tsx @@ -0,0 +1,115 @@ +import { useForm, conform } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { type FetcherWithComponents } from '@remix-run/react' +import { useRef } from 'react' +import { AuthenticityTokenInput } from 'remix-utils/csrf/react' +import { type z } from 'zod' +import { Icon, type IconName } from '#app/components/ui/icon' +import { Input } from '#app/components/ui/input' +import { Label } from '#app/components/ui/label' +import { useOptimisticValue } from '#app/utils/forms' +import { useDebounce, useIsPending } from '#app/utils/misc' +import { TooltipHydrated } from '../tooltip' + +export const FetcherText = ({ + fetcher, + fetcherKey, + route, + schema, + formId, + fieldName, + fieldValue, + placeholder, + tooltipText, + isHydrated, + disabled, + children, + icon, +}: { + fetcher: FetcherWithComponents + fetcherKey: string + route: string + schema: z.ZodSchema + formId: string + fieldName: string + fieldValue: string + placeholder: string + tooltipText: string + isHydrated: boolean + disabled?: boolean + children: JSX.Element + icon?: IconName +}) => { + const optimisticValue = useOptimisticValue(fetcherKey, schema, fieldName) + const value = optimisticValue ?? fieldValue ?? 0 + const lastSubmission = fetcher.data?.submission + const isPending = useIsPending() + const [form, fields] = useForm({ + id: formId, + constraint: getFieldsetConstraint(schema), + lastSubmission, + shouldValidate: 'onInput', + shouldRevalidate: 'onInput', + onValidate: ({ formData }) => { + return parse(formData, { schema: schema }) + }, + onSubmit: async (event, { formData }) => { + event.preventDefault() + fetcher.submit(formData, { + method: 'POST', + action: route, + }) + }, + defaultValue: { + [fieldName]: value, + }, + }) + + // hack to submit select form on change + // through conform-to and fetcher + const submitRef = useRef(null) + const handleChangeSubmit = useDebounce((f: HTMLFormElement) => { + submitRef.current?.click() + }, 400) + + return ( + handleChangeSubmit(e.currentTarget)} + {...form.props} + className="flex-1" + > + + + {/* hidden field values */} + {children} + + {/* need this div class for icon */} +
+ {/* icon might be for artwork height, width */} + {icon && ( + + )} + + + +
+ + +
+ ) +} diff --git a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx index 3b8a0f41..854b31f8 100644 --- a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx @@ -1,6 +1,9 @@ import { memo, useCallback } from 'react' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' import { type IDesign } from '#app/models/design/design.server' import { ArtworkVersionDesignDelete } from '#app/routes/resources+/api.v1+/artwork-version.design.delete' +import { AssetImageArtworkVersionDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.delete' import { LayerDesignDelete } from '#app/routes/resources+/api.v1+/layer.design.delete' import { type entityParentTypeEnum, @@ -22,6 +25,13 @@ interface DeleteChildEntityFormProps { const ArtworkVersionDeleteChildEntityForm = memo( ({ entityType, entity, parent }: DeleteChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( { const entityCount = entities.length @@ -41,13 +43,17 @@ export const DashboardEntityPanel = ({ {entities.map((entity, index) => { return ( - + {skipReorder ? ( +
+ ) : ( + + )} { + // display color on popover trigger if fill is defined and solid + // const { fill } = entity as IDesignWithFill + // const { basis, value } = fill + // const displayColor = + // basis === FillBasisTypeEnum.DEFINED && FillStyleTypeEnum.SOLID + // const backgroundColor = displayColor ? value : undefined + + return ( + + + Name + + + + ) +}) +EntityPopover.displayName = 'EntityPopover' + +const EntityMainForm = memo(({ entity }: EntityProps) => { + return +}) +EntityMainForm.displayName = 'EntityMainForm' + +export const PanelEntityValuesAssetImage = ({ + entity, +}: { + entity: IAssetImage +}) => { + const entityPopover = useCallback( + () => , + [entity], + ) + + const entityMainForm = useCallback( + () => , + [entity], + ) + + return ( + + {entityPopover()} + {entityMainForm()} + + ) +} diff --git a/app/components/templates/panel/dashboard-entity-panel.values.tsx b/app/components/templates/panel/dashboard-entity-panel.values.tsx index 1d9e3432..71ccb669 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.tsx @@ -1,9 +1,12 @@ +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { AssetTypeEnum } from '#app/schema/asset' import { DesignTypeEnum } from '#app/schema/design' import { type IEntityParentType, type IEntity, type IEntityType, } from '#app/schema/entity' +import { PanelEntityValuesAssetImage } from './dashboard-entity-panel.values.asset.image' import { PanelEntityValuesDesignFill } from './dashboard-entity-panel.values.design.fill' import { PanelEntityValuesDesignLayout } from './dashboard-entity-panel.values.design.layout' import { PanelEntityValuesDesignLine } from './dashboard-entity-panel.values.design.line' @@ -28,6 +31,8 @@ export const PanelEntityValues = ({ } switch (type) { + case AssetTypeEnum.IMAGE: + return case DesignTypeEnum.FILL: return case DesignTypeEnum.LAYOUT: diff --git a/app/components/ui/radio-group.tsx b/app/components/ui/radio-group.tsx new file mode 100644 index 00000000..5e64ce03 --- /dev/null +++ b/app/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' +import { Circle } from 'lucide-react' +import * as React from 'react' + +import { cn } from '#app/utils/misc.tsx' + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + +

yo

+ +
+
+ ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/app/models/asset/asset.get.server.ts b/app/models/asset/asset.get.server.ts index e66866b4..cbe46635 100644 --- a/app/models/asset/asset.get.server.ts +++ b/app/models/asset/asset.get.server.ts @@ -10,6 +10,7 @@ export const assetSelect = { description: true, type: true, attributes: true, + visible: true, // no blob, too much memory on query createdAt: true, updatedAt: true, diff --git a/app/models/asset/asset.server.ts b/app/models/asset/asset.server.ts index c7ed1666..4a99a6df 100644 --- a/app/models/asset/asset.server.ts +++ b/app/models/asset/asset.server.ts @@ -57,6 +57,7 @@ export type IAssetParent = interface IAssetData { name: string description?: string + visible: boolean } export interface IAssetSubmission extends IAssetData { diff --git a/app/models/asset/image/image.delete.artwork-version.server.ts b/app/models/asset/image/image.delete.artwork-version.server.ts new file mode 100644 index 00000000..d2131ce1 --- /dev/null +++ b/app/models/asset/image/image.delete.artwork-version.server.ts @@ -0,0 +1,20 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' + +export const validateDeleteAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + // not validateEntityImageSubmission + // there is no image file to parse and transform + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteAssetImageArtworkVersionSchema, + strategy, + }) +} diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 04a53530..0a29554b 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -11,6 +11,7 @@ const whereArgs = z.object({ id: z.string().optional(), ownerId: z.string().optional(), artworkId: z.string().optional(), + artworkVersionId: z.string().optional(), }) // TODO: Add schemas for each type of query and parse with zod diff --git a/app/models/asset/image/image.update.artwork-version.server.ts b/app/models/asset/image/image.update.artwork-version.server.ts new file mode 100644 index 00000000..66d1ad8e --- /dev/null +++ b/app/models/asset/image/image.update.artwork-version.server.ts @@ -0,0 +1,73 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { + EditAssetImageArtworkVersionSchema, + EditVisibleAssetImageArtworkVersionSchema, +} from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { + validateEntityImageSubmission, + validateEntitySubmission, +} from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' +import { + type IAssetImageUpdateData, + type IAssetImageUpdateSubmission, +} from './image.update.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: EditAssetImageArtworkVersionSchema, + strategy, + }) +} + +export const validateEditVisibleAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditVisibleAssetImageArtworkVersionSchema, + strategy, + }) +} + +export interface IAssetImageArtworkVersionUpdateSubmission + extends IAssetImageUpdateSubmission { + artworkVersionId: IArtworkVersion['id'] +} + +interface IAssetImageArtworkVersionUpdateData extends IAssetImageUpdateData { + artworkVersionId: IArtworkVersion['id'] +} + +export const updateAssetImageArtworkVersion = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageArtworkVersionUpdateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.update.artwork-version.visible.server.ts b/app/models/asset/image/image.update.artwork-version.visible.server.ts new file mode 100644 index 00000000..4d4a0e2e --- /dev/null +++ b/app/models/asset/image/image.update.artwork-version.visible.server.ts @@ -0,0 +1,45 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IUser } from '#app/models/user/user.server' +import { EditVisibleAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage } from './image.server' + +export const validateEditVisibleAssetImageArtworkVersionSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditVisibleAssetImageArtworkVersionSchema, + strategy, + }) +} + +export interface IAssetImageArtworkVersionUpdateVisibleSubmission { + id: IAssetImage['id'] + userId: IUser['id'] + artworkVersionId: IArtworkVersion['id'] +} + +interface IAssetImageArtworkVersionUpdateVisibleData { + visible: boolean +} + +export const updateAssetImageArtworkVersionVisible = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageArtworkVersionUpdateVisibleData +}) => { + return prisma.asset.update({ + where: { id }, + data, + }) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx index 8d05848b..b62ec476 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.artwork-version.tsx @@ -1,7 +1,7 @@ import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { DashboardPanelCreateArtworkVersionAssetTypeStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' -import { DashboardPanelArtworkVersionDesignActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' -import { DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' +import { DashboardPanelArtworkVersionAssetActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' +import { DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' import { PanelAssets } from './sidebars.panel.assets' export const PanelArtworkVersionAssets = ({ @@ -12,8 +12,8 @@ export const PanelArtworkVersionAssets = ({ const strategyEntityNew = new DashboardPanelCreateArtworkVersionAssetTypeStrategy() const strategyReorder = - new DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy() - const strategyActions = new DashboardPanelArtworkVersionDesignActionStrategy() + new DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy() + const strategyActions = new DashboardPanelArtworkVersionAssetActionStrategy() return ( ) } diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx new file mode 100644 index 00000000..f318aef2 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.clone.tsx @@ -0,0 +1,115 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageSelect } from '#app/components/templates/form/fetcher-image-select' +import { useArtworkFromVersion } from '#app/models/artwork/hooks' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' +import { validateNewAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.create.artwork-version.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { CloneAssetImageArtworkToArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionCreateService } from '#app/services/asset.image.artwork-version.create.service' +import { ArtworkAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// technically this would be a clone of the artowrk asset image by id + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.CREATE +const schema = CloneAssetImageArtworkToArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + // consider intent if cloning or uploading new image + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateNewAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkVersionCreateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionClone = ({ + version, +}: { + version: IArtworkVersion +}) => { + const artworkVersionId = version.id + const images = useAssetImagesArtwork() + const artwork = useArtworkFromVersion() + const artworkId = artwork.id + const strategy = new ArtworkAssetImageSrcStrategy() + const formId = `asset-image-artwork-version-${artworkVersionId}-create` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx index 02f0ea48..c4c407ae 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.create.tsx @@ -8,16 +8,13 @@ import { import { useFetcher } from '@remix-run/react' import { redirectBack } from 'remix-utils/redirect-back' import { useHydrated } from 'remix-utils/use-hydrated' -import { FetcherImageSelect } from '#app/components/templates/form/fetcher-image-select' -import { useArtworkFromVersion } from '#app/models/artwork/hooks' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' -import { useAssetImagesArtwork } from '#app/models/asset/image/hooks' import { validateNewAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.create.artwork-version.server' import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' import { NewAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkVersionCreateService } from '#app/services/asset.image.artwork-version.create.service' -import { ArtworkAssetImageSrcStrategy } from '#app/strategies/asset.image.src.strategy' import { requireUserId } from '#app/utils/auth.server' import { Routes } from '#app/utils/routes.const' @@ -78,33 +75,27 @@ export const AssetImageArtworkVersionCreate = ({ version: IArtworkVersion }) => { const artworkVersionId = version.id - const images = useAssetImagesArtwork() - const artwork = useArtworkFromVersion() - const strategy = new ArtworkAssetImageSrcStrategy() - const formId = `asset-image-artwork-${artworkVersionId}-create` + const formId = `asset-image-artwork-version-${artworkVersionId}-create` const fetcher = useFetcher() let isHydrated = useHydrated() return ( -
-
+ ) } diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx new file mode 100644 index 00000000..4bc9345d --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.delete.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { validateDeleteAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.delete.artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { DeleteAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionDeleteService } from '#app/services/asset.image.artwork-version.delete.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.DELETE +const schema = DeleteAssetImageArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateDeleteAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageArtworkVersionDeleteService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionDelete = ({ + image, + artworkVersion, +}: { + image: IAssetImage + artworkVersion: IArtworkVersion +}) => { + const imageId = image.id + const artworkVersionId = artworkVersion.id + const iconText = `Delete Image...` + const formId = `asset-image--${imageId}-artworkVersion-${artworkVersionId}-delete` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx b/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx new file mode 100644 index 00000000..1aab0ffd --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible.tsx @@ -0,0 +1,102 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconButton } from '#app/components/templates/form/fetcher-icon-button' +import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditVisibleAssetImageArtworkVersionSubmission } from '#app/models/asset/image/image.update.artwork-version.server' +import { EditVisibleAssetImageArtworkVersionSchema } from '#app/schema/asset/image.artwork-version' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageArtworkVersionUpdateVisibleService } from '#app/services/asset.image.artwork-version.update.visible.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.ARTWORK_VERSION.UPDATE_VISIBLE +const schema = EditVisibleAssetImageArtworkVersionSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateEditVisibleAssetImageArtworkVersionSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = + await assetImageArtworkVersionUpdateVisibleService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageArtworkVersionUpdateVisible = ({ + image, + artworkVersion, +}: { + image: IAssetImage + artworkVersion: IArtworkVersion +}) => { + const imageId = image.id + const artworkVersionId = artworkVersion.id + const isVisible = image.visible + const icon = isVisible ? 'eye-open' : 'eye-closed' + const iconText = `${isVisible ? 'Hide' : 'Show'} ${image.name}` + const formId = `asset-image--${imageId}-artworkVersion-${artworkVersionId}-update-visible` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.update.name.tsx b/app/routes/resources+/api.v1+/asset.update.name.tsx new file mode 100644 index 00000000..3754ee6a --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.update.name.tsx @@ -0,0 +1,108 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherText } from '#app/components/templates/form/fetcher-text' +import { type IAssetType } from '#app/models/asset/asset.server' +import { + updateDesignTypeFillValue, + validateDesignTypeUpdateFillValueSubmission, +} from '#app/models/design-type/fill/fill.update.server' +import { EntityParentIdType } from '#app/schema/entity' +import { EditDesignFillValueSchema } from '#app/schema/fill' +import { validateNoJS } from '#app/schema/form-data' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.DESIGN.TYPE.FILL.UPDATE.VALUE +const schema = EditDesignFillValueSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + const { status, submission } = + await validateDesignTypeUpdateFillValueSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success } = await updateDesignTypeFillValue({ + userId, + ...submission.value, + }) + updateSuccess = success + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} + +export const AssetUpdateName = ({ + asset, + formLocation = '', +}: { + asset: IAssetType + formLocation?: string +}) => { + const assetId = asset.id + const field = 'name' + const fetcherKey = `asset-update-${field}-${assetId}` + const formId = `${fetcherKey}${formLocation ? `-${formLocation}` : ''}` + const value = asset[field] + + let isHydrated = useHydrated() + const fetcher = useFetcher({ + key: fetcherKey, + }) + + return ( + +
+ + +
+
+ ) +} diff --git a/app/schema/asset/__shared.ts b/app/schema/asset/__shared.ts index 14193c67..9dacf696 100644 --- a/app/schema/asset/__shared.ts +++ b/app/schema/asset/__shared.ts @@ -8,3 +8,10 @@ export const AssetDescriptionSchema = z .string() .max(MAX_DESCRIPTION_LENGTH) .optional() + +export const AssetDataSchema = z.object({ + name: AssetNameSchema, + description: AssetDescriptionSchema, + visible: z.boolean(), + ownerId: z.string(), +}) diff --git a/app/schema/asset/image.artwork-version.ts b/app/schema/asset/image.artwork-version.ts index 07f6a539..50d2dbcb 100644 --- a/app/schema/asset/image.artwork-version.ts +++ b/app/schema/asset/image.artwork-version.ts @@ -14,13 +14,27 @@ export const AssetImageArtworkVersionDataSchema = AssetImageDataSchema.merge( ArtworkVersionParentSchema, ) +export const AssetImageArtworkVersionVisibleDataSchema = z.object({ + visible: z.boolean(), +}) + export const NewAssetImageArtworkVersionSchema = NewAssetImageSchema.merge( ArtworkVersionParentSchema, ) +// this is more like a clone from artwork +export const CloneAssetImageArtworkToArtworkVersionSchema = z.object({ + assetImageId: z.string(), + artworkVersionId: z.string(), +}) export const EditAssetImageArtworkVersionSchema = EditAssetImageSchema.merge( ArtworkVersionParentSchema, ) +export const EditVisibleAssetImageArtworkVersionSchema = z.object({ + id: z.string(), + artworkVersionId: z.string(), +}) + export const DeleteAssetImageArtworkVersionSchema = DeleteAssetImageSchema.merge(ArtworkVersionParentSchema) diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index 7867cce5..30bdb773 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -1,5 +1,9 @@ import { z } from 'zod' -import { AssetDescriptionSchema, AssetNameSchema } from './__shared' +import { + AssetDataSchema, + AssetDescriptionSchema, + AssetNameSchema, +} from './__shared' const MAX_ALT_TEXT_LENGTH = 240 const AltTextSchema = z.string().max(MAX_ALT_TEXT_LENGTH).optional() @@ -46,13 +50,12 @@ export const AssetAttributesImageSchema = z.object({ // zod schema for blob Buffer/File is not working // pass in separately from validation -export const AssetImageDataSchema = z.object({ - name: AssetNameSchema, - description: AssetDescriptionSchema, - type: z.literal('image'), - attributes: AssetAttributesImageSchema, - ownerId: z.string(), -}) +export const AssetImageDataSchema = z + .object({ + type: z.literal('image'), + attributes: AssetAttributesImageSchema, + }) + .merge(AssetDataSchema) // form data validation diff --git a/app/schema/entity.ts b/app/schema/entity.ts index af31ea12..63618b77 100644 --- a/app/schema/entity.ts +++ b/app/schema/entity.ts @@ -48,7 +48,7 @@ export type IEntity = | ITemplate | IAssetType -export type IEntityVisible = IDesign | IDesignWithType | ILayer +export type IEntityVisible = IDesign | IDesignWithType | ILayer | IAssetType export type IEntitySelectable = ILayer export type IEntityWithSlug = | IArtwork diff --git a/app/services/asset.image.artwork-version.delete.service.ts b/app/services/asset.image.artwork-version.delete.service.ts new file mode 100644 index 00000000..7382aa2d --- /dev/null +++ b/app/services/asset.image.artwork-version.delete.service.ts @@ -0,0 +1,31 @@ +import { + deleteAssetImage, + type IAssetImageDeletedResponse, +} from '#app/models/asset/image/image.delete.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type IUser } from '#app/models/user/user.server' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkVersionDeleteService = async ({ + userId, + id, +}: { + userId: IUser['id'] + id: IAssetImage['id'] +}): Promise => { + try { + // Step 1: delete the asset image via promise + const deleteAssetImageArtworkVersionPromise = deleteAssetImage({ id }) + + // Step 2: execute the transaction + await prisma.$transaction([deleteAssetImageArtworkVersionPromise]) + + return { success: true } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkVersionDeleteService', + } + } +} diff --git a/app/services/asset.image.artwork-version.update.visible.service.ts b/app/services/asset.image.artwork-version.update.visible.service.ts new file mode 100644 index 00000000..9749dbec --- /dev/null +++ b/app/services/asset.image.artwork-version.update.visible.service.ts @@ -0,0 +1,51 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + updateAssetImageArtworkVersionVisible, + type IAssetImageArtworkVersionUpdateVisibleSubmission, +} from '#app/models/asset/image/image.update.artwork-version.visible.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetImageArtworkVersionVisibleDataSchema } from '#app/schema/asset/image.artwork-version' +import { prisma } from '#app/utils/db.server' + +export const assetImageArtworkVersionUpdateVisibleService = async ({ + userId, + id, + artworkVersionId, +}: IAssetImageArtworkVersionUpdateVisibleSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, artworkVersionId, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + + // Step 2: validate asset image data + const data = { + visible: !assetImage.visible, + } + const assetImageData = AssetImageArtworkVersionVisibleDataSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageArtworkVersionVisible({ + id, + data: { ...assetImageData }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageArtworkVersionUpdateVisibleService', + } + } +} diff --git a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts index 9ea1caee..8b71243d 100644 --- a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts +++ b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts @@ -12,6 +12,14 @@ export interface IDashboardPanelDeleteEntityStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelDeleteArtworkVersionAssetStrategy + implements IDashboardPanelDeleteEntityStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.DELETE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelDeleteArtworkVersionDesignStrategy implements IDashboardPanelDeleteEntityStrategy { diff --git a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts index 7057042c..86896749 100644 --- a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts +++ b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts @@ -1,10 +1,12 @@ import { + DashboardPanelDeleteArtworkVersionAssetStrategy, DashboardPanelDeleteArtworkVersionDesignStrategy, DashboardPanelDeleteLayerDesignStrategy, type IDashboardPanelDeleteEntityStrategy, } from '../delete-entity.strategy' import { DashboardPanelSelectArtworkVersionLayerStrategy } from '../update-entity-selected.strategy' import { + DashboardPanelUpdateArtworkVersionAssetVisibleStrategy, DashboardPanelUpdateArtworkVersionDesignVisibleStrategy, DashboardPanelUpdateArtworkVersionLayerVisibleStrategy, DashboardPanelUpdateLayerDesignVisibleStrategy, @@ -19,7 +21,18 @@ export interface IDashboardPanelEntityActionStrategy { getPanelActions(): IPanelEntityActionStrategy[] } -// artwork design +export class DashboardPanelArtworkVersionAssetActionStrategy + implements IDashboardPanelEntityActionStrategy +{ + getPanelActions(): IPanelEntityActionStrategy[] { + const strategyToggleVisible = + new DashboardPanelUpdateArtworkVersionAssetVisibleStrategy() + const strategyDelete = new DashboardPanelDeleteArtworkVersionAssetStrategy() + + return [strategyToggleVisible, strategyDelete] + } +} + export class DashboardPanelArtworkVersionDesignActionStrategy implements IDashboardPanelEntityActionStrategy { @@ -41,8 +54,7 @@ export class DashboardPanelArtworkVersionLayerActionStrategy const strategyToggleVisible = new DashboardPanelUpdateArtworkVersionLayerVisibleStrategy() - const strategySelect = - new DashboardPanelSelectArtworkVersionLayerStrategy() + const strategySelect = new DashboardPanelSelectArtworkVersionLayerStrategy() // delete in popover so it's less easy to click accidentally from left sidebar diff --git a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts index 0369c02e..4c314863 100644 --- a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts @@ -10,6 +10,13 @@ export interface IDashboardPanelUpdateEntityOrderStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelUpdateArtworkVersionAssetTypeOrderStrategy + implements IDashboardPanelUpdateEntityOrderStrategy +{ + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelUpdateArtworkVersionDesignTypeOrderStrategy implements IDashboardPanelUpdateEntityOrderStrategy { diff --git a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts index 94a4dcb1..97020291 100644 --- a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts @@ -13,6 +13,14 @@ export interface IDashboardPanelUpdateEntityVisibleStrategy { parentType: entityParentTypeEnum } +export class DashboardPanelUpdateArtworkVersionAssetVisibleStrategy + implements IDashboardPanelUpdateEntityVisibleStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.TOGGLE_VISIBLE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION +} + export class DashboardPanelUpdateArtworkVersionDesignVisibleStrategy implements IDashboardPanelUpdateEntityVisibleStrategy { diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index b7178ee8..43fac77d 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -45,8 +45,10 @@ export const Routes = { }, ARTWORK_VERSION: { CREATE: `${pathBase}/asset/image/artwork-version/create`, + CLONE: `${pathBase}/asset/image/artwork-version/clone`, DELETE: `${pathBase}/asset/image/artwork-version/delete`, UPDATE: `${pathBase}/asset/image/artwork-version/update`, + UPDATE_VISIBLE: `${pathBase}/asset/image/artwork-version/update/visible`, }, }, }, diff --git a/package-lock.json b/package-lock.json index d70113ab..668e59f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -2732,6 +2733,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -22916,6 +22949,24 @@ "@radix-ui/react-slot": "1.0.2" } }, + "@radix-ui/react-radio-group": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz", + "integrity": "sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + } + }, "@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", diff --git a/package.json b/package.json index 0ba5ddf8..4ddcc910 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", diff --git a/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql b/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql new file mode 100644 index 00000000..be677a54 --- /dev/null +++ b/prisma/migrations/20240613070911_add_visible_to_asset/migration.sql @@ -0,0 +1,30 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Asset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "type" TEXT NOT NULL, + "attributes" TEXT NOT NULL DEFAULT '{}', + "blob" BLOB, + "visible" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ownerId" TEXT NOT NULL, + "projectId" TEXT, + "artworkId" TEXT, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Asset_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkId_fkey" FOREIGN KEY ("artworkId") REFERENCES "Artwork" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Asset_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Asset" ("artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt") SELECT "artworkId", "artworkVersionId", "attributes", "blob", "createdAt", "description", "id", "layerId", "name", "ownerId", "projectId", "type", "updatedAt" FROM "Asset"; +DROP TABLE "Asset"; +ALTER TABLE "new_Asset" RENAME TO "Asset"; +CREATE INDEX "Asset_ownerId_idx" ON "Asset"("ownerId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4d955fb6..3e90b049 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -549,6 +549,7 @@ model Asset { type String // e.g. image, media, palette, gradient, shapes, etc. attributes String @default("{}") // json string of attributes specific to the type blob Bytes? + visible Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt From 61aa333ee12254d0c105582d1e5c144414d55c7e Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Fri, 14 Jun 2024 15:36:31 -0400 Subject: [PATCH 32/54] image preview in popover --- ...hboard-entity-panel.values.asset.image.tsx | 30 +++++++------- .../image/image.get.artwork-version.server.ts | 39 ++++++++++++++++++ .../asset/image/image.get.artwork.server.ts | 39 ++++++++++++++++++ app/models/asset/image/image.get.server.ts | 37 +---------------- app/models/asset/image/utils.ts | 41 ++++++++++++++++++- ...ars.panel.artwork-version.images.image.tsx | 5 +-- .../api.v1+/asset.image.artwork.update.tsx | 2 +- .../resources+/api.v1+/asset.update.name.tsx | 2 +- ...sion.$artworkVersionId.images.$imageId.tsx | 28 +++++++++++++ .../artwork.$artworkId.images.$imageId.tsx | 2 +- app/strategies/asset.image.src.strategy.ts | 2 +- app/utils/misc.tsx | 10 ----- 12 files changed, 169 insertions(+), 68 deletions(-) create mode 100644 app/models/asset/image/image.get.artwork-version.server.ts create mode 100644 app/models/asset/image/image.get.artwork.server.ts create mode 100644 app/routes/resources+/artwork-version.$artworkVersionId.images.$imageId.tsx diff --git a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx index b85f5a0f..e8c4f4c5 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx @@ -1,36 +1,38 @@ import { memo, useCallback } from 'react' +import { ImagePreview } from '#app/components/image' import { SidebarPanelPopoverFormContainer } from '#app/components/layout/popover' -import { type IAssetType } from '#app/models/asset/asset.server' import { type IAssetImage } from '#app/models/asset/image/image.server' +import { getAssetImgSrc } from '#app/models/asset/image/utils' import { AssetUpdateName } from '#app/routes/resources+/api.v1+/asset.update.name' import { SidebarPanelRowValuesContainer } from '..' import { PanelEntityPopover } from './dashboard-entity-panel.popover' interface EntityProps { - entity: IAssetType + image: IAssetImage } -const EntityPopover = memo(({ entity }: EntityProps) => { - // display color on popover trigger if fill is defined and solid - // const { fill } = entity as IDesignWithFill - // const { basis, value } = fill - // const displayColor = - // basis === FillBasisTypeEnum.DEFINED && FillStyleTypeEnum.SOLID - // const backgroundColor = displayColor ? value : undefined +const EntityPopover = memo(({ image }: EntityProps) => { + const { attributes } = image + const { altText } = attributes + const imgSrc = getAssetImgSrc({ image }) return ( + + Image + + Name - + ) }) EntityPopover.displayName = 'EntityPopover' -const EntityMainForm = memo(({ entity }: EntityProps) => { - return +const EntityMainForm = memo(({ image }: EntityProps) => { + return }) EntityMainForm.displayName = 'EntityMainForm' @@ -40,12 +42,12 @@ export const PanelEntityValuesAssetImage = ({ entity: IAssetImage }) => { const entityPopover = useCallback( - () => , + () => , [entity], ) const entityMainForm = useCallback( - () => , + () => , [entity], ) diff --git a/app/models/asset/image/image.get.artwork-version.server.ts b/app/models/asset/image/image.get.artwork-version.server.ts new file mode 100644 index 00000000..16e32fe2 --- /dev/null +++ b/app/models/asset/image/image.get.artwork-version.server.ts @@ -0,0 +1,39 @@ +import { invariant } from '@epic-web/invariant' +import { AssetTypeEnum } from '#app/schema/asset' +import { prisma } from '#app/utils/db.server' +import { type IAssetImageSrc, type IAssetImage } from './image.server' +import { parseAssetImageAttributes } from './utils' + +// just return the minimum required data +// for loading the image from the route url +export const getAssetImageArtworkVersionSrc = async ({ + id, + artworkVersionId, + ownerId, +}: { + id: IAssetImage['id'] + artworkVersionId: IAssetImage['artworkVersionId'] + ownerId: IAssetImage['ownerId'] +}): Promise => { + const image = await prisma.asset.findUnique({ + where: { + id, + ownerId, + artworkVersionId, + type: AssetTypeEnum.IMAGE, + }, + select: { + attributes: true, + blob: true, + }, + }) + invariant(image, 'Asset Image Not found: ' + id) + invariant(image.blob, 'Asset Image has no blob: ' + id) + + const attributes = parseAssetImageAttributes(image.attributes) + + return { + contentType: attributes.contentType, + blob: image.blob, + } +} diff --git a/app/models/asset/image/image.get.artwork.server.ts b/app/models/asset/image/image.get.artwork.server.ts new file mode 100644 index 00000000..4d8ad80e --- /dev/null +++ b/app/models/asset/image/image.get.artwork.server.ts @@ -0,0 +1,39 @@ +import { invariant } from '@epic-web/invariant' +import { AssetTypeEnum } from '#app/schema/asset' +import { prisma } from '#app/utils/db.server' +import { type IAssetImageSrc, type IAssetImage } from './image.server' +import { parseAssetImageAttributes } from './utils' + +// just return the minimum required data +// for loading the image from the route url +export const getAssetImageArtworkSrc = async ({ + id, + artworkId, + ownerId, +}: { + id: IAssetImage['id'] + artworkId: IAssetImage['artworkId'] + ownerId: IAssetImage['ownerId'] +}): Promise => { + const image = await prisma.asset.findUnique({ + where: { + id, + ownerId, + artworkId, + type: AssetTypeEnum.IMAGE, + }, + select: { + attributes: true, + blob: true, + }, + }) + invariant(image, 'Asset Image Not found: ' + id) + invariant(image.blob, 'Asset Image has no blob: ' + id) + + const attributes = parseAssetImageAttributes(image.attributes) + + return { + contentType: attributes.contentType, + blob: image.blob, + } +} diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 0a29554b..17011016 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -3,8 +3,7 @@ import { z } from 'zod' import { AssetTypeEnum } from '#app/schema/asset' import { deserializeAsset } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' -import { type IAssetImageSrc, type IAssetImage } from './image.server' -import { parseAssetImageAttributes } from './utils' +import { type IAssetImage } from './image.server' export type queryAssetImageWhereArgsType = z.infer const whereArgs = z.object({ @@ -51,37 +50,3 @@ export const getAssetImage = async ({ invariant(asset, 'Asset Image Not found') return deserializeAsset({ asset }) as IAssetImage } - -// just return the minimum required data -// for loading the image from the route url -export const getAssetImageArtworkSrc = async ({ - id, - artworkId, - ownerId, -}: { - id: IAssetImage['id'] - artworkId: IAssetImage['artworkId'] - ownerId: IAssetImage['ownerId'] -}): Promise => { - const image = await prisma.asset.findUnique({ - where: { - id, - ownerId, - artworkId, - type: AssetTypeEnum.IMAGE, - }, - select: { - attributes: true, - blob: true, - }, - }) - invariant(image, 'Asset Image Not found: ' + id) - invariant(image.blob, 'Asset Image has no blob: ' + id) - - const attributes = parseAssetImageAttributes(image.attributes) - - return { - contentType: attributes.contentType, - blob: image.blob, - } -} diff --git a/app/models/asset/image/utils.ts b/app/models/asset/image/utils.ts index 596b1471..8b84581f 100644 --- a/app/models/asset/image/utils.ts +++ b/app/models/asset/image/utils.ts @@ -1,5 +1,8 @@ import { ZodError } from 'zod' -import { type IAssetAttributesImage } from '#app/models/asset/image/image.server' +import { + type IAssetImage, + type IAssetAttributesImage, +} from '#app/models/asset/image/image.server' import { AssetAttributesImageSchema } from '#app/schema/asset/image' export const parseAssetImageAttributes = ( @@ -41,3 +44,39 @@ export const stringifyAssetImageAttributes = ( export const sizeInMB = (sizeInBytes: number) => { return (sizeInBytes / 1024 / 1024).toFixed(2) } + +export function getArtworkAssetImgSrc({ + artworkId, + imageId, +}: { + artworkId: string + imageId: string +}) { + return `/resources/artwork/${artworkId}/images/${imageId}` +} + +export function getArtworkVersionAssetImgSrc({ + artworkVersionId, + imageId, +}: { + artworkVersionId: string + imageId: string +}) { + return `/resources/artwork-version/${artworkVersionId}/images/${imageId}` +} + +export const getAssetImgSrc = ({ image }: { image: IAssetImage }) => { + if (image.artworkId) { + return getArtworkAssetImgSrc({ + imageId: image.id, + artworkId: image.artworkId, + }) + } else if (image.artworkVersionId) { + return getArtworkVersionAssetImgSrc({ + imageId: image.id, + artworkVersionId: image.artworkVersionId, + }) + } else { + throw new Error('Image does not have artwork or artwork version id') + } +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx index 82500106..8224f10f 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.artwork-version.images.image.tsx @@ -15,11 +15,10 @@ import { } from '#app/components/ui/dialog' import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' -import { sizeInMB } from '#app/models/asset/image/utils' +import { getAssetImgSrc, sizeInMB } from '#app/models/asset/image/utils' import { AssetImageArtworkCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork.create' import { AssetImageArtworkDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork.delete' import { AssetImageArtworkUpdate } from '#app/routes/resources+/api.v1+/asset.image.artwork.update' -import { getArtworkAssetImgSrc } from '#app/utils/misc' const ImageCreate = memo(({ artwork }: { artwork: IArtworkWithAssets }) => { return @@ -44,7 +43,7 @@ export const ImageListItem = memo( ({ image, artwork }: { image: IAssetImage; artwork: IArtworkWithAssets }) => { const { id, name, attributes } = image const { altText, height, width, size } = attributes - const imgSrc = getArtworkAssetImgSrc({ imageId: id, artworkId: artwork.id }) + const imgSrc = getAssetImgSrc({ image }) return ( diff --git a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx index 0398bded..a6bddda1 100644 --- a/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx +++ b/app/routes/resources+/api.v1+/asset.image.artwork.update.tsx @@ -12,12 +12,12 @@ import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IAssetImage } from '#app/models/asset/image/image.server' import { validateEditAssetImageArtworkSubmission } from '#app/models/asset/image/image.update.artwork.server' +import { getArtworkAssetImgSrc } from '#app/models/asset/image/utils' import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' import { EditAssetImageArtworkSchema } from '#app/schema/asset/image.artwork' import { validateNoJS } from '#app/schema/form-data' import { assetImageArtworkUpdateService } from '#app/services/asset.image.artwork.update.service' import { requireUserId } from '#app/utils/auth.server' -import { getArtworkAssetImgSrc } from '#app/utils/misc' import { Routes } from '#app/utils/routes.const' // https://www.epicweb.dev/full-stack-components diff --git a/app/routes/resources+/api.v1+/asset.update.name.tsx b/app/routes/resources+/api.v1+/asset.update.name.tsx index 3754ee6a..ea769fba 100644 --- a/app/routes/resources+/api.v1+/asset.update.name.tsx +++ b/app/routes/resources+/api.v1+/asset.update.name.tsx @@ -93,7 +93,7 @@ export const AssetUpdateName = ({ tooltipText={`Fill ${field}`} isHydrated={isHydrated} placeholder={`Select a ${field}`} - disabled={true} + disabled >
diff --git a/app/routes/resources+/artwork-version.$artworkVersionId.images.$imageId.tsx b/app/routes/resources+/artwork-version.$artworkVersionId.images.$imageId.tsx new file mode 100644 index 00000000..964cdf89 --- /dev/null +++ b/app/routes/resources+/artwork-version.$artworkVersionId.images.$imageId.tsx @@ -0,0 +1,28 @@ +import { invariantResponse } from '@epic-web/invariant' +import { type LoaderFunctionArgs } from '@remix-run/node' +import { getAssetImageArtworkVersionSrc } from '#app/models/asset/image/image.get.artwork-version.server' +import { requireUserId } from '#app/utils/auth.server' + +export async function loader({ params, request }: LoaderFunctionArgs) { + const userId = await requireUserId(request) + invariantResponse(params.artworkVersionId, 'Artwork Version ID is required', { + status: 400, + }) + invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) + const image = await getAssetImageArtworkVersionSrc({ + id: params.imageId, + artworkVersionId: params.artworkVersionId, + ownerId: userId, + }) + + invariantResponse(image, 'Not found', { status: 404 }) + + return new Response(image.blob, { + headers: { + 'Content-Type': image.contentType, + 'Content-Length': Buffer.byteLength(image.blob).toString(), + 'Content-Disposition': `inline; filename="${params.imageId}"`, + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }) +} diff --git a/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx b/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx index 1871bbf5..841914ab 100644 --- a/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx +++ b/app/routes/resources+/artwork.$artworkId.images.$imageId.tsx @@ -1,6 +1,6 @@ import { invariantResponse } from '@epic-web/invariant' import { type LoaderFunctionArgs } from '@remix-run/node' -import { getAssetImageArtworkSrc } from '#app/models/asset/image/image.get.server' +import { getAssetImageArtworkSrc } from '#app/models/asset/image/image.get.artwork.server' import { requireUserId } from '#app/utils/auth.server' export async function loader({ params, request }: LoaderFunctionArgs) { diff --git a/app/strategies/asset.image.src.strategy.ts b/app/strategies/asset.image.src.strategy.ts index 858cc565..89da236c 100644 --- a/app/strategies/asset.image.src.strategy.ts +++ b/app/strategies/asset.image.src.strategy.ts @@ -1,4 +1,4 @@ -import { getArtworkAssetImgSrc } from '#app/utils/misc' +import { getArtworkAssetImgSrc } from '#app/models/asset/image/utils' export interface IAssetImageSrcStrategy { getAssetSrc(args: { assetId: string; parentId: string }): string diff --git a/app/utils/misc.tsx b/app/utils/misc.tsx index 98ce13dd..4fbba9b3 100644 --- a/app/utils/misc.tsx +++ b/app/utils/misc.tsx @@ -13,16 +13,6 @@ export function getNoteImgSrc(imageId: string) { return `/resources/note-images/${imageId}` } -export function getArtworkAssetImgSrc({ - artworkId, - imageId, -}: { - artworkId: string - imageId: string -}) { - return `/resources/artwork/${artworkId}/images/${imageId}` -} - export function getErrorMessage(error: unknown) { if (typeof error === 'string') return error if ( From e17ad395f7ae5f1e5c2573902018012e5251ab27 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 15 Jun 2024 12:59:42 -0400 Subject: [PATCH 33/54] image fit added to attributes --- ...hboard-entity-panel.values.asset.image.tsx | 5 + app/models/asset/image/image.server.ts | 11 ++ .../asset/image/image.update.fit.server.ts | 53 +++++++++ .../api.v1+/asset.image.update.fit.tsx | 106 ++++++++++++++++++ .../resources+/api.v1+/asset.update.name.tsx | 6 - app/schema/asset/image.ts | 20 ++++ .../asset.image.update.fit.service.ts | 53 +++++++++ app/utils/forms.ts | 2 + app/utils/routes.const.ts | 3 + 9 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 app/models/asset/image/image.update.fit.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.update.fit.tsx create mode 100644 app/services/asset.image.update.fit.service.ts diff --git a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx index e8c4f4c5..8e2901db 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx @@ -3,6 +3,7 @@ import { ImagePreview } from '#app/components/image' import { SidebarPanelPopoverFormContainer } from '#app/components/layout/popover' import { type IAssetImage } from '#app/models/asset/image/image.server' import { getAssetImgSrc } from '#app/models/asset/image/utils' +import { AssetImageUpdateFit } from '#app/routes/resources+/api.v1+/asset.image.update.fit' import { AssetUpdateName } from '#app/routes/resources+/api.v1+/asset.update.name' import { SidebarPanelRowValuesContainer } from '..' import { PanelEntityPopover } from './dashboard-entity-panel.popover' @@ -26,6 +27,10 @@ const EntityPopover = memo(({ image }: EntityProps) => { Name + + Fit + + ) }) diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts index 4f74ccce..5f454198 100644 --- a/app/models/asset/image/image.server.ts +++ b/app/models/asset/image/image.server.ts @@ -6,6 +6,16 @@ export interface IAssetImage extends IAssetParsed { attributes: IAssetAttributesImage } +// https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit +export type IAssetImageFit = + | 'fill' + | 'contain' + | 'cover' + | 'none' + | 'scale-down' +// TODO: add position later +// https://developer.mozilla.org/en-US/docs/Web/CSS/object-position + export interface IAssetImageFileData { contentType: string height: number @@ -13,6 +23,7 @@ export interface IAssetImageFileData { size: number lastModified?: number filename: string + fit?: IAssetImageFit } // when adding attributes to an asset type, diff --git a/app/models/asset/image/image.update.fit.server.ts b/app/models/asset/image/image.update.fit.server.ts new file mode 100644 index 00000000..f9f440d3 --- /dev/null +++ b/app/models/asset/image/image.update.fit.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditAssetImageFitSchema } from '#app/schema/asset/image' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IAssetImageFit, + type IAssetImage, + type IAssetImageFileData, +} from './image.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditFitAssetImageSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditAssetImageFitSchema, + strategy, + }) +} + +export interface IAssetImageUpdateFitSubmission { + userId: IUser['id'] + id: IAssetImage['id'] + fit: IAssetImageFit +} + +interface IAssetImageUpdateFitData { + attributes: IAssetImageFileData +} + +export const updateAssetImageFit = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageUpdateFitData +}) => { + const { attributes } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/routes/resources+/api.v1+/asset.image.update.fit.tsx b/app/routes/resources+/api.v1+/asset.image.update.fit.tsx new file mode 100644 index 00000000..b9396720 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.update.fit.tsx @@ -0,0 +1,106 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherSelect } from '#app/components/templates/form/fetcher-select' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditFitAssetImageSubmission } from '#app/models/asset/image/image.update.fit.server' +import { + AssetImageFitTypeEnum, + EditAssetImageFitSchema, +} from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageUpdateFitService } from '#app/services/asset.image.update.fit.service' +import { requireUserId } from '#app/utils/auth.server' +import { schemaEnumToSelectOptions } from '#app/utils/forms' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.UPDATE.FIT +const schema = EditAssetImageFitSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + let errorMessage = '' + const { status, submission } = await validateEditFitAssetImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageUpdateFitService({ + userId, + ...submission.value, + }) + updateSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} + +export const AssetImageUpdateFit = ({ + image, + formLocation = '', +}: { + image: IAssetImage + formLocation?: string +}) => { + const assetId = image.id + const field = 'fit' + const fetcherKey = `asset-image-update-${field}-${assetId}` + const formId = `${fetcherKey}${formLocation ? `-${formLocation}` : ''}` + const value = image.attributes[field] || '' + const options = schemaEnumToSelectOptions(AssetImageFitTypeEnum) + + let isHydrated = useHydrated() + const fetcher = useFetcher({ + key: fetcherKey, + }) + + return ( + +
+ +
+
+ ) +} diff --git a/app/routes/resources+/api.v1+/asset.update.name.tsx b/app/routes/resources+/api.v1+/asset.update.name.tsx index ea769fba..9fd8a5c6 100644 --- a/app/routes/resources+/api.v1+/asset.update.name.tsx +++ b/app/routes/resources+/api.v1+/asset.update.name.tsx @@ -12,7 +12,6 @@ import { updateDesignTypeFillValue, validateDesignTypeUpdateFillValueSubmission, } from '#app/models/design-type/fill/fill.update.server' -import { EntityParentIdType } from '#app/schema/entity' import { EditDesignFillValueSchema } from '#app/schema/fill' import { validateNoJS } from '#app/schema/form-data' import { requireUserId } from '#app/utils/auth.server' @@ -97,11 +96,6 @@ export const AssetUpdateName = ({ >
-
) diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index 30bdb773..d33ea3af 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' import { AssetDataSchema, AssetDescriptionSchema, @@ -32,6 +33,19 @@ const FileSchema = z 'Image must be a JPEG, PNG, WEBP, or GIF', ) +// https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit +export const AssetImageFitTypeEnum = { + FILL: 'fill', + CONTAIN: 'contain', + COVER: 'cover', + NONE: 'none', + SCALE_DOWN: 'scale-down', + // add more asset fit types here +} as const +export type assetImageFitTypeEnum = ObjectValues +// TODO: add position later +// https://developer.mozilla.org/en-US/docs/Web/CSS/object-position + // asset image validation before saving to db // use this to (de)serealize data to/from the db @@ -46,6 +60,7 @@ export const AssetAttributesImageSchema = z.object({ size: z.number(), lastModified: z.number().optional(), filename: z.string(), + fit: z.nativeEnum(AssetImageFitTypeEnum).optional(), }) // zod schema for blob Buffer/File is not working @@ -74,6 +89,11 @@ export const EditAssetImageSchema = z.object({ altText: AltTextSchema, }) +export const EditAssetImageFitSchema = z.object({ + id: z.string(), + fit: z.nativeEnum(AssetImageFitTypeEnum), +}) + export const DeleteAssetImageSchema = z.object({ id: z.string(), }) diff --git a/app/services/asset.image.update.fit.service.ts b/app/services/asset.image.update.fit.service.ts new file mode 100644 index 00000000..cc3df213 --- /dev/null +++ b/app/services/asset.image.update.fit.service.ts @@ -0,0 +1,53 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + updateAssetImageFit, + type IAssetImageUpdateFitSubmission, +} from '#app/models/asset/image/image.update.fit.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetAttributesImageSchema } from '#app/schema/asset/image' +import { prisma } from '#app/utils/db.server' + +export const assetImageUpdateFitService = async ({ + userId, + id, + fit, +}: IAssetImageUpdateFitSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + const { attributes: assetImageAttributes } = assetImage + + // Step 2: validate asset image data + const data = { + ...assetImageAttributes, + fit, + } + const assetImageData = AssetAttributesImageSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageFit({ + id, + data: { attributes: { ...assetImageData } }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageUpdateFitService', + } + } +} diff --git a/app/utils/forms.ts b/app/utils/forms.ts index 1ef8a044..1d42ab60 100644 --- a/app/utils/forms.ts +++ b/app/utils/forms.ts @@ -1,6 +1,7 @@ import { parse } from '@conform-to/zod' import { useFetcher } from '@remix-run/react' import { type z } from 'zod' +import { type AssetImageFitTypeEnum } from '#app/schema/asset/image' import { type FillStyleTypeEnum, type FillBasisTypeEnum, @@ -34,6 +35,7 @@ type EnumSchema = | typeof StrokeStyleTypeEnum | typeof SizeFormatTypeEnum | typeof TemplateStyleTypeEnum + | typeof AssetImageFitTypeEnum export const schemaEnumToSelectOptions = (enumSchema: EnumSchema) => { return Object.values(enumSchema).map(optionEnum => ({ diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index 43fac77d..c2204e93 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -50,6 +50,9 @@ export const Routes = { UPDATE: `${pathBase}/asset/image/artwork-version/update`, UPDATE_VISIBLE: `${pathBase}/asset/image/artwork-version/update/visible`, }, + UPDATE: { + FIT: `${pathBase}/asset/image/update/fit`, + }, }, }, DESIGN: { From 41c9bac3bf091d5a648b7eda1476b1169185c83e Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 15 Jun 2024 13:54:53 -0400 Subject: [PATCH 34/54] can add images to layer --- .../panel/dashboard-entity-panel.header.tsx | 4 + .../artwork-version/artwork-version.server.ts | 4 +- .../asset/image/image.create.layer.server.ts | 49 ++++++++++ .../asset/image/image.get.layer.server.ts | 39 ++++++++ app/models/asset/image/utils.ts | 15 +++ .../sidebars.panel.assets.layer.tsx | 20 ++++ .../__components/sidebars.panel.layer.tsx | 6 +- .../$artworkSlug+/__components/sidebars.tsx | 6 +- .../api.v1+/asset.image.layer.create.tsx | 96 +++++++++++++++++++ .../layer.$layerId.images.$imageId.tsx | 26 +++++ app/schema/asset/image.layer.ts | 32 +++++++ ...et.image.artwork-version.create.service.ts | 1 + .../asset.image.artwork.create.service.ts | 1 + .../asset.image.layer.create.service.ts | 76 +++++++++++++++ .../dashboard-panel/create-entity.strategy.ts | 7 ++ .../dashboard-panel/delete-entity.strategy.ts | 8 ++ .../entity-action/entity-action.ts | 14 +++ .../update-entity-order.strategy.ts | 7 ++ .../update-entity-visible.strategy.ts | 8 ++ app/utils/routes.const.ts | 5 + 20 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 app/models/asset/image/image.create.layer.server.ts create mode 100644 app/models/asset/image/image.get.layer.server.ts create mode 100644 app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.layer.tsx create mode 100644 app/routes/resources+/api.v1+/asset.image.layer.create.tsx create mode 100644 app/routes/resources+/layer.$layerId.images.$imageId.tsx create mode 100644 app/schema/asset/image.layer.ts create mode 100644 app/services/asset.image.layer.create.service.ts diff --git a/app/components/templates/panel/dashboard-entity-panel.header.tsx b/app/components/templates/panel/dashboard-entity-panel.header.tsx index 73165a35..5a89e87c 100644 --- a/app/components/templates/panel/dashboard-entity-panel.header.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.header.tsx @@ -1,8 +1,10 @@ import { memo, useCallback } from 'react' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type ILayer } from '#app/models/layer/layer.server' import { ArtworkVersionDesignCreate } from '#app/routes/resources+/api.v1+/artwork-version.design.create' import { ArtworkVersionLayerCreate } from '#app/routes/resources+/api.v1+/artwork-version.layer.create' import { AssetImageArtworkVersionCreate } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.create' +import { AssetImageLayerCreate } from '#app/routes/resources+/api.v1+/asset.image.layer.create' import { LayerDesignCreate } from '#app/routes/resources+/api.v1+/layer.design.create' import { type designTypeEnum } from '#app/schema/design' import { @@ -58,6 +60,8 @@ ArtworkVersionCreateChildEntityForm.displayName = const LayerCreateChildEntityForm = memo( ({ entityType, type, parent }: CreateChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return case EntityType.DESIGN: return ( { + const strategy = new ValidateLayerParentSubmissionStrategy() + + return await validateEntityImageSubmission({ + userId, + formData, + schema: NewAssetImageLayerSchema, + strategy, + }) +} + +export interface IAssetImageLayerCreateSubmission + extends IAssetImageCreateSubmission { + layerId: ILayer['id'] +} + +interface IAssetImageLayerCreateData extends IAssetImageCreateData { + layerId: ILayer['id'] +} + +export const createAssetImageLayer = ({ + data, +}: { + data: IAssetImageLayerCreateData +}) => { + const { attributes, ...rest } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.create({ + data: { + ...rest, + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/image/image.get.layer.server.ts b/app/models/asset/image/image.get.layer.server.ts new file mode 100644 index 00000000..0d13debe --- /dev/null +++ b/app/models/asset/image/image.get.layer.server.ts @@ -0,0 +1,39 @@ +import { invariant } from '@epic-web/invariant' +import { AssetTypeEnum } from '#app/schema/asset' +import { prisma } from '#app/utils/db.server' +import { type IAssetImageSrc, type IAssetImage } from './image.server' +import { parseAssetImageAttributes } from './utils' + +// just return the minimum required data +// for loading the image from the route url +export const getAssetImageLayerSrc = async ({ + id, + layerId, + ownerId, +}: { + id: IAssetImage['id'] + layerId: IAssetImage['layerId'] + ownerId: IAssetImage['ownerId'] +}): Promise => { + const image = await prisma.asset.findUnique({ + where: { + id, + ownerId, + layerId, + type: AssetTypeEnum.IMAGE, + }, + select: { + attributes: true, + blob: true, + }, + }) + invariant(image, 'Asset Image Not found: ' + id) + invariant(image.blob, 'Asset Image has no blob: ' + id) + + const attributes = parseAssetImageAttributes(image.attributes) + + return { + contentType: attributes.contentType, + blob: image.blob, + } +} diff --git a/app/models/asset/image/utils.ts b/app/models/asset/image/utils.ts index 8b84581f..6dbde4ce 100644 --- a/app/models/asset/image/utils.ts +++ b/app/models/asset/image/utils.ts @@ -65,6 +65,16 @@ export function getArtworkVersionAssetImgSrc({ return `/resources/artwork-version/${artworkVersionId}/images/${imageId}` } +export function getLayerAssetImgSrc({ + layerId, + imageId, +}: { + layerId: string + imageId: string +}) { + return `/resources/layer/${layerId}/images/${imageId}` +} + export const getAssetImgSrc = ({ image }: { image: IAssetImage }) => { if (image.artworkId) { return getArtworkAssetImgSrc({ @@ -76,6 +86,11 @@ export const getAssetImgSrc = ({ image }: { image: IAssetImage }) => { imageId: image.id, artworkVersionId: image.artworkVersionId, }) + } else if (image.layerId) { + return getLayerAssetImgSrc({ + imageId: image.id, + layerId: image.layerId, + }) } else { throw new Error('Image does not have artwork or artwork version id') } diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.layer.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.layer.tsx new file mode 100644 index 00000000..29a48475 --- /dev/null +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.layer.tsx @@ -0,0 +1,20 @@ +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { DashboardPanelCreateLayerAssetTypeStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' +import { DashboardPanelLayerAssetActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' +import { DashboardPanelUpdateLayerAssetTypeOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' +import { PanelAssets } from './sidebars.panel.assets' + +export const PanelLayerAssets = ({ layer }: { layer: ILayerWithChildren }) => { + const strategyEntityNew = new DashboardPanelCreateLayerAssetTypeStrategy() + const strategyReorder = new DashboardPanelUpdateLayerAssetTypeOrderStrategy() + const strategyActions = new DashboardPanelLayerAssetActionStrategy() + + return ( + + ) +} diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.layer.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.layer.tsx index aa86570b..1e922afc 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.layer.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.layer.tsx @@ -1,9 +1,11 @@ -import { type ILayerWithDesigns } from '#app/models/layer/layer.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { PanelLayerAssets } from './sidebars.panel.assets.layer' import { PanelLayerDesigns } from './sidebars.panel.designs.layer' -export const PanelLayer = ({ layer }: { layer: ILayerWithDesigns }) => { +export const PanelLayer = ({ layer }: { layer: ILayerWithChildren }) => { return (
+
) diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx index c475a346..0937e956 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.tsx @@ -1,7 +1,7 @@ import { Sidebar } from '#app/components/layout' import { SidebarTabs, SidebarTabsContent } from '#app/components/templates' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' -import { type ILayerWithDesigns } from '#app/models/layer/layer.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' import { PanelArtworkVersion } from './sidebars.panel.artwork-version' import { PanelArtworkVersionImages } from './sidebars.panel.artwork-version.images' import { PanelArtworkVersionLayers } from './sidebars.panel.artwork-version.layers' @@ -14,7 +14,7 @@ export const SidebarLeft = ({ }) => { return ( - + @@ -31,7 +31,7 @@ export const SidebarRight = ({ selectedLayer, }: { version: IArtworkVersionWithChildren - selectedLayer: ILayerWithDesigns | undefined + selectedLayer: ILayerWithChildren | undefined }) => { return ( diff --git a/app/routes/resources+/api.v1+/asset.image.layer.create.tsx b/app/routes/resources+/api.v1+/asset.image.layer.create.tsx new file mode 100644 index 00000000..bbe6d7e6 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.layer.create.tsx @@ -0,0 +1,96 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, + unstable_createMemoryUploadHandler as createMemoryUploadHandler, + unstable_parseMultipartFormData as parseMultipartFormData, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherImageUpload } from '#app/components/templates/form/fetcher-image-upload' +import { validateNewAssetImageLayerSubmission } from '#app/models/asset/image/image.create.layer.server' +import { type ILayer } from '#app/models/layer/layer.server' +import { MAX_UPLOAD_SIZE } from '#app/schema/asset/image' +import { NewAssetImageLayerSchema } from '#app/schema/asset/image.layer' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageLayerCreateService } from '#app/services/asset.image.layer.create.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.LAYER.CREATE +const schema = NewAssetImageLayerSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await parseMultipartFormData( + request, + createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), + ) + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateNewAssetImageLayerSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageLayerCreateService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageLayerCreate = ({ layer }: { layer: ILayer }) => { + const layerId = layer.id + const formId = `asset-image-layer-${layerId}-create` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ +
+
+ ) +} diff --git a/app/routes/resources+/layer.$layerId.images.$imageId.tsx b/app/routes/resources+/layer.$layerId.images.$imageId.tsx new file mode 100644 index 00000000..8fd01d11 --- /dev/null +++ b/app/routes/resources+/layer.$layerId.images.$imageId.tsx @@ -0,0 +1,26 @@ +import { invariantResponse } from '@epic-web/invariant' +import { type LoaderFunctionArgs } from '@remix-run/node' +import { getAssetImageLayerSrc } from '#app/models/asset/image/image.get.layer.server' +import { requireUserId } from '#app/utils/auth.server' + +export async function loader({ params, request }: LoaderFunctionArgs) { + const userId = await requireUserId(request) + invariantResponse(params.layerId, 'Layer ID is required', { status: 400 }) + invariantResponse(params.imageId, 'Image ID is required', { status: 400 }) + const image = await getAssetImageLayerSrc({ + id: params.imageId, + layerId: params.layerId, + ownerId: userId, + }) + + invariantResponse(image, 'Not found', { status: 404 }) + + return new Response(image.blob, { + headers: { + 'Content-Type': image.contentType, + 'Content-Length': Buffer.byteLength(image.blob).toString(), + 'Content-Disposition': `inline; filename="${params.imageId}"`, + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }) +} diff --git a/app/schema/asset/image.layer.ts b/app/schema/asset/image.layer.ts new file mode 100644 index 00000000..f6d62f40 --- /dev/null +++ b/app/schema/asset/image.layer.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { + AssetImageDataSchema, + DeleteAssetImageSchema, + EditAssetImageSchema, + NewAssetImageSchema, +} from './image' + +const LayerParentSchema = z.object({ + layerId: z.string(), +}) + +export const AssetImageLayerDataSchema = + AssetImageDataSchema.merge(LayerParentSchema) + +export const AssetImageLayerVisibleDataSchema = z.object({ + visible: z.boolean(), +}) + +export const NewAssetImageLayerSchema = + NewAssetImageSchema.merge(LayerParentSchema) + +export const EditAssetImageLayerSchema = + EditAssetImageSchema.merge(LayerParentSchema) + +export const EditVisibleAssetImageLayerSchema = z.object({ + id: z.string(), + layerId: z.string(), +}) + +export const DeleteAssetImageLayerSchema = + DeleteAssetImageSchema.merge(LayerParentSchema) diff --git a/app/services/asset.image.artwork-version.create.service.ts b/app/services/asset.image.artwork-version.create.service.ts index 2faf09ff..e234ea20 100644 --- a/app/services/asset.image.artwork-version.create.service.ts +++ b/app/services/asset.image.artwork-version.create.service.ts @@ -46,6 +46,7 @@ export const assetImageArtworkVersionCreateService = async ({ lastModified, filename, }, + visible: true, ownerId: userId, artworkVersionId, } diff --git a/app/services/asset.image.artwork.create.service.ts b/app/services/asset.image.artwork.create.service.ts index 8821a9d9..5033870b 100644 --- a/app/services/asset.image.artwork.create.service.ts +++ b/app/services/asset.image.artwork.create.service.ts @@ -46,6 +46,7 @@ export const assetImageArtworkCreateService = async ({ lastModified, filename, }, + visible: true, ownerId: userId, artworkId, } diff --git a/app/services/asset.image.layer.create.service.ts b/app/services/asset.image.layer.create.service.ts new file mode 100644 index 00000000..98b7bded --- /dev/null +++ b/app/services/asset.image.layer.create.service.ts @@ -0,0 +1,76 @@ +import { invariant } from '@epic-web/invariant' +import { + type IAssetImageLayerCreateSubmission, + createAssetImageLayer, +} from '#app/models/asset/image/image.create.layer.server' +import { type IAssetImageCreatedResponse } from '#app/models/asset/image/image.create.server' +import { getLayer } from '#app/models/layer/layer.get.server' +import { AssetTypeEnum } from '#app/schema/asset' +import { AssetImageLayerDataSchema } from '#app/schema/asset/image.layer' +import { prisma } from '#app/utils/db.server' + +export const assetImageLayerCreateService = async ({ + userId, + layerId, + name, + description, + blob, + altText, + contentType, + height, + width, + size, + lastModified, + filename, +}: IAssetImageLayerCreateSubmission): Promise => { + try { + // Step 1: verify the layer exists + const layer = await getLayer({ + where: { id: layerId, ownerId: userId }, + }) + invariant(layer, 'Layer not found') + + // Step 2: validate asset image data + // zod schema for blob Buffer/File is not working + // pass in separately from validation + const data = { + name, + description, + type: AssetTypeEnum.IMAGE, + attributes: { + altText: altText || 'No alt text provided.', + contentType, + height, + width, + size, + lastModified, + filename, + }, + visible: true, + ownerId: userId, + layerId, + } + const assetImageData = AssetImageLayerDataSchema.parse(data) + + // Step 3: create the asset image via promise + const createAssetImagePromise = createAssetImageLayer({ + data: { ...assetImageData, blob }, + }) + + // Step 4: execute the transaction + const [createdAssetImage] = await prisma.$transaction([ + createAssetImagePromise, + ]) + + return { + createdAssetImage, + success: true, + } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageLayerCreateService', + } + } +} diff --git a/app/strategies/component/dashboard-panel/create-entity.strategy.ts b/app/strategies/component/dashboard-panel/create-entity.strategy.ts index 178b0b75..591c4287 100644 --- a/app/strategies/component/dashboard-panel/create-entity.strategy.ts +++ b/app/strategies/component/dashboard-panel/create-entity.strategy.ts @@ -31,6 +31,13 @@ export class DashboardPanelCreateArtworkVersionLayerStrategy parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION } +export class DashboardPanelCreateLayerAssetTypeStrategy + implements IDashboardPanelCreateEntityStrategy +{ + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.LAYER +} + export class DashboardPanelCreateLayerDesignTypeStrategy implements IDashboardPanelCreateEntityStrategy { diff --git a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts index 8b71243d..4d915adf 100644 --- a/app/strategies/component/dashboard-panel/delete-entity.strategy.ts +++ b/app/strategies/component/dashboard-panel/delete-entity.strategy.ts @@ -36,6 +36,14 @@ export class DashboardPanelDeleteArtworkVersionLayerStrategy parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION } +export class DashboardPanelDeleteLayerAssetStrategy + implements IDashboardPanelDeleteEntityStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.DELETE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.LAYER +} + export class DashboardPanelDeleteLayerDesignStrategy implements IDashboardPanelDeleteEntityStrategy { diff --git a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts index 86896749..b29cb29a 100644 --- a/app/strategies/component/dashboard-panel/entity-action/entity-action.ts +++ b/app/strategies/component/dashboard-panel/entity-action/entity-action.ts @@ -1,6 +1,7 @@ import { DashboardPanelDeleteArtworkVersionAssetStrategy, DashboardPanelDeleteArtworkVersionDesignStrategy, + DashboardPanelDeleteLayerAssetStrategy, DashboardPanelDeleteLayerDesignStrategy, type IDashboardPanelDeleteEntityStrategy, } from '../delete-entity.strategy' @@ -9,6 +10,7 @@ import { DashboardPanelUpdateArtworkVersionAssetVisibleStrategy, DashboardPanelUpdateArtworkVersionDesignVisibleStrategy, DashboardPanelUpdateArtworkVersionLayerVisibleStrategy, + DashboardPanelUpdateLayerAssetVisibleStrategy, DashboardPanelUpdateLayerDesignVisibleStrategy, type IDashboardPanelUpdateEntityVisibleStrategy, } from '../update-entity-visible.strategy' @@ -62,6 +64,18 @@ export class DashboardPanelArtworkVersionLayerActionStrategy } } +export class DashboardPanelLayerAssetActionStrategy + implements IDashboardPanelEntityActionStrategy +{ + getPanelActions(): IPanelEntityActionStrategy[] { + const strategyToggleVisible = + new DashboardPanelUpdateLayerAssetVisibleStrategy() + const strategyDelete = new DashboardPanelDeleteLayerAssetStrategy() + + return [strategyToggleVisible, strategyDelete] + } +} + export class DashboardPanelLayerDesignActionStrategy implements IDashboardPanelEntityActionStrategy { diff --git a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts index 4c314863..dfe32679 100644 --- a/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-order.strategy.ts @@ -31,6 +31,13 @@ export class DashboardPanelUpdateArtworkVersionLayerTypeOrderStrategy parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION } +export class DashboardPanelUpdateLayerAssetTypeOrderStrategy + implements IDashboardPanelUpdateEntityOrderStrategy +{ + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.LAYER +} + export class DashboardPanelUpdateLayerDesignTypeOrderStrategy implements IDashboardPanelUpdateEntityOrderStrategy { diff --git a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts index 97020291..259db344 100644 --- a/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts +++ b/app/strategies/component/dashboard-panel/update-entity-visible.strategy.ts @@ -37,6 +37,14 @@ export class DashboardPanelUpdateArtworkVersionLayerVisibleStrategy parentType: entityParentTypeEnum = EntityParentType.ARTWORK_VERSION } +export class DashboardPanelUpdateLayerAssetVisibleStrategy + implements IDashboardPanelUpdateEntityVisibleStrategy +{ + actionType: entityActionTypeEnum = EntityActionType.TOGGLE_VISIBLE + entityType: entityTypeEnum = EntityType.ASSET + parentType: entityParentTypeEnum = EntityParentType.LAYER +} + export class DashboardPanelUpdateLayerDesignVisibleStrategy implements IDashboardPanelUpdateEntityVisibleStrategy { diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index c2204e93..bbfbf36f 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -50,6 +50,11 @@ export const Routes = { UPDATE: `${pathBase}/asset/image/artwork-version/update`, UPDATE_VISIBLE: `${pathBase}/asset/image/artwork-version/update/visible`, }, + LAYER: { + CREATE: `${pathBase}/asset/image/layer/create`, + DELETE: `${pathBase}/asset/image/layer/delete`, + UPDATE: `${pathBase}/asset/image/layer/update`, + }, UPDATE: { FIT: `${pathBase}/asset/image/update/fit`, }, From c1416359220047a7480c25668fbec2b5850d8a4f Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 15 Jun 2024 14:08:23 -0400 Subject: [PATCH 35/54] can toggle visible images to layer --- ...rd-entity-panel.actions.toggle-visible.tsx | 8 ++ app/models/asset/image/image.get.server.ts | 1 + .../image.update.layer.visible.server.ts | 45 ++++++++ .../asset.image.layer.update.visible.tsx | 101 ++++++++++++++++++ ...sset.image.layer.update.visible.service.ts | 51 +++++++++ app/utils/routes.const.ts | 1 + 6 files changed, 207 insertions(+) create mode 100644 app/models/asset/image/image.update.layer.visible.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.layer.update.visible.tsx create mode 100644 app/services/asset.image.layer.update.visible.service.ts diff --git a/app/components/templates/panel/dashboard-entity-panel.actions.toggle-visible.tsx b/app/components/templates/panel/dashboard-entity-panel.actions.toggle-visible.tsx index 09595707..c4d88c20 100644 --- a/app/components/templates/panel/dashboard-entity-panel.actions.toggle-visible.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.actions.toggle-visible.tsx @@ -6,6 +6,7 @@ import { type ILayer } from '#app/models/layer/layer.server' import { ArtworkVersionDesignToggleVisible } from '#app/routes/resources+/api.v1+/artwork-version.design.update.visible' import { ArtworkVersionLayerToggleVisible } from '#app/routes/resources+/api.v1+/artwork-version.layer.update.visible' import { AssetImageArtworkVersionUpdateVisible } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.update.visible' +import { AssetImageLayerUpdateVisible } from '#app/routes/resources+/api.v1+/asset.image.layer.update.visible' import { LayerDesignToggleVisible } from '#app/routes/resources+/api.v1+/layer.design.update.visible' import { type entityParentTypeEnum, @@ -60,6 +61,13 @@ ArtworkVersionToggleVisibleChildEntityForm.displayName = const LayerToggleVisibleChildEntityForm = memo( ({ entityType, entity, parent }: ToggleVisibleChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditVisibleAssetImageLayerSchema, + strategy, + }) +} + +export interface IAssetImageLayerUpdateVisibleSubmission { + id: IAssetImage['id'] + userId: IUser['id'] + layerId: ILayer['id'] +} + +interface IAssetImageLayerUpdateVisibleData { + visible: boolean +} + +export const updateAssetImageLayerVisible = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageLayerUpdateVisibleData +}) => { + return prisma.asset.update({ + where: { id }, + data, + }) +} diff --git a/app/routes/resources+/api.v1+/asset.image.layer.update.visible.tsx b/app/routes/resources+/api.v1+/asset.image.layer.update.visible.tsx new file mode 100644 index 00000000..9eb76700 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.layer.update.visible.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconButton } from '#app/components/templates/form/fetcher-icon-button' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditVisibleAssetImageLayerSubmission } from '#app/models/asset/image/image.update.layer.visible.server' +import { type ILayer } from '#app/models/layer/layer.server' +import { EditVisibleAssetImageLayerSchema } from '#app/schema/asset/image.layer' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageLayerUpdateVisibleService } from '#app/services/asset.image.layer.update.visible.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.LAYER.UPDATE_VISIBLE +const schema = EditVisibleAssetImageLayerSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = + await validateEditVisibleAssetImageLayerSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageLayerUpdateVisibleService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageLayerUpdateVisible = ({ + image, + layer, +}: { + image: IAssetImage + layer: ILayer +}) => { + const imageId = image.id + const layerId = layer.id + const isVisible = image.visible + const icon = isVisible ? 'eye-open' : 'eye-closed' + const iconText = `${isVisible ? 'Hide' : 'Show'} ${image.name}` + const formId = `asset-image-${imageId}-layer-${layerId}-update-visible` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/services/asset.image.layer.update.visible.service.ts b/app/services/asset.image.layer.update.visible.service.ts new file mode 100644 index 00000000..ecf90c94 --- /dev/null +++ b/app/services/asset.image.layer.update.visible.service.ts @@ -0,0 +1,51 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + type IAssetImageLayerUpdateVisibleSubmission, + updateAssetImageLayerVisible, +} from '#app/models/asset/image/image.update.layer.visible.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetImageLayerVisibleDataSchema } from '#app/schema/asset/image.layer' +import { prisma } from '#app/utils/db.server' + +export const assetImageLayerUpdateVisibleService = async ({ + userId, + id, + layerId, +}: IAssetImageLayerUpdateVisibleSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, layerId, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + + // Step 2: validate asset image data + const data = { + visible: !assetImage.visible, + } + const assetImageData = AssetImageLayerVisibleDataSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageLayerVisible({ + id, + data: { ...assetImageData }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageLayerUpdateVisibleService', + } + } +} diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index bbfbf36f..b5a778c0 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -54,6 +54,7 @@ export const Routes = { CREATE: `${pathBase}/asset/image/layer/create`, DELETE: `${pathBase}/asset/image/layer/delete`, UPDATE: `${pathBase}/asset/image/layer/update`, + UPDATE_VISIBLE: `${pathBase}/asset/image/layer/update/visible`, }, UPDATE: { FIT: `${pathBase}/asset/image/update/fit`, From a0521298837aed6decc32cb1dc92dced74a7e874 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sat, 15 Jun 2024 14:14:23 -0400 Subject: [PATCH 36/54] can delete images to layer --- .../dashboard-entity-panel.actions.delete.tsx | 9 ++ .../asset/image/image.delete.layer.server.ts | 20 ++++ .../api.v1+/asset.image.layer.delete.tsx | 100 ++++++++++++++++++ .../asset.image.layer.delete.service.ts | 31 ++++++ 4 files changed, 160 insertions(+) create mode 100644 app/models/asset/image/image.delete.layer.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.layer.delete.tsx create mode 100644 app/services/asset.image.layer.delete.service.ts diff --git a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx index 854b31f8..a0eb66f5 100644 --- a/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.actions.delete.tsx @@ -2,8 +2,10 @@ import { memo, useCallback } from 'react' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' import { type IAssetImage } from '#app/models/asset/image/image.server' import { type IDesign } from '#app/models/design/design.server' +import { type ILayer } from '#app/models/layer/layer.server' import { ArtworkVersionDesignDelete } from '#app/routes/resources+/api.v1+/artwork-version.design.delete' import { AssetImageArtworkVersionDelete } from '#app/routes/resources+/api.v1+/asset.image.artwork-version.delete' +import { AssetImageLayerDelete } from '#app/routes/resources+/api.v1+/asset.image.layer.delete' import { LayerDesignDelete } from '#app/routes/resources+/api.v1+/layer.design.delete' import { type entityParentTypeEnum, @@ -58,6 +60,13 @@ ArtworkVersionDeleteChildEntityForm.displayName = const LayerDeleteChildEntityForm = memo( ({ entityType, entity, parent }: DeleteChildEntityFormProps) => { switch (entityType) { + case EntityType.ASSET: + return ( + + ) case EntityType.DESIGN: return ( diff --git a/app/models/asset/image/image.delete.layer.server.ts b/app/models/asset/image/image.delete.layer.server.ts new file mode 100644 index 00000000..226cde1a --- /dev/null +++ b/app/models/asset/image/image.delete.layer.server.ts @@ -0,0 +1,20 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { DeleteAssetImageLayerSchema } from '#app/schema/asset/image.layer' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' + +export const validateDeleteAssetImageLayerSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + // not validateEntityImageSubmission + // there is no image file to parse and transform + return await validateEntitySubmission({ + userId, + formData, + schema: DeleteAssetImageLayerSchema, + strategy, + }) +} diff --git a/app/routes/resources+/api.v1+/asset.image.layer.delete.tsx b/app/routes/resources+/api.v1+/asset.image.layer.delete.tsx new file mode 100644 index 00000000..0b094bb3 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.layer.delete.tsx @@ -0,0 +1,100 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconConfirm } from '#app/components/templates/form/fetcher-icon-confirm' +import { validateDeleteAssetImageLayerSubmission } from '#app/models/asset/image/image.delete.layer.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type ILayer } from '#app/models/layer/layer.server' +import { DeleteAssetImageLayerSchema } from '#app/schema/asset/image.layer' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageLayerDeleteService } from '#app/services/asset.image.layer.delete.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.LAYER.DELETE +const schema = DeleteAssetImageLayerSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let createSuccess = false + let errorMessage = '' + const { status, submission } = await validateDeleteAssetImageLayerSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageLayerDeleteService({ + userId, + ...submission.value, + }) + + createSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !createSuccess ? 422 : 201, + }, + ) +} + +export const AssetImageLayerDelete = ({ + image, + layer, +}: { + image: IAssetImage + layer: ILayer +}) => { + const imageId = image.id + const layerId = layer.id + const iconText = `Delete Image...` + const formId = `asset-image-${imageId}-layer-${layerId}-delete` + + const fetcher = useFetcher() + let isHydrated = useHydrated() + + return ( + +
+ + +
+
+ ) +} diff --git a/app/services/asset.image.layer.delete.service.ts b/app/services/asset.image.layer.delete.service.ts new file mode 100644 index 00000000..471bc78d --- /dev/null +++ b/app/services/asset.image.layer.delete.service.ts @@ -0,0 +1,31 @@ +import { + deleteAssetImage, + type IAssetImageDeletedResponse, +} from '#app/models/asset/image/image.delete.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { type IUser } from '#app/models/user/user.server' +import { prisma } from '#app/utils/db.server' + +export const assetImageLayerDeleteService = async ({ + userId, + id, +}: { + userId: IUser['id'] + id: IAssetImage['id'] +}): Promise => { + try { + // Step 1: delete the asset image via promise + const deleteAssetImageLayerPromise = deleteAssetImage({ id }) + + // Step 2: execute the transaction + await prisma.$transaction([deleteAssetImageLayerPromise]) + + return { success: true } + } catch (error) { + console.log(error) + return { + success: false, + message: 'Unknown error: assetImageLayerDeleteService', + } + } +} From 0d5da1a5d01e36fa0d655b1631221a1c964068f1 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Sun, 16 Jun 2024 18:52:22 -0400 Subject: [PATCH 37/54] generator includes assets; moved utils/asset.ts to app/model/asset/utils.ts --- app/definitions/artwork-generator.ts | 3 +++ .../artwork-version.get.server.ts | 2 +- app/models/artwork/artwork.get.server.ts | 2 +- app/models/asset/image/hooks.ts | 2 +- app/models/asset/image/image.get.server.ts | 2 +- app/{utils/asset.ts => models/asset/utils.ts} | 2 +- .../__components/sidebars.panel.assets.tsx | 9 ++++--- .../version/generator/build.service.ts | 25 +++++++++++-------- .../layer/build/build-draw-layers.service.ts | 1 + .../canvas/layer/draw/draw-layers.service.ts | 1 + app/utils/layer.utils.ts | 12 ++++++--- 11 files changed, 40 insertions(+), 21 deletions(-) rename app/{utils/asset.ts => models/asset/utils.ts} (98%) diff --git a/app/definitions/artwork-generator.ts b/app/definitions/artwork-generator.ts index 193920f6..e9f8a8bf 100644 --- a/app/definitions/artwork-generator.ts +++ b/app/definitions/artwork-generator.ts @@ -1,6 +1,7 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetByType } from '#app/models/asset/asset.server' import { type IFill } from '#app/models/design-type/fill/fill.server' import { type ILayout } from '#app/models/design-type/layout/layout.server' import { type ILine } from '#app/models/design-type/line/line.server' @@ -72,6 +73,8 @@ export interface ILayerGenerator extends IGeneratorDesigns { // create this design type container: ILayerGeneratorContainer + + assets: IAssetByType } // TODO: make container a design type diff --git a/app/models/artwork-version/artwork-version.get.server.ts b/app/models/artwork-version/artwork-version.get.server.ts index 568acb3a..b281152a 100644 --- a/app/models/artwork-version/artwork-version.get.server.ts +++ b/app/models/artwork-version/artwork-version.get.server.ts @@ -1,9 +1,9 @@ import { invariant } from '@epic-web/invariant' import { z } from 'zod' import { zodStringOrNull } from '#app/schema/zod-helpers' -import { deserializeAssets } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' import { assetSelect } from '../asset/asset.get.server' +import { deserializeAssets } from '../asset/utils' import { type IArtworkVersion, type IArtworkVersionWithChildren, diff --git a/app/models/artwork/artwork.get.server.ts b/app/models/artwork/artwork.get.server.ts index 4dd40585..5a44ddfd 100644 --- a/app/models/artwork/artwork.get.server.ts +++ b/app/models/artwork/artwork.get.server.ts @@ -1,6 +1,5 @@ import { invariant } from '@epic-web/invariant' import { z } from 'zod' -import { deserializeAssets } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' import { type IArtworkWithProject, @@ -9,6 +8,7 @@ import { type IArtworkWithAssets, } from '../artwork/artwork.server' import { assetSelect } from '../asset/asset.get.server' +import { deserializeAssets } from '../asset/utils' export type queryArtworkWhereArgsType = z.infer const whereArgs = z.object({ diff --git a/app/models/asset/image/hooks.ts b/app/models/asset/image/hooks.ts index 3c387e0e..5b761a56 100644 --- a/app/models/asset/image/hooks.ts +++ b/app/models/asset/image/hooks.ts @@ -1,7 +1,7 @@ import { type IArtworkWithAssets } from '#app/models/artwork/artwork.server' import { useArtworkFromVersion } from '#app/models/artwork/hooks' import { AssetTypeEnum } from '#app/schema/asset' -import { filterAssetType } from '#app/utils/asset' +import { filterAssetType } from '../utils' import { type IAssetImage } from './image.server' export function useAssetImagesArtwork(): IAssetImage[] { diff --git a/app/models/asset/image/image.get.server.ts b/app/models/asset/image/image.get.server.ts index 1dd56e8e..59e586ab 100644 --- a/app/models/asset/image/image.get.server.ts +++ b/app/models/asset/image/image.get.server.ts @@ -1,8 +1,8 @@ import { invariant } from '@epic-web/invariant' import { z } from 'zod' import { AssetTypeEnum } from '#app/schema/asset' -import { deserializeAsset } from '#app/utils/asset' import { prisma } from '#app/utils/db.server' +import { deserializeAsset } from '../utils' import { type IAssetImage } from './image.server' export type queryAssetImageWhereArgsType = z.infer diff --git a/app/utils/asset.ts b/app/models/asset/utils.ts similarity index 98% rename from app/utils/asset.ts rename to app/models/asset/utils.ts index 923a9c36..be3cebd0 100644 --- a/app/utils/asset.ts +++ b/app/models/asset/utils.ts @@ -75,7 +75,7 @@ export const filterAssetType = ({ return assets.filter(asset => asset.type === type) } -export const filterAssetsByType = ({ +export const groupAssetsByType = ({ assets, }: { assets: IAssetParsed[] diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx index 4d3a1621..30dba061 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.assets.tsx @@ -3,10 +3,13 @@ import { type IAssetParent, type IAssetsByTypeWithType, } from '#app/models/asset/asset.server' +import { + assetsByTypeToPanelArray, + groupAssetsByType, +} from '#app/models/asset/utils' import { type IDashboardPanelCreateEntityStrategy } from '#app/strategies/component/dashboard-panel/create-entity.strategy' import { type IDashboardPanelEntityActionStrategy } from '#app/strategies/component/dashboard-panel/entity-action/entity-action' import { type IDashboardPanelUpdateEntityOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' -import { assetsByTypeToPanelArray, filterAssetsByType } from '#app/utils/asset' export const PanelAssets = ({ parent, @@ -19,11 +22,11 @@ export const PanelAssets = ({ strategyReorder: IDashboardPanelUpdateEntityOrderStrategy strategyActions: IDashboardPanelEntityActionStrategy }) => { - const assetsByType = filterAssetsByType({ + const groupedAssetsByType = groupAssetsByType({ assets: parent.assets, }) const assetTypePanels = assetsByTypeToPanelArray({ - assets: assetsByType, + assets: groupedAssetsByType, }) return ( diff --git a/app/services/artwork/version/generator/build.service.ts b/app/services/artwork/version/generator/build.service.ts index 914ddd54..26ae44f3 100644 --- a/app/services/artwork/version/generator/build.service.ts +++ b/app/services/artwork/version/generator/build.service.ts @@ -7,6 +7,7 @@ import { type IArtworkVersionGeneratorMetadata, } from '#app/definitions/artwork-generator' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { groupAssetsByType } from '#app/models/asset/utils' import { findManyDesignsWithType, type IDesignWithType, @@ -20,10 +21,7 @@ import { getLayerVisibleRotates, } from '#app/models/design-layer/design-layer.server' import { type IRotate } from '#app/models/design-type/rotate/rotate.server' -import { - type ILayerWithDesigns, - type ILayer, -} from '#app/models/layer/layer.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' import { type rotateBasisTypeEnum } from '#app/schema/rotate' import { prisma } from '#app/utils/db.server' import { @@ -73,7 +71,7 @@ export const artworkVersionGeneratorBuildService = async ({ // Step 4: build the generator layers // each layer can override any of the global settings - const orderedLayers = await orderLinkedItems( + const orderedLayers = await orderLinkedItems( version.layers, ) const generatorLayers = await buildGeneratorLayers({ @@ -197,11 +195,14 @@ const buildDefaultGeneratorLayer = async ({ // container defaults to version dimensions const container = getArtworkVersionContainer({ version }) + const assets = groupAssetsByType({ assets: version.assets }) + return { ...defaultGeneratorDesigns, palette: palettes, rotates, container, + assets, } } @@ -228,10 +229,10 @@ const buildGeneratorLayers = async ({ layers, defaultGeneratorLayer, }: { - layers: ILayer[] + layers: ILayerWithChildren[] defaultGeneratorLayer: ILayerGenerator }) => { - const visibleLayers = filterLayersVisible({ layers }) + const visibleLayers = filterLayersVisible({ layers }) as ILayerWithChildren[] return await Promise.all( visibleLayers.map(layer => @@ -247,7 +248,7 @@ const buildGeneratorLayer = async ({ layer, defaultGeneratorLayer, }: { - layer: ILayer + layer: ILayerWithChildren defaultGeneratorLayer: ILayerGenerator }): Promise => { const layerId = layer.id @@ -272,12 +273,16 @@ const buildGeneratorLayer = async ({ // and the layer generator designs as overrides // and layer details const { id, name, description } = layer + + const assets = groupAssetsByType({ assets: layer.assets }) + const layerGenerator = { ...defaultGeneratorLayer, ...layerGeneratorDesigns, id, name, description, + assets, } // Step 5: get all visible palettes to use for fill or stroke @@ -307,7 +312,7 @@ const buildGeneratorLayer = async ({ const getLayerSelectedDesigns = async ({ layerId, }: { - layerId: ILayer['id'] + layerId: ILayerWithChildren['id'] }) => { return await findManyDesignsWithType({ where: { layerId, selected: true }, @@ -320,7 +325,7 @@ const getRotates = async ({ rotate, }: { artworkVersionId?: IArtworkVersionWithChildren['id'] - layerId?: ILayer['id'] + layerId?: ILayerWithChildren['id'] rotate: IRotate }) => { const allRotates = isArrayRotateBasisType(rotate.basis as rotateBasisTypeEnum) diff --git a/app/services/canvas/layer/build/build-draw-layers.service.ts b/app/services/canvas/layer/build/build-draw-layers.service.ts index c941a6f5..e72dd504 100644 --- a/app/services/canvas/layer/build/build-draw-layers.service.ts +++ b/app/services/canvas/layer/build/build-draw-layers.service.ts @@ -24,6 +24,7 @@ export const canvasLayerBuildDrawLayersService = ({ const drawLayers = [] for (let i = 0; i < layers.length; i++) { const layer = layers[i] + console.log('canvasLayerBuildDrawLayersService layer.assets', layer.assets) const layerDrawItems = buildLayerGenerationItems({ ctx, layer }) drawLayers.push(layerDrawItems) } diff --git a/app/services/canvas/layer/draw/draw-layers.service.ts b/app/services/canvas/layer/draw/draw-layers.service.ts index 27aac4e6..349cbddc 100644 --- a/app/services/canvas/layer/draw/draw-layers.service.ts +++ b/app/services/canvas/layer/draw/draw-layers.service.ts @@ -23,6 +23,7 @@ const drawLayerItems = ({ }) => { for (let i = 0; i < layerDrawItems.length; i++) { const layerDrawItem = layerDrawItems[i] + console.log('count: ', i) drawLayerItemService({ ctx, layerDrawItem }) } } diff --git a/app/utils/layer.utils.ts b/app/utils/layer.utils.ts index b8e05ef1..5f00eea5 100644 --- a/app/utils/layer.utils.ts +++ b/app/utils/layer.utils.ts @@ -1,9 +1,15 @@ -import { type ILayer } from '#app/models/layer/layer.server' +import { + type ILayerWithDesigns, + type ILayer, + type ILayerWithChildren, +} from '#app/models/layer/layer.server' + +type FilteredLayer = ILayer | ILayerWithDesigns | ILayerWithChildren export const filterLayersVisible = ({ layers, }: { - layers: ILayer[] -}): ILayer[] => { + layers: FilteredLayer[] +}): FilteredLayer[] => { return layers.filter(layer => layer.visible) } From 2d49e0b99d9e89762cb2efe82280e580fff066ce Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 00:00:37 -0400 Subject: [PATCH 38/54] canvas cleanup and image load setup --- .../canvas/artwork-canvas.footer.tsx | 44 +++++ .../canvas/artwork-canvas.link-to-editor.tsx | 35 ++++ .../templates/canvas/artwork-canvas.tsx | 82 ++------ app/definitions/artwork-generator.ts | 6 + .../asset/image/image.generate.server.ts | 10 + app/schema/asset/image.ts | 10 +- app/services/canvas/draw.service.ts | 4 +- .../layer/build/build-draw-layers.service.ts | 34 +++- .../build-layer-draw-image.fit.service.ts | 186 ++++++++++++++++++ .../build/build-layer-draw-image.service.ts | 34 ++++ .../canvas/layer/draw/draw-layers.service.ts | 43 +++- app/utils/image.ts | 24 +++ 12 files changed, 425 insertions(+), 87 deletions(-) create mode 100644 app/components/templates/canvas/artwork-canvas.footer.tsx create mode 100644 app/components/templates/canvas/artwork-canvas.link-to-editor.tsx create mode 100644 app/models/asset/image/image.generate.server.ts create mode 100644 app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts create mode 100644 app/services/canvas/layer/build/build-layer-draw-image.service.ts create mode 100644 app/utils/image.ts diff --git a/app/components/templates/canvas/artwork-canvas.footer.tsx b/app/components/templates/canvas/artwork-canvas.footer.tsx new file mode 100644 index 00000000..302a0c1a --- /dev/null +++ b/app/components/templates/canvas/artwork-canvas.footer.tsx @@ -0,0 +1,44 @@ +import { memo, useCallback } from 'react' +import { FlexRow } from '#app/components/layout' +import { PanelIconButton } from '#app/components/ui/panel-icon-button' +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' +import { TooltipHydrated } from '../tooltip' +import { LinkToEditor } from './artwork-canvas.link-to-editor' +import { DownloadCanvas, ShareCanvas } from '.' + +interface CanvasFooterProps { + isHydrated: boolean + handleRefresh: () => void + canvasRef: React.RefObject + generator: IArtworkVersionGenerator +} + +export const CanvasFooter = memo( + ({ isHydrated, handleRefresh, canvasRef, generator }: CanvasFooterProps) => { + const { metadata } = generator + + const linkToEditor = useCallback( + () => + metadata ? ( + + ) : null, + [metadata, isHydrated], + ) + + return ( + + + + + + + {linkToEditor()} + + ) + }, +) +CanvasFooter.displayName = 'CanvasFooter' diff --git a/app/components/templates/canvas/artwork-canvas.link-to-editor.tsx b/app/components/templates/canvas/artwork-canvas.link-to-editor.tsx new file mode 100644 index 00000000..1aae137c --- /dev/null +++ b/app/components/templates/canvas/artwork-canvas.link-to-editor.tsx @@ -0,0 +1,35 @@ +import { memo } from 'react' +import { PanelIconLink } from '#app/components/ui/panel-icon-link' +import { type IArtworkVersionGeneratorMetadata } from '#app/definitions/artwork-generator' +import { useOptionalUser } from '#app/utils/user' +import { TooltipHydrated } from '../tooltip' + +export const LinkToEditor = memo( + ({ + metadata, + isHydrated, + }: { + metadata: IArtworkVersionGeneratorMetadata + isHydrated: boolean + }) => { + const { projectSlug, artworkSlug, branchSlug, versionSlug, ownerId } = + metadata + const user = useOptionalUser() + const isOwner = user?.id === ownerId + if (!isOwner) return null + + const editorPath = `/editor/projects/${projectSlug}/artworks/${artworkSlug}/${branchSlug}/${versionSlug}` + return ( +
+ + + +
+ ) + }, +) +LinkToEditor.displayName = 'LinkToEditor' diff --git a/app/components/templates/canvas/artwork-canvas.tsx b/app/components/templates/canvas/artwork-canvas.tsx index fb119be1..364ee8a8 100644 --- a/app/components/templates/canvas/artwork-canvas.tsx +++ b/app/components/templates/canvas/artwork-canvas.tsx @@ -1,46 +1,9 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useHydrated } from 'remix-utils/use-hydrated' -import { FlexColumn, FlexRow } from '#app/components/layout' -import { PanelIconButton } from '#app/components/ui/panel-icon-button' -import { PanelIconLink } from '#app/components/ui/panel-icon-link' -import { - type IArtworkVersionGeneratorMetadata, - type IArtworkVersionGenerator, -} from '#app/definitions/artwork-generator' +import { FlexColumn } from '#app/components/layout' +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { canvasDrawService } from '#app/services/canvas/draw.service' -import { useOptionalUser } from '#app/utils/user' -import { TooltipHydrated } from '../tooltip' -import { DownloadCanvas, ShareCanvas } from '.' - -const LinkToEditor = memo( - ({ - metadata, - isHydrated, - }: { - metadata: IArtworkVersionGeneratorMetadata - isHydrated: boolean - }) => { - const { projectSlug, artworkSlug, branchSlug, versionSlug, ownerId } = - metadata - const user = useOptionalUser() - const isOwner = user?.id === ownerId - if (!isOwner) return null - - const editorPath = `/editor/projects/${projectSlug}/artworks/${artworkSlug}/${branchSlug}/${versionSlug}` - return ( -
- - - -
- ) - }, -) -LinkToEditor.displayName = 'LinkToEditor' +import { CanvasFooter } from './artwork-canvas.footer' // The ArtworkCanvas component is wrapped in React.memo to optimize performance by memoizing the component. // This prevents unnecessary re-renders when the props passed to the component have not changed. @@ -48,7 +11,7 @@ LinkToEditor.displayName = 'LinkToEditor' // memoizing ensures that these operations are only re-executed when necessary, such as when the 'generator' prop changes. export const ArtworkCanvas = memo( ({ generator }: { generator: IArtworkVersionGenerator }) => { - const { metadata, settings } = generator + const { settings } = generator const { width, height, background } = settings const canvasRef = useRef(null) const [refresh, setRefresh] = useState(0) @@ -61,17 +24,19 @@ export const ArtworkCanvas = memo( } }, [canvasRef, generator, refresh]) - const linkToEditor = useCallback( - () => - metadata ? ( - - ) : null, - [metadata, isHydrated], - ) - - const handleRefresh = () => { - setRefresh(prev => prev + 1) - } + const canvasFooter = useCallback(() => { + const handleRefresh = () => { + setRefresh(prev => prev + 1) + } + return ( + + ) + }, [isHydrated, canvasRef, generator]) return ( @@ -83,18 +48,7 @@ export const ArtworkCanvas = memo( style={{ backgroundColor: `#${background}` }} className="h-full w-full" /> - - - - - - - {generator.metadata && linkToEditor()} - + {canvasFooter()} ) }, diff --git a/app/definitions/artwork-generator.ts b/app/definitions/artwork-generator.ts index e9f8a8bf..a0eff123 100644 --- a/app/definitions/artwork-generator.ts +++ b/app/definitions/artwork-generator.ts @@ -2,6 +2,7 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' import { type IAssetByType } from '#app/models/asset/asset.server' +import { type IAssetImageGeneration } from '#app/models/asset/image/image.generate.server' import { type IFill } from '#app/models/design-type/fill/fill.server' import { type ILayout } from '#app/models/design-type/layout/layout.server' import { type ILine } from '#app/models/design-type/line/line.server' @@ -93,9 +94,14 @@ export interface ILayerGeneratorContainer { export interface IGenerationLayer { generator: ILayerGenerator + assets: IGenerationAssets items: IGenerationItem[] } +export interface IGenerationAssets { + image: IAssetImageGeneration | null +} + export interface IGenerationItem { id: string fillStyle: string diff --git a/app/models/asset/image/image.generate.server.ts b/app/models/asset/image/image.generate.server.ts new file mode 100644 index 00000000..d3344edf --- /dev/null +++ b/app/models/asset/image/image.generate.server.ts @@ -0,0 +1,10 @@ +export interface IAssetImageDrawGeneration { + x: number + y: number + width: number + height: number +} + +export interface IAssetImageGeneration extends IAssetImageDrawGeneration { + src: string +} diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index d33ea3af..f4f98d60 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -35,11 +35,11 @@ const FileSchema = z // https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit export const AssetImageFitTypeEnum = { - FILL: 'fill', - CONTAIN: 'contain', - COVER: 'cover', - NONE: 'none', - SCALE_DOWN: 'scale-down', + FILL: 'fill', // stretch to fill content box + CONTAIN: 'contain', // scale to maintain aspect ratio, letterboxed + COVER: 'cover', // scale to maintain aspect ratio, overflow clipped + NONE: 'none', // not resized + SCALE_DOWN: 'scale-down', // none or contain, smaller of the two // add more asset fit types here } as const export type assetImageFitTypeEnum = ObjectValues diff --git a/app/services/canvas/draw.service.ts b/app/services/canvas/draw.service.ts index fac4fb8f..82cde916 100644 --- a/app/services/canvas/draw.service.ts +++ b/app/services/canvas/draw.service.ts @@ -1,3 +1,4 @@ +import { invariant } from '@epic-web/invariant' import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { canvasDrawBackgroundService } from './draw-background.service' import { canvasDrawWatermarkService } from './draw-watermark.service' @@ -22,6 +23,7 @@ export const canvasDrawService = ({ ctx, generator, }) + console.log('drawLayers count: ', drawLayers.length) // Step 4: draw layers to canvas canvasDrawLayersService({ ctx, drawLayers }) @@ -32,6 +34,6 @@ export const canvasDrawService = ({ const getContext = (canvas: HTMLCanvasElement) => { const ctx = canvas.getContext('2d') - if (!ctx) throw new Error('Canvas context not found') + invariant(ctx, 'Canvas context not found') return ctx } diff --git a/app/services/canvas/layer/build/build-draw-layers.service.ts b/app/services/canvas/layer/build/build-draw-layers.service.ts index e72dd504..32a9a69d 100644 --- a/app/services/canvas/layer/build/build-draw-layers.service.ts +++ b/app/services/canvas/layer/build/build-draw-layers.service.ts @@ -1,10 +1,13 @@ import { + type IGenerationLayer, type IArtworkVersionGenerator, type IGenerationItem, type ILayerGenerator, + type IGenerationAssets, } from '#app/definitions/artwork-generator' import { canvasBuildLayerDrawCountService } from './build-layer-draw-count.service' import { canvasBuildLayerDrawFillService } from './build-layer-draw-fill.service' +import { canvasBuildLayerDrawImageService } from './build-layer-draw-image.service' import { canvasBuildLayerDrawLineService } from './build-layer-draw-line.service' import { canvasBuildLayerDrawPositionService } from './build-layer-draw-position.service' import { canvasBuildLayerDrawRotateService } from './build-layer-draw-rotate.service' @@ -18,17 +21,32 @@ export const canvasLayerBuildDrawLayersService = ({ }: { ctx: CanvasRenderingContext2D generator: IArtworkVersionGenerator -}): IGenerationItem[][] => { +}): IGenerationLayer[] => { const { layers } = generator - const drawLayers = [] - for (let i = 0; i < layers.length; i++) { - const layer = layers[i] - console.log('canvasLayerBuildDrawLayersService layer.assets', layer.assets) - const layerDrawItems = buildLayerGenerationItems({ ctx, layer }) - drawLayers.push(layerDrawItems) + return layers.map(layer => { + const assets = buildLayerGenerationAssets({ ctx, layer }) + const items = buildLayerGenerationItems({ ctx, layer }) + return { + generator: layer, + assets, + items, + } + }) +} + +const buildLayerGenerationAssets = ({ + ctx, + layer, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator +}): IGenerationAssets => { + const image = canvasBuildLayerDrawImageService({ ctx, layer }) + + return { + image, } - return drawLayers } const buildLayerGenerationItems = ({ diff --git a/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts new file mode 100644 index 00000000..d54ae54b --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts @@ -0,0 +1,186 @@ +import { type IAssetImageDrawGeneration } from '#app/models/asset/image/image.generate.server' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { AssetImageFitTypeEnum } from '#app/schema/asset/image' + +// https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit + +const imageDimensions = ({ image }: { image: IAssetImage }) => { + const { attributes } = image + const { width, height } = attributes + return { width, height, ratio: width / height } +} + +const canvasDimensions = ({ canvas }: { canvas: HTMLCanvasElement }) => { + const { width, height } = canvas + return { width, height, ratio: width / height } +} + +const imageFitFill = ({ ctx }: { ctx: CanvasRenderingContext2D }) => { + const { width: canvasWidth, height: canvasHeight } = canvasDimensions({ + canvas: ctx.canvas, + }) + return { + x: 0, + y: 0, + width: canvasWidth, + height: canvasHeight, + } +} + +const imageFitContain = ({ + ctx, + image, +}: { + ctx: CanvasRenderingContext2D + image: IAssetImage +}) => { + const { + // width: imageWidth, + // height: imageHeight, + ratio: imageRatio, + } = imageDimensions({ image }) + const { + width: canvasWidth, + height: canvasHeight, + ratio: canvasRatio, + } = canvasDimensions({ canvas: ctx.canvas }) + + let width = canvasWidth + let height = canvasHeight + + if (imageRatio > canvasRatio) { + height = canvasWidth / imageRatio + } else { + width = canvasHeight * imageRatio + } + + return { + x: (canvasWidth - width) / 2, + y: (canvasHeight - height) / 2, + width, + height, + } +} + +const imageFitCover = ({ + ctx, + image, +}: { + ctx: CanvasRenderingContext2D + image: IAssetImage +}): IAssetImageDrawGeneration => { + const { + // width: imageWidth, + // height: imageHeight, + ratio: imageRatio, + } = imageDimensions({ image }) + const { + width: canvasWidth, + height: canvasHeight, + ratio: canvasRatio, + } = canvasDimensions({ canvas: ctx.canvas }) + + let width = canvasWidth + let height = canvasHeight + + if (imageRatio < canvasRatio) { + height = canvasWidth / imageRatio + } else { + width = canvasHeight * imageRatio + } + + return { + x: (canvasWidth - width) / 2, + y: (canvasHeight - height) / 2, + width, + height, + } +} + +const imageFitNone = ({ + ctx, + image, +}: { + ctx: CanvasRenderingContext2D + image: IAssetImage +}) => { + const { + width: imageWidth, + height: imageHeight, + // ratio: imageRatio, + } = imageDimensions({ image }) + const { + width: canvasWidth, + height: canvasHeight, + // ratio: canvasRatio, + } = canvasDimensions({ canvas: ctx.canvas }) + + return { + x: (canvasWidth - imageWidth) / 2, + y: (canvasHeight - imageHeight) / 2, + width: imageWidth, + height: imageHeight, + } +} + +const imageFitScaleDown = ({ + ctx, + image, +}: { + ctx: CanvasRenderingContext2D + image: IAssetImage +}) => { + const { + width: imageWidth, + height: imageHeight, + ratio: imageRatio, + } = imageDimensions({ image }) + const { + width: canvasWidth, + height: canvasHeight, + ratio: canvasRatio, + } = canvasDimensions({ canvas: ctx.canvas }) + + let width = imageWidth + let height = imageHeight + + if (imageWidth > canvasWidth || imageHeight > canvasHeight) { + if (imageRatio > canvasRatio) { + width = canvasWidth + height = canvasWidth / imageRatio + } else { + height = canvasHeight + width = canvasHeight * imageRatio + } + } + + return { + x: (canvasWidth - width) / 2, + y: (canvasHeight - height) / 2, + width, + height, + } +} + +export const canvasBuildLayerDrawImageFitService = ({ + ctx, + image, +}: { + ctx: CanvasRenderingContext2D + image: IAssetImage +}) => { + switch (image.attributes.fit) { + case AssetImageFitTypeEnum.FILL: + return imageFitFill({ ctx }) + case AssetImageFitTypeEnum.CONTAIN: + return imageFitContain({ ctx, image }) + case AssetImageFitTypeEnum.COVER: + return imageFitCover({ ctx, image }) + case AssetImageFitTypeEnum.NONE: + return imageFitNone({ ctx, image }) + case AssetImageFitTypeEnum.SCALE_DOWN: + return imageFitScaleDown({ ctx, image }) + default: + return null + } +} diff --git a/app/services/canvas/layer/build/build-layer-draw-image.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.service.ts new file mode 100644 index 00000000..13f74f52 --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-image.service.ts @@ -0,0 +1,34 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' +import { + type IAssetImageDrawGeneration, + type IAssetImageGeneration, +} from '#app/models/asset/image/image.generate.server' +import { getAssetImgSrc } from '#app/models/asset/image/utils' +import { canvasBuildLayerDrawImageFitService } from './build-layer-draw-image.fit.service' + +export const canvasBuildLayerDrawImageService = ({ + ctx, + layer, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator +}): IAssetImageGeneration | null => { + const { assets } = layer + const { assetImages } = assets + + if (!assetImages.length) return null + + // just one image to start + const image = assetImages[0] + + const src = getAssetImgSrc({ image }) + const fit = canvasBuildLayerDrawImageFitService({ + ctx, + image, + }) as IAssetImageDrawGeneration + + return { + src, + ...fit, + } +} diff --git a/app/services/canvas/layer/draw/draw-layers.service.ts b/app/services/canvas/layer/draw/draw-layers.service.ts index 349cbddc..cda10fc3 100644 --- a/app/services/canvas/layer/draw/draw-layers.service.ts +++ b/app/services/canvas/layer/draw/draw-layers.service.ts @@ -1,4 +1,9 @@ -import { type IGenerationItem } from '#app/definitions/artwork-generator' +import { + type IGenerationLayer, + type IGenerationItem, + type IGenerationAssets, +} from '#app/definitions/artwork-generator' +import { loadImage } from '#app/utils/image' import { drawLayerItemService } from './draw-layer-item.service' export const canvasDrawLayersService = ({ @@ -6,24 +11,44 @@ export const canvasDrawLayersService = ({ drawLayers, }: { ctx: CanvasRenderingContext2D - drawLayers: IGenerationItem[][] + drawLayers: IGenerationLayer[] }) => { for (let i = 0; i < drawLayers.length; i++) { - const layerDrawItems = drawLayers[i] - drawLayerItems({ ctx, layerDrawItems }) + const layer = drawLayers[i] + drawLayerAssets({ ctx, assets: layer.assets }) + drawLayerItems({ ctx, items: layer.items }) + } +} + +const drawLayerAssets = ({ + ctx, + assets, +}: { + ctx: CanvasRenderingContext2D + assets: IGenerationAssets +}) => { + const { image } = assets + if (image && image.src) { + console.log('assets.image: ', image) + const img = loadImage({ src: image.src }) + console.log('img: ', img) + // load image + // async + // const { x, y, width, height } = assets.image + // ctx.drawImage(img, x, y, width, height) } } const drawLayerItems = ({ ctx, - layerDrawItems, + items, }: { ctx: CanvasRenderingContext2D - layerDrawItems: IGenerationItem[] + items: IGenerationItem[] }) => { - for (let i = 0; i < layerDrawItems.length; i++) { - const layerDrawItem = layerDrawItems[i] - console.log('count: ', i) + for (let i = 0; i < items.length; i++) { + const layerDrawItem = items[i] + // console.log('count: ', i) drawLayerItemService({ ctx, layerDrawItem }) } } diff --git a/app/utils/image.ts b/app/utils/image.ts new file mode 100644 index 00000000..49ac6ac0 --- /dev/null +++ b/app/utils/image.ts @@ -0,0 +1,24 @@ +export const loadImage = async ({ + src, +}: { + src: string +}): Promise => { + return new Promise((resolve, reject) => { + const img = new Image() + + img.onload = () => { + resolve(img) + } + + img.onerror = () => { + reject(new Error(`Failed to load image from source: ${src}`)) + } + + // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image + // add this so we can use the image in a canvas + // and not get a tainted canvas error + img.crossOrigin = 'anonymous' + + img.src = src + }) +} From d8642f55afb358fb9154339b2823c6bba50d8f1c Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 05:33:12 -0400 Subject: [PATCH 39/54] arrange when image is on canvas for pixel data and whether to hide on canvas draw; rearranged some files for modularity --- ...hboard-entity-panel.values.asset.image.tsx | 5 + app/definitions/artwork-generator.ts | 13 +- app/models/asset/asset.generation.server.ts | 5 + .../asset/image/image.generate.server.ts | 13 +- app/models/asset/image/image.server.ts | 1 + .../image/image.update.hide-on-draw.server.ts | 49 +++ app/models/asset/utils.ts | 8 + .../asset.image.update.hide-on-draw.tsx | 101 +++++ app/schema/asset/image.ts | 5 + .../generator/build.container.service.ts | 20 + .../version/generator/build.layers.service.ts | 156 ++++++++ .../generator/build.metadata.service.ts | 77 ++++ .../generator/build.rotates.service.ts | 28 ++ .../version/generator/build.service.ts | 375 +----------------- .../version/generator/build.verify.service.ts | 66 +++ .../generator/build.watermark.service.ts | 31 ++ ...asset.image.update.hide-on-draw.service.ts | 52 +++ .../canvas/draw-background.service.ts | 23 ++ .../canvas/draw.load-assets.service.ts | 48 +++ app/services/canvas/draw.service.ts | 10 +- .../build-draw-layers.layer.assets.service.ts | 17 + .../build-draw-layers.layer.items.service.ts | 86 ++++ .../build/build-draw-layers.layer.service.ts | 25 ++ .../layer/build/build-draw-layers.service.ts | 99 +---- .../build-layer-draw-image.fit.service.ts | 6 +- .../build/build-layer-draw-image.service.ts | 30 +- .../build-layer-draw-position.grid.service.ts | 36 ++ ...build-layer-draw-position.pixel.service.ts | 96 +++++ .../build-layer-draw-position.service.ts | 135 +------ .../layer/draw/draw-layers.asset.service.ts | 27 ++ .../canvas/layer/draw/draw-layers.service.ts | 33 +- app/utils/routes.const.ts | 1 + 32 files changed, 1041 insertions(+), 636 deletions(-) create mode 100644 app/models/asset/asset.generation.server.ts create mode 100644 app/models/asset/image/image.update.hide-on-draw.server.ts create mode 100644 app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx create mode 100644 app/services/artwork/version/generator/build.container.service.ts create mode 100644 app/services/artwork/version/generator/build.layers.service.ts create mode 100644 app/services/artwork/version/generator/build.metadata.service.ts create mode 100644 app/services/artwork/version/generator/build.rotates.service.ts create mode 100644 app/services/artwork/version/generator/build.verify.service.ts create mode 100644 app/services/artwork/version/generator/build.watermark.service.ts create mode 100644 app/services/asset.image.update.hide-on-draw.service.ts create mode 100644 app/services/canvas/draw.load-assets.service.ts create mode 100644 app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts create mode 100644 app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts create mode 100644 app/services/canvas/layer/build/build-draw-layers.layer.service.ts create mode 100644 app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts create mode 100644 app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts create mode 100644 app/services/canvas/layer/draw/draw-layers.asset.service.ts diff --git a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx index 8e2901db..f0f26f72 100644 --- a/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx +++ b/app/components/templates/panel/dashboard-entity-panel.values.asset.image.tsx @@ -4,6 +4,7 @@ import { SidebarPanelPopoverFormContainer } from '#app/components/layout/popover import { type IAssetImage } from '#app/models/asset/image/image.server' import { getAssetImgSrc } from '#app/models/asset/image/utils' import { AssetImageUpdateFit } from '#app/routes/resources+/api.v1+/asset.image.update.fit' +import { AssetImageUpdateHideOnDraw } from '#app/routes/resources+/api.v1+/asset.image.update.hide-on-draw' import { AssetUpdateName } from '#app/routes/resources+/api.v1+/asset.update.name' import { SidebarPanelRowValuesContainer } from '..' import { PanelEntityPopover } from './dashboard-entity-panel.popover' @@ -31,6 +32,10 @@ const EntityPopover = memo(({ image }: EntityProps) => { Fit + + Hide on Draw + + ) }) diff --git a/app/definitions/artwork-generator.ts b/app/definitions/artwork-generator.ts index a0eff123..83438129 100644 --- a/app/definitions/artwork-generator.ts +++ b/app/definitions/artwork-generator.ts @@ -1,8 +1,8 @@ import { type IArtwork } from '#app/models/artwork/artwork.server' import { type IArtworkBranch } from '#app/models/artwork-branch/artwork-branch.server' import { type IArtworkVersion } from '#app/models/artwork-version/artwork-version.server' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' import { type IAssetByType } from '#app/models/asset/asset.server' -import { type IAssetImageGeneration } from '#app/models/asset/image/image.generate.server' import { type IFill } from '#app/models/design-type/fill/fill.server' import { type ILayout } from '#app/models/design-type/layout/layout.server' import { type ILine } from '#app/models/design-type/line/line.server' @@ -72,6 +72,11 @@ export interface ILayerGenerator extends IGeneratorDesigns { name?: ILayer['name'] description?: ILayer['description'] + // layer always has access to the background color + // if drawing image to get pixel data + // this allows the background to be redrawn + background: string + // create this design type container: ILayerGeneratorContainer @@ -94,14 +99,10 @@ export interface ILayerGeneratorContainer { export interface IGenerationLayer { generator: ILayerGenerator - assets: IGenerationAssets + assets: IAssetGenerationByType items: IGenerationItem[] } -export interface IGenerationAssets { - image: IAssetImageGeneration | null -} - export interface IGenerationItem { id: string fillStyle: string diff --git a/app/models/asset/asset.generation.server.ts b/app/models/asset/asset.generation.server.ts new file mode 100644 index 00000000..45f5d750 --- /dev/null +++ b/app/models/asset/asset.generation.server.ts @@ -0,0 +1,5 @@ +import { type IAssetImageGeneration } from './image/image.generate.server' + +export interface IAssetGenerationByType { + assetImages: IAssetImageGeneration[] +} diff --git a/app/models/asset/image/image.generate.server.ts b/app/models/asset/image/image.generate.server.ts index d3344edf..e1f3fcb2 100644 --- a/app/models/asset/image/image.generate.server.ts +++ b/app/models/asset/image/image.generate.server.ts @@ -1,3 +1,10 @@ +import { type IAssetImage } from './image.server' + +export interface IAssetImageSrcGeneration { + id: IAssetImage['id'] + src: string +} + export interface IAssetImageDrawGeneration { x: number y: number @@ -5,6 +12,8 @@ export interface IAssetImageDrawGeneration { height: number } -export interface IAssetImageGeneration extends IAssetImageDrawGeneration { - src: string +export interface IAssetImageGeneration + extends IAssetImageSrcGeneration, + IAssetImageDrawGeneration { + hideOnDraw: boolean } diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts index 5f454198..7d53debd 100644 --- a/app/models/asset/image/image.server.ts +++ b/app/models/asset/image/image.server.ts @@ -24,6 +24,7 @@ export interface IAssetImageFileData { lastModified?: number filename: string fit?: IAssetImageFit + hideOnDraw?: boolean } // when adding attributes to an asset type, diff --git a/app/models/asset/image/image.update.hide-on-draw.server.ts b/app/models/asset/image/image.update.hide-on-draw.server.ts new file mode 100644 index 00000000..16a33cc7 --- /dev/null +++ b/app/models/asset/image/image.update.hide-on-draw.server.ts @@ -0,0 +1,49 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditAssetImageHideOnDrawSchema } from '#app/schema/asset/image' +import { ValidateAssetSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { type IAssetImage, type IAssetImageFileData } from './image.server' +import { stringifyAssetImageAttributes } from './utils' + +export const validateEditHideOnDrawAssetImageSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateAssetSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditAssetImageHideOnDrawSchema, + strategy, + }) +} + +export interface IAssetImageUpdateHideOnDrawSubmission { + userId: IUser['id'] + id: IAssetImage['id'] + hideOnDraw: boolean +} + +interface IAssetImageUpdateHideOnDrawData { + attributes: IAssetImageFileData +} + +export const updateAssetImageHideOnDraw = ({ + id, + data, +}: { + id: IAssetImage['id'] + data: IAssetImageUpdateHideOnDrawData +}) => { + const { attributes } = data + const jsonAttributes = stringifyAssetImageAttributes(attributes) + return prisma.asset.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/asset/utils.ts b/app/models/asset/utils.ts index be3cebd0..450567ad 100644 --- a/app/models/asset/utils.ts +++ b/app/models/asset/utils.ts @@ -65,6 +65,14 @@ export const validateAssetAttributes = ({ } } +export const filterAssetsVisible = ({ + assets, +}: { + assets: IAssetParsed[] +}): IAssetType[] => { + return assets.filter(asset => asset.visible) +} + export const filterAssetType = ({ assets, type, diff --git a/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx b/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx new file mode 100644 index 00000000..498b0609 --- /dev/null +++ b/app/routes/resources+/api.v1+/asset.image.update.hide-on-draw.tsx @@ -0,0 +1,101 @@ +import { + json, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from '@remix-run/node' +import { useFetcher } from '@remix-run/react' +import { redirectBack } from 'remix-utils/redirect-back' +import { useHydrated } from 'remix-utils/use-hydrated' +import { FetcherIconButton } from '#app/components/templates/form/fetcher-icon-button' +import { type IAssetImage } from '#app/models/asset/image/image.server' +import { validateEditHideOnDrawAssetImageSubmission } from '#app/models/asset/image/image.update.hide-on-draw.server' +import { EditAssetImageHideOnDrawSchema } from '#app/schema/asset/image' +import { validateNoJS } from '#app/schema/form-data' +import { assetImageUpdateHideOnDrawService } from '#app/services/asset.image.update.hide-on-draw.service' +import { requireUserId } from '#app/utils/auth.server' +import { Routes } from '#app/utils/routes.const' + +// https://www.epicweb.dev/full-stack-components + +const route = Routes.RESOURCES.API.V1.ASSET.IMAGE.UPDATE.HIDE_ON_DRAW +const schema = EditAssetImageHideOnDrawSchema + +// auth GET request to endpoint +export async function loader({ request }: LoaderFunctionArgs) { + await requireUserId(request) + return json({}) +} + +export async function action({ request }: ActionFunctionArgs) { + const userId = await requireUserId(request) + const formData = await request.formData() + const noJS = validateNoJS({ formData }) + + let updateSuccess = false + let errorMessage = '' + const { status, submission } = + await validateEditHideOnDrawAssetImageSubmission({ + userId, + formData, + }) + + if (status === 'success') { + const { success, message } = await assetImageUpdateHideOnDrawService({ + userId, + ...submission.value, + }) + updateSuccess = success + errorMessage = message || '' + } + + if (noJS) { + throw redirectBack(request, { + fallback: '/', + }) + } + + return json( + { status, submission, message: errorMessage }, + { + status: status === 'error' || !updateSuccess ? 404 : 200, + }, + ) +} + +export const AssetImageUpdateHideOnDraw = ({ + image, + formLocation = '', +}: { + image: IAssetImage + formLocation?: string +}) => { + const assetId = image.id + const field = 'hideOnDraw' + const isHiddenOnDraw = image.attributes[field] || false + const icon = isHiddenOnDraw ? 'eye-none' : 'eye-open' + const iconText = `${isHiddenOnDraw ? 'Show' : 'Show'} on draw` + const fetcherKey = `asset-image-update-${field}-${assetId}` + const formId = `${fetcherKey}${formLocation ? `-${formLocation}` : ''}` + + let isHydrated = useHydrated() + const fetcher = useFetcher({ + key: fetcherKey, + }) + + return ( + +
+ +
+
+ ) +} diff --git a/app/schema/asset/image.ts b/app/schema/asset/image.ts index f4f98d60..8d57f801 100644 --- a/app/schema/asset/image.ts +++ b/app/schema/asset/image.ts @@ -61,6 +61,7 @@ export const AssetAttributesImageSchema = z.object({ lastModified: z.number().optional(), filename: z.string(), fit: z.nativeEnum(AssetImageFitTypeEnum).optional(), + hideOnDraw: z.boolean().optional(), }) // zod schema for blob Buffer/File is not working @@ -94,6 +95,10 @@ export const EditAssetImageFitSchema = z.object({ fit: z.nativeEnum(AssetImageFitTypeEnum), }) +export const EditAssetImageHideOnDrawSchema = z.object({ + id: z.string(), +}) + export const DeleteAssetImageSchema = z.object({ id: z.string(), }) diff --git a/app/services/artwork/version/generator/build.container.service.ts b/app/services/artwork/version/generator/build.container.service.ts new file mode 100644 index 00000000..77dbb630 --- /dev/null +++ b/app/services/artwork/version/generator/build.container.service.ts @@ -0,0 +1,20 @@ +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' + +export const getArtworkVersionContainer = ({ + version, +}: { + version: IArtworkVersionWithChildren +}) => { + const { width, height } = version + return { + width, + height, + top: 0, + left: 0, + margin: 0, + canvas: { + width, + height, + }, + } +} diff --git a/app/services/artwork/version/generator/build.layers.service.ts b/app/services/artwork/version/generator/build.layers.service.ts new file mode 100644 index 00000000..f0b26dd2 --- /dev/null +++ b/app/services/artwork/version/generator/build.layers.service.ts @@ -0,0 +1,156 @@ +import { + type IGeneratorDesigns, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { filterAssetsVisible, groupAssetsByType } from '#app/models/asset/utils' +import { findManyDesignsWithType } from '#app/models/design/design.server' +import { getArtworkVersionVisiblePalettes } from '#app/models/design-artwork-version/design-artwork-version.server' +import { getLayerVisiblePalettes } from '#app/models/design-layer/design-layer.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { + filterSelectedDesignTypes, + findFirstDesignsByTypeInArray, +} from '#app/utils/design' +import { filterLayersVisible } from '#app/utils/layer.utils' +import { orderLinkedItems } from '#app/utils/linked-list.utils' +import { getArtworkVersionContainer } from './build.container.service' +import { getRotates } from './build.rotates.service' + +// default/global design settings for each layer +// layer can override any of these values +export const buildDefaultGeneratorLayer = async ({ + version, + defaultGeneratorDesigns, +}: { + version: IArtworkVersionWithChildren + defaultGeneratorDesigns: IGeneratorDesigns +}): Promise => { + const artworkVersionId = version.id + + // get all visible palettes to use for fill or stroke + const palettes = await getArtworkVersionVisiblePalettes({ + artworkVersionId, + }) + + // get all visible rotates to use for rotate if visible random + const rotates = await getRotates({ + artworkVersionId, + rotate: defaultGeneratorDesigns.rotate, + }) + + // container defaults to version dimensions + const container = getArtworkVersionContainer({ version }) + + const assets = groupAssetsByType({ assets: version.assets }) + + return { + ...defaultGeneratorDesigns, + background: version.background, + palette: palettes, + rotates, + container, + assets, + } +} + +export const buildGeneratorLayers = async ({ + version, + defaultGeneratorLayer, +}: { + version: IArtworkVersionWithChildren + defaultGeneratorLayer: ILayerGenerator +}) => { + const orderedLayers = await orderLinkedItems( + version.layers, + ) + const visibleLayers = filterLayersVisible({ + layers: orderedLayers, + }) as ILayerWithChildren[] + + return await Promise.all( + visibleLayers.map(layer => + buildGeneratorLayer({ + layer, + defaultGeneratorLayer, + }), + ), + ) +} + +const buildGeneratorLayer = async ({ + layer, + defaultGeneratorLayer, +}: { + layer: ILayerWithChildren + defaultGeneratorLayer: ILayerGenerator +}): Promise => { + const layerId = layer.id + + // Step 1: get all selected designs for the layer + const layerSelectedDesigns = await getLayerSelectedDesigns({ layerId }) + + // Step 2: split the selected designs into the first of each type + const selectedDesignTypes = findFirstDesignsByTypeInArray({ + designs: layerSelectedDesigns, + }) + + // Step 3: filter the selected designs that are present + // separate the palette from the rest of the layer generator designs + // if the layer has no palette we do not want to override the default palette + const { palette, ...layerGeneratorDesigns } = filterSelectedDesignTypes({ + selectedDesignTypes, + }) + + // Step 4: initialize the generator layer + // with the default generator layer + // and the layer generator designs as overrides + // and layer details + const { id, name, description } = layer + + const assets = groupAssetsByType({ + assets: filterAssetsVisible({ assets: layer.assets }), + }) + + const layerGenerator = { + ...defaultGeneratorLayer, + ...layerGeneratorDesigns, + id, + name, + description, + assets, + } + + // Step 5: get all visible palettes to use for fill or stroke + // if empty, then use the default palette + const palettes = await getLayerVisiblePalettes({ layerId }) + if (palettes.length > 0) { + layerGenerator.palette = palettes + } + + // Step 6: get all visible rotates to use for rotate if visible random + // if empty, then use the default rotate + const { rotate } = layerGeneratorDesigns + if (rotate) { + const rotates = await getRotates({ + layerId, + rotate, + }) + + if (rotates.length > 0) { + layerGenerator.rotates = rotates + } + } + + return layerGenerator +} + +const getLayerSelectedDesigns = async ({ + layerId, +}: { + layerId: ILayerWithChildren['id'] +}) => { + return await findManyDesignsWithType({ + where: { layerId, selected: true }, + }) +} diff --git a/app/services/artwork/version/generator/build.metadata.service.ts b/app/services/artwork/version/generator/build.metadata.service.ts new file mode 100644 index 00000000..001d9a31 --- /dev/null +++ b/app/services/artwork/version/generator/build.metadata.service.ts @@ -0,0 +1,77 @@ +import { invariant } from '@epic-web/invariant' +import { type IArtworkVersionGeneratorMetadata } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { prisma } from '#app/utils/db.server' + +export const buildGeneratorMetadata = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise => { + const branch = await prisma.artworkBranch.findUnique({ + where: { id: version.branchId }, + select: { + id: true, + name: true, + slug: true, + description: true, + artwork: { + select: { + id: true, + name: true, + slug: true, + description: true, + project: { + select: { + id: true, + name: true, + slug: true, + description: true, + }, + }, + owner: { + select: { + id: true, + name: true, + username: true, + }, + }, + }, + }, + }, + }) + invariant(branch, `Branch not found for version ${version.id}`) + const artwork = branch.artwork + invariant(artwork, `Artwork not found for branch ${branch.id}`) + const project = artwork.project + invariant(project, `Project not found for artwork ${artwork.id}`) + const owner = artwork.owner + invariant(owner, `Owner not found for artwork ${artwork.id}`) + + return { + // version + versionId: version.id, + versionName: version.name, + versionSlug: version.slug, + versionDescription: version.description, + // branch + branchId: version.branchId, + branchName: branch.name, + branchSlug: branch.slug, + branchDescription: branch.description, + // artwork + artworkId: artwork.id, + artworkName: artwork.name, + artworkSlug: artwork.slug, + artworkDescription: artwork.description, + // project + projectId: project.id, + projectName: project.name, + projectSlug: project.slug, + projectDescription: project.description, + // owner + ownerId: owner.id, + ownerName: owner.name, + ownerUsername: owner.username, + } +} diff --git a/app/services/artwork/version/generator/build.rotates.service.ts b/app/services/artwork/version/generator/build.rotates.service.ts new file mode 100644 index 00000000..bc24083f --- /dev/null +++ b/app/services/artwork/version/generator/build.rotates.service.ts @@ -0,0 +1,28 @@ +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { getArtworkVersionVisibleRotates } from '#app/models/design-artwork-version/design-artwork-version.server' +import { getLayerVisibleRotates } from '#app/models/design-layer/design-layer.server' +import { type IRotate } from '#app/models/design-type/rotate/rotate.server' +import { type ILayerWithChildren } from '#app/models/layer/layer.server' +import { type rotateBasisTypeEnum } from '#app/schema/rotate' +import { isArrayRotateBasisType } from '#app/utils/rotate' + +export const getRotates = async ({ + artworkVersionId, + layerId, + rotate, +}: { + artworkVersionId?: IArtworkVersionWithChildren['id'] + layerId?: ILayerWithChildren['id'] + rotate: IRotate +}) => { + const allRotates = isArrayRotateBasisType(rotate.basis as rotateBasisTypeEnum) + + if (allRotates) { + if (artworkVersionId) { + return await getArtworkVersionVisibleRotates({ artworkVersionId }) + } else if (layerId) { + return await getLayerVisibleRotates({ layerId }) + } + } + return [] +} diff --git a/app/services/artwork/version/generator/build.service.ts b/app/services/artwork/version/generator/build.service.ts index 26ae44f3..b223ef0b 100644 --- a/app/services/artwork/version/generator/build.service.ts +++ b/app/services/artwork/version/generator/build.service.ts @@ -1,37 +1,12 @@ -import { invariant } from '@epic-web/invariant' -import { - type IGeneratorDesigns, - type ILayerGenerator, - type IArtworkVersionGenerator, - type IGeneratorWatermark, - type IArtworkVersionGeneratorMetadata, -} from '#app/definitions/artwork-generator' +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' -import { groupAssetsByType } from '#app/models/asset/utils' -import { - findManyDesignsWithType, - type IDesignWithType, -} from '#app/models/design/design.server' -import { - getArtworkVersionVisiblePalettes, - getArtworkVersionVisibleRotates, -} from '#app/models/design-artwork-version/design-artwork-version.server' import { - getLayerVisiblePalettes, - getLayerVisibleRotates, -} from '#app/models/design-layer/design-layer.server' -import { type IRotate } from '#app/models/design-type/rotate/rotate.server' -import { type ILayerWithChildren } from '#app/models/layer/layer.server' -import { type rotateBasisTypeEnum } from '#app/schema/rotate' -import { prisma } from '#app/utils/db.server' -import { - filterSelectedDesignTypes, - findFirstDesignsByTypeInArray, - verifySelectedDesignTypesAllPresent, -} from '#app/utils/design' -import { filterLayersVisible } from '#app/utils/layer.utils' -import { orderLinkedItems } from '#app/utils/linked-list.utils' -import { isArrayRotateBasisType } from '#app/utils/rotate' + buildDefaultGeneratorLayer, + buildGeneratorLayers, +} from './build.layers.service' +import { buildGeneratorMetadata } from './build.metadata.service' +import { verifyDefaultGeneratorDesigns } from './build.verify.service' +import { buildGeneratorWatermark } from './build.watermark.service' // "build" since it is creating the version generator each time // later, if we like the generator, we can save it to the database @@ -71,11 +46,8 @@ export const artworkVersionGeneratorBuildService = async ({ // Step 4: build the generator layers // each layer can override any of the global settings - const orderedLayers = await orderLinkedItems( - version.layers, - ) - const generatorLayers = await buildGeneratorLayers({ - layers: orderedLayers, + const layers = await buildGeneratorLayers({ + version, defaultGeneratorLayer, }) @@ -92,7 +64,7 @@ export const artworkVersionGeneratorBuildService = async ({ height: version.height, background: version.background, }, - layers: generatorLayers, + layers, watermark, metadata, success: true, @@ -113,330 +85,3 @@ export const artworkVersionGeneratorBuildService = async ({ } } } - -const verifyDefaultGeneratorDesigns = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise<{ - defaultGeneratorDesigns: IGeneratorDesigns | null - message: string -}> => { - // Step 1: get all selected designs for the version - // design table has `selected: boolean` field - const selectedDesigns = await getVersionSelectedDesigns({ - artworkVersionId: version.id, - }) - - // Step 2: split the selected designs into the first of each type - const selectedDesignTypes = findFirstDesignsByTypeInArray({ - designs: selectedDesigns, - }) - - // Step 3: validate that all selected design types are present - // message will indicate which design type is missing - const { success, message } = verifySelectedDesignTypesAllPresent({ - selectedDesignTypes, - }) - - // Step 4: return failure with message if selected designs are not valid - if (!success) { - return { - message, - defaultGeneratorDesigns: null, - } - } - - // Step 5: reformat the selected designs to be generator designs - // this is to ensure that the selected designs are not null - const defaultGeneratorDesigns = { - ...selectedDesignTypes, - palette: [selectedDesignTypes.palette], - } as IGeneratorDesigns - - return { - defaultGeneratorDesigns, - message: 'Version generator designs are present.', - } -} - -const getVersionSelectedDesigns = async ({ - artworkVersionId, -}: { - artworkVersionId: IArtworkVersionWithChildren['id'] -}): Promise => { - return await findManyDesignsWithType({ - where: { artworkVersionId, selected: true }, - }) -} - -// default/global design settings for each layer -// layer can override any of these values -const buildDefaultGeneratorLayer = async ({ - version, - defaultGeneratorDesigns, -}: { - version: IArtworkVersionWithChildren - defaultGeneratorDesigns: IGeneratorDesigns -}): Promise => { - const artworkVersionId = version.id - - // get all visible palettes to use for fill or stroke - const palettes = await getArtworkVersionVisiblePalettes({ - artworkVersionId, - }) - - // get all visible rotates to use for rotate if visible random - const rotates = await getRotates({ - artworkVersionId, - rotate: defaultGeneratorDesigns.rotate, - }) - - // container defaults to version dimensions - const container = getArtworkVersionContainer({ version }) - - const assets = groupAssetsByType({ assets: version.assets }) - - return { - ...defaultGeneratorDesigns, - palette: palettes, - rotates, - container, - assets, - } -} - -const getArtworkVersionContainer = ({ - version, -}: { - version: IArtworkVersionWithChildren -}) => { - const { width, height } = version - return { - width, - height, - top: 0, - left: 0, - margin: 0, - canvas: { - width, - height, - }, - } -} - -const buildGeneratorLayers = async ({ - layers, - defaultGeneratorLayer, -}: { - layers: ILayerWithChildren[] - defaultGeneratorLayer: ILayerGenerator -}) => { - const visibleLayers = filterLayersVisible({ layers }) as ILayerWithChildren[] - - return await Promise.all( - visibleLayers.map(layer => - buildGeneratorLayer({ - layer, - defaultGeneratorLayer, - }), - ), - ) -} - -const buildGeneratorLayer = async ({ - layer, - defaultGeneratorLayer, -}: { - layer: ILayerWithChildren - defaultGeneratorLayer: ILayerGenerator -}): Promise => { - const layerId = layer.id - - // Step 1: get all selected designs for the layer - const layerSelectedDesigns = await getLayerSelectedDesigns({ layerId }) - - // Step 2: split the selected designs into the first of each type - const selectedDesignTypes = findFirstDesignsByTypeInArray({ - designs: layerSelectedDesigns, - }) - - // Step 3: filter the selected designs that are present - // separate the palette from the rest of the layer generator designs - // if the layer has no palette we do not want to override the default palette - const { palette, ...layerGeneratorDesigns } = filterSelectedDesignTypes({ - selectedDesignTypes, - }) - - // Step 4: initialize the generator layer - // with the default generator layer - // and the layer generator designs as overrides - // and layer details - const { id, name, description } = layer - - const assets = groupAssetsByType({ assets: layer.assets }) - - const layerGenerator = { - ...defaultGeneratorLayer, - ...layerGeneratorDesigns, - id, - name, - description, - assets, - } - - // Step 5: get all visible palettes to use for fill or stroke - // if empty, then use the default palette - const palettes = await getLayerVisiblePalettes({ layerId }) - if (palettes.length > 0) { - layerGenerator.palette = palettes - } - - // Step 6: get all visible rotates to use for rotate if visible random - // if empty, then use the default rotate - const { rotate } = layerGeneratorDesigns - if (rotate) { - const rotates = await getRotates({ - layerId, - rotate, - }) - - if (rotates.length > 0) { - layerGenerator.rotates = rotates - } - } - - return layerGenerator -} - -const getLayerSelectedDesigns = async ({ - layerId, -}: { - layerId: ILayerWithChildren['id'] -}) => { - return await findManyDesignsWithType({ - where: { layerId, selected: true }, - }) -} - -const getRotates = async ({ - artworkVersionId, - layerId, - rotate, -}: { - artworkVersionId?: IArtworkVersionWithChildren['id'] - layerId?: ILayerWithChildren['id'] - rotate: IRotate -}) => { - const allRotates = isArrayRotateBasisType(rotate.basis as rotateBasisTypeEnum) - - if (allRotates) { - if (artworkVersionId) { - return await getArtworkVersionVisibleRotates({ artworkVersionId }) - } else if (layerId) { - return await getLayerVisibleRotates({ layerId }) - } - } - return [] -} - -const buildGeneratorWatermark = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise => { - if (!version.watermark) return null - - const userInstagramUrl = await prisma.artworkBranch - .findUnique({ - where: { id: version.branchId }, - select: { - owner: { - select: { sm_url_instagram: true }, - }, - }, - }) - .then(branch => branch?.owner?.sm_url_instagram) - - const text = userInstagramUrl - ? `@${userInstagramUrl.split('/').pop()}` - : 'PPPAAATTT' - - return { - text, - color: version.watermarkColor, - } -} - -const buildGeneratorMetadata = async ({ - version, -}: { - version: IArtworkVersionWithChildren -}): Promise => { - const branch = await prisma.artworkBranch.findUnique({ - where: { id: version.branchId }, - select: { - id: true, - name: true, - slug: true, - description: true, - artwork: { - select: { - id: true, - name: true, - slug: true, - description: true, - project: { - select: { - id: true, - name: true, - slug: true, - description: true, - }, - }, - owner: { - select: { - id: true, - name: true, - username: true, - }, - }, - }, - }, - }, - }) - invariant(branch, `Branch not found for version ${version.id}`) - const artwork = branch.artwork - invariant(artwork, `Artwork not found for branch ${branch.id}`) - const project = artwork.project - invariant(project, `Project not found for artwork ${artwork.id}`) - const owner = artwork.owner - invariant(owner, `Owner not found for artwork ${artwork.id}`) - - return { - // version - versionId: version.id, - versionName: version.name, - versionSlug: version.slug, - versionDescription: version.description, - // branch - branchId: version.branchId, - branchName: branch.name, - branchSlug: branch.slug, - branchDescription: branch.description, - // artwork - artworkId: artwork.id, - artworkName: artwork.name, - artworkSlug: artwork.slug, - artworkDescription: artwork.description, - // project - projectId: project.id, - projectName: project.name, - projectSlug: project.slug, - projectDescription: project.description, - // owner - ownerId: owner.id, - ownerName: owner.name, - ownerUsername: owner.username, - } -} diff --git a/app/services/artwork/version/generator/build.verify.service.ts b/app/services/artwork/version/generator/build.verify.service.ts new file mode 100644 index 00000000..986528e9 --- /dev/null +++ b/app/services/artwork/version/generator/build.verify.service.ts @@ -0,0 +1,66 @@ +import { type IGeneratorDesigns } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { + type IDesignWithType, + findManyDesignsWithType, +} from '#app/models/design/design.server' +import { + findFirstDesignsByTypeInArray, + verifySelectedDesignTypesAllPresent, +} from '#app/utils/design' + +export const verifyDefaultGeneratorDesigns = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise<{ + defaultGeneratorDesigns: IGeneratorDesigns | null + message: string +}> => { + // Step 1: get all selected designs for the version + // design table has `selected: boolean` field + const selectedDesigns = await getVersionSelectedDesigns({ + artworkVersionId: version.id, + }) + + // Step 2: split the selected designs into the first of each type + const selectedDesignTypes = findFirstDesignsByTypeInArray({ + designs: selectedDesigns, + }) + + // Step 3: validate that all selected design types are present + // message will indicate which design type is missing + const { success, message } = verifySelectedDesignTypesAllPresent({ + selectedDesignTypes, + }) + + // Step 4: return failure with message if selected designs are not valid + if (!success) { + return { + message, + defaultGeneratorDesigns: null, + } + } + + // Step 5: reformat the selected designs to be generator designs + // this is to ensure that the selected designs are not null + const defaultGeneratorDesigns = { + ...selectedDesignTypes, + palette: [selectedDesignTypes.palette], + } as IGeneratorDesigns + + return { + defaultGeneratorDesigns, + message: 'Version generator designs are present.', + } +} + +const getVersionSelectedDesigns = async ({ + artworkVersionId, +}: { + artworkVersionId: IArtworkVersionWithChildren['id'] +}): Promise => { + return await findManyDesignsWithType({ + where: { artworkVersionId, selected: true }, + }) +} diff --git a/app/services/artwork/version/generator/build.watermark.service.ts b/app/services/artwork/version/generator/build.watermark.service.ts new file mode 100644 index 00000000..3cbd4045 --- /dev/null +++ b/app/services/artwork/version/generator/build.watermark.service.ts @@ -0,0 +1,31 @@ +import { type IGeneratorWatermark } from '#app/definitions/artwork-generator' +import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' +import { prisma } from '#app/utils/db.server' + +export const buildGeneratorWatermark = async ({ + version, +}: { + version: IArtworkVersionWithChildren +}): Promise => { + if (!version.watermark) return null + + const userInstagramUrl = await prisma.artworkBranch + .findUnique({ + where: { id: version.branchId }, + select: { + owner: { + select: { sm_url_instagram: true }, + }, + }, + }) + .then(branch => branch?.owner?.sm_url_instagram) + + const text = userInstagramUrl + ? `@${userInstagramUrl.split('/').pop()}` + : 'PPPAAATTT' + + return { + text, + color: version.watermarkColor, + } +} diff --git a/app/services/asset.image.update.hide-on-draw.service.ts b/app/services/asset.image.update.hide-on-draw.service.ts new file mode 100644 index 00000000..e35e327e --- /dev/null +++ b/app/services/asset.image.update.hide-on-draw.service.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { getAssetImage } from '#app/models/asset/image/image.get.server' +import { + type IAssetImageUpdateHideOnDrawSubmission, + updateAssetImageHideOnDraw, +} from '#app/models/asset/image/image.update.hide-on-draw.server' +import { type IAssetImageUpdatedResponse } from '#app/models/asset/image/image.update.server' +import { AssetAttributesImageSchema } from '#app/schema/asset/image' +import { prisma } from '#app/utils/db.server' + +export const assetImageUpdateHideOnDrawService = async ({ + userId, + id, +}: IAssetImageUpdateHideOnDrawSubmission): Promise => { + try { + // Step 1: verify the asset image exists + const assetImage = await getAssetImage({ + where: { id, ownerId: userId }, + }) + invariant(assetImage, 'Asset Image not found') + const { attributes: assetImageAttributes } = assetImage + + // Step 2: validate asset image data + const data = { + ...assetImageAttributes, + hideOnDraw: !assetImageAttributes.hideOnDraw, + } + const assetImageData = AssetAttributesImageSchema.parse(data) + + // Step 3: update the asset image via promise + const updateAssetImagePromise = updateAssetImageHideOnDraw({ + id, + data: { attributes: { ...assetImageData } }, + }) + + // Step 4: execute the transaction + const [updatedAssetImage] = await prisma.$transaction([ + updateAssetImagePromise, + ]) + + return { + updatedAssetImage, + success: true, + } + } catch (error) { + console.error(error) + return { + success: false, + message: 'Unknown error: assetImageUpdateHideOnDrawService', + } + } +} diff --git a/app/services/canvas/draw-background.service.ts b/app/services/canvas/draw-background.service.ts index c71bd3f0..8055557f 100644 --- a/app/services/canvas/draw-background.service.ts +++ b/app/services/canvas/draw-background.service.ts @@ -1,5 +1,11 @@ import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' +const canvasDimensions = ({ canvas }: { canvas: HTMLCanvasElement }) => { + const { width, height } = canvas + return { width, height, ratio: width / height } +} + +// do this at the beginning export const canvasDrawBackgroundService = ({ ctx, generator, @@ -12,3 +18,20 @@ export const canvasDrawBackgroundService = ({ ctx.fillStyle = `#${background}` ctx.fillRect(0, 0, width, height) } + +// do this if drawing an image to get pixel data +// then redraw the background to clear the canvas +export const canvasRedrawDrawBackgroundService = ({ + ctx, + background, +}: { + ctx: CanvasRenderingContext2D + background: string +}) => { + const { width: canvasWidth, height: canvasHeight } = canvasDimensions({ + canvas: ctx.canvas, + }) + + ctx.fillStyle = `#${background}` + ctx.fillRect(0, 0, canvasWidth, canvasHeight) +} diff --git a/app/services/canvas/draw.load-assets.service.ts b/app/services/canvas/draw.load-assets.service.ts new file mode 100644 index 00000000..6a5573ed --- /dev/null +++ b/app/services/canvas/draw.load-assets.service.ts @@ -0,0 +1,48 @@ +import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' +import { getAssetImgSrc } from '#app/models/asset/image/utils' +import { loadImage } from '#app/utils/image' + +// loaded assets: +// key: asset id +// value: image element, or something else when other assets are added + +export interface ILoadedAssets { + [key: string]: HTMLImageElement +} + +export const canvasDrawLoadAssetsService = async ({ + generator, +}: { + generator: IArtworkVersionGenerator +}): Promise => { + const { layers } = generator + const loadedAssets: ILoadedAssets = {} + + const imageLoadPromises: Promise[] = [] + + for (const layer of layers) { + const { + assets: { assetImages }, + } = layer + + for (const image of assetImages) { + const src = getAssetImgSrc({ image }) + const loadPromise = loadImage({ src }) + .then(img => { + loadedAssets[image.id] = img + }) + .catch(error => { + console.error( + `Failed to load image with ID ${image.id} from source: ${src}`, + error, + ) + }) + + imageLoadPromises.push(loadPromise) + } + } + + await Promise.all(imageLoadPromises) + + return loadedAssets +} diff --git a/app/services/canvas/draw.service.ts b/app/services/canvas/draw.service.ts index 82cde916..1bc16673 100644 --- a/app/services/canvas/draw.service.ts +++ b/app/services/canvas/draw.service.ts @@ -2,16 +2,20 @@ import { invariant } from '@epic-web/invariant' import { type IArtworkVersionGenerator } from '#app/definitions/artwork-generator' import { canvasDrawBackgroundService } from './draw-background.service' import { canvasDrawWatermarkService } from './draw-watermark.service' +import { canvasDrawLoadAssetsService } from './draw.load-assets.service' import { canvasLayerBuildDrawLayersService } from './layer/build/build-draw-layers.service' import { canvasDrawLayersService } from './layer/draw/draw-layers.service' -export const canvasDrawService = ({ +export const canvasDrawService = async ({ canvas, generator, }: { canvas: HTMLCanvasElement generator: IArtworkVersionGenerator }) => { + // Step 0: load assets + const loadedAssets = await canvasDrawLoadAssetsService({ generator }) + // Step 1: get canvas const ctx = getContext(canvas) @@ -22,11 +26,11 @@ export const canvasDrawService = ({ const drawLayers = canvasLayerBuildDrawLayersService({ ctx, generator, + loadedAssets, }) - console.log('drawLayers count: ', drawLayers.length) // Step 4: draw layers to canvas - canvasDrawLayersService({ ctx, drawLayers }) + canvasDrawLayersService({ ctx, drawLayers, loadedAssets }) // Step 5: draw watermark canvasDrawWatermarkService({ ctx, generator }) diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts new file mode 100644 index 00000000..dc8e8f71 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.assets.service.ts @@ -0,0 +1,17 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { canvasBuildLayerDrawImageService } from './build-layer-draw-image.service' + +export const buildLayerGenerationAssets = ({ + ctx, + layer, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator +}): IAssetGenerationByType => { + const assetImages = canvasBuildLayerDrawImageService({ ctx, layer }) + + return { + assetImages, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts new file mode 100644 index 00000000..5d5ec683 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.items.service.ts @@ -0,0 +1,86 @@ +import { + type IGenerationItem, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { canvasRedrawDrawBackgroundService } from '../../draw-background.service' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { canvasDrawLayerAssets } from '../draw/draw-layers.asset.service' +import { canvasBuildLayerDrawCountService } from './build-layer-draw-count.service' +import { canvasBuildLayerDrawFillService } from './build-layer-draw-fill.service' +import { canvasBuildLayerDrawLineService } from './build-layer-draw-line.service' +import { shouldGetPixelHex } from './build-layer-draw-position.pixel.service' +import { canvasBuildLayerDrawPositionService } from './build-layer-draw-position.service' +import { canvasBuildLayerDrawRotateService } from './build-layer-draw-rotate.service' +import { canvasBuildLayerDrawSizeService } from './build-layer-draw-size.service' +import { canvasBuildLayerDrawStrokeService } from './build-layer-draw-stroke.service' +import { canvasBuildLayerDrawTemplateService } from './build-layer-draw-template.service' + +export const buildLayerGenerationItems = ({ + ctx, + layer, + assets, + loadedAssets, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + assets: IAssetGenerationByType + loadedAssets: ILoadedAssets +}): IGenerationItem[] => { + const count = canvasBuildLayerDrawCountService({ layer }) + const drawAssets = shouldGetPixelHex({ layer }) + + if (drawAssets) { + canvasDrawLayerAssets({ ctx, assets, loadedAssets }) + } + + const layerGenerationItems = [] + for (let index = 0; index < count; index++) { + const generationItem = buildLayerGenerationItem({ + ctx, + layer, + index, + count, + }) + layerGenerationItems.push(generationItem) + } + + canvasRedrawDrawBackgroundService({ ctx, background: layer.background }) + + return layerGenerationItems +} + +const buildLayerGenerationItem = ({ + ctx, + layer, + index, + count, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + index: number + count: number +}): IGenerationItem => { + const { x, y, pixelHex } = canvasBuildLayerDrawPositionService({ + ctx, + layer, + index, + }) + const size = canvasBuildLayerDrawSizeService({ layer, index }) + const fill = canvasBuildLayerDrawFillService({ layer, index, pixelHex }) + const stroke = canvasBuildLayerDrawStrokeService({ layer, index, pixelHex }) + const line = canvasBuildLayerDrawLineService({ layer, index }) + const rotate = canvasBuildLayerDrawRotateService({ layer, index }) + const template = canvasBuildLayerDrawTemplateService({ layer, index }) + + return { + id: `layer-${layer.id}-${index}-${count}`, + fillStyle: fill, + lineWidth: line, + position: { x, y }, + rotate, + size, + strokeStyle: stroke, + template, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.layer.service.ts b/app/services/canvas/layer/build/build-draw-layers.layer.service.ts new file mode 100644 index 00000000..622e5ca2 --- /dev/null +++ b/app/services/canvas/layer/build/build-draw-layers.layer.service.ts @@ -0,0 +1,25 @@ +import { + type IGenerationLayer, + type ILayerGenerator, +} from '#app/definitions/artwork-generator' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { buildLayerGenerationAssets } from './build-draw-layers.layer.assets.service' +import { buildLayerGenerationItems } from './build-draw-layers.layer.items.service' + +export const canvasLayerBuildDrawLayersLayerService = ({ + ctx, + layer, + loadedAssets, +}: { + ctx: CanvasRenderingContext2D + layer: ILayerGenerator + loadedAssets: ILoadedAssets +}): IGenerationLayer => { + const assets = buildLayerGenerationAssets({ ctx, layer }) + const items = buildLayerGenerationItems({ ctx, layer, assets, loadedAssets }) + return { + generator: layer, + assets, + items, + } +} diff --git a/app/services/canvas/layer/build/build-draw-layers.service.ts b/app/services/canvas/layer/build/build-draw-layers.service.ts index 32a9a69d..586723cc 100644 --- a/app/services/canvas/layer/build/build-draw-layers.service.ts +++ b/app/services/canvas/layer/build/build-draw-layers.service.ts @@ -1,107 +1,26 @@ import { type IGenerationLayer, type IArtworkVersionGenerator, - type IGenerationItem, - type ILayerGenerator, - type IGenerationAssets, } from '#app/definitions/artwork-generator' -import { canvasBuildLayerDrawCountService } from './build-layer-draw-count.service' -import { canvasBuildLayerDrawFillService } from './build-layer-draw-fill.service' -import { canvasBuildLayerDrawImageService } from './build-layer-draw-image.service' -import { canvasBuildLayerDrawLineService } from './build-layer-draw-line.service' -import { canvasBuildLayerDrawPositionService } from './build-layer-draw-position.service' -import { canvasBuildLayerDrawRotateService } from './build-layer-draw-rotate.service' -import { canvasBuildLayerDrawSizeService } from './build-layer-draw-size.service' -import { canvasBuildLayerDrawStrokeService } from './build-layer-draw-stroke.service' -import { canvasBuildLayerDrawTemplateService } from './build-layer-draw-template.service' +import { type ILoadedAssets } from '../../draw.load-assets.service' +import { canvasLayerBuildDrawLayersLayerService } from './build-draw-layers.layer.service' export const canvasLayerBuildDrawLayersService = ({ ctx, generator, + loadedAssets, }: { ctx: CanvasRenderingContext2D generator: IArtworkVersionGenerator + loadedAssets: ILoadedAssets }): IGenerationLayer[] => { const { layers } = generator - return layers.map(layer => { - const assets = buildLayerGenerationAssets({ ctx, layer }) - const items = buildLayerGenerationItems({ ctx, layer }) - return { - generator: layer, - assets, - items, - } - }) -} - -const buildLayerGenerationAssets = ({ - ctx, - layer, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator -}): IGenerationAssets => { - const image = canvasBuildLayerDrawImageService({ ctx, layer }) - - return { - image, - } -} - -const buildLayerGenerationItems = ({ - ctx, - layer, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator -}): IGenerationItem[] => { - const count = canvasBuildLayerDrawCountService({ layer }) - - const layerGenerationItems = [] - for (let index = 0; index < count; index++) { - const generationItem = buildLayerGenerationItem({ + return layers.map(layer => + canvasLayerBuildDrawLayersLayerService({ ctx, layer, - index, - count, - }) - layerGenerationItems.push(generationItem) - } - return layerGenerationItems -} - -const buildLayerGenerationItem = ({ - ctx, - layer, - index, - count, -}: { - ctx: CanvasRenderingContext2D - layer: ILayerGenerator - index: number - count: number -}): IGenerationItem => { - const { x, y, pixelHex } = canvasBuildLayerDrawPositionService({ - ctx, - layer, - index, - }) - const size = canvasBuildLayerDrawSizeService({ layer, index }) - const fill = canvasBuildLayerDrawFillService({ layer, index, pixelHex }) - const stroke = canvasBuildLayerDrawStrokeService({ layer, index, pixelHex }) - const line = canvasBuildLayerDrawLineService({ layer, index }) - const rotate = canvasBuildLayerDrawRotateService({ layer, index }) - const template = canvasBuildLayerDrawTemplateService({ layer, index }) - - return { - id: `layer-${layer.id}-${index}-${count}`, - fillStyle: fill, - lineWidth: line, - position: { x, y }, - rotate, - size, - strokeStyle: stroke, - template, - } + loadedAssets, + }), + ) } diff --git a/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts index d54ae54b..d56a4172 100644 --- a/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-image.fit.service.ts @@ -68,7 +68,7 @@ const imageFitCover = ({ }: { ctx: CanvasRenderingContext2D image: IAssetImage -}): IAssetImageDrawGeneration => { +}) => { const { // width: imageWidth, // height: imageHeight, @@ -168,7 +168,7 @@ export const canvasBuildLayerDrawImageFitService = ({ }: { ctx: CanvasRenderingContext2D image: IAssetImage -}) => { +}): IAssetImageDrawGeneration => { switch (image.attributes.fit) { case AssetImageFitTypeEnum.FILL: return imageFitFill({ ctx }) @@ -181,6 +181,6 @@ export const canvasBuildLayerDrawImageFitService = ({ case AssetImageFitTypeEnum.SCALE_DOWN: return imageFitScaleDown({ ctx, image }) default: - return null + return imageFitNone({ ctx, image }) } } diff --git a/app/services/canvas/layer/build/build-layer-draw-image.service.ts b/app/services/canvas/layer/build/build-layer-draw-image.service.ts index 13f74f52..f2436e21 100644 --- a/app/services/canvas/layer/build/build-layer-draw-image.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-image.service.ts @@ -12,23 +12,25 @@ export const canvasBuildLayerDrawImageService = ({ }: { ctx: CanvasRenderingContext2D layer: ILayerGenerator -}): IAssetImageGeneration | null => { +}): IAssetImageGeneration[] => { const { assets } = layer const { assetImages } = assets - if (!assetImages.length) return null + if (!assetImages.length) return [] - // just one image to start - const image = assetImages[0] + return assetImages.map(image => { + const src = getAssetImgSrc({ image }) + const fit = canvasBuildLayerDrawImageFitService({ + ctx, + image, + }) as IAssetImageDrawGeneration + const hideOnDraw = image.attributes.hideOnDraw || false - const src = getAssetImgSrc({ image }) - const fit = canvasBuildLayerDrawImageFitService({ - ctx, - image, - }) as IAssetImageDrawGeneration - - return { - src, - ...fit, - } + return { + id: image.id, + src, + ...fit, + hideOnDraw, + } + }) } diff --git a/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts new file mode 100644 index 00000000..b94f3f4b --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-position.grid.service.ts @@ -0,0 +1,36 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' + +export const getGridPosition = ({ + layer, + index, + ctx, +}: { + layer: ILayerGenerator + index: number + ctx: CanvasRenderingContext2D +}) => { + const { layout, container } = layer + const { rows, columns } = layout + const { width, height, top, left } = container + + // subtract 1 from denominator to get the proper step size + // for example, if columns is 10, we want 10 divide by 9 + // so that the first and last positions on the grid are the edges + // and not cells in the middle + const stepX = width / (columns - 1) + const stepY = height / (rows - 1) + + // quotient is the row index + const rowIndex = Math.floor(index / columns) + // remainder is the column index + const columnIndex = index % columns + + // calculate the x and y position based on the row and column index + // and the step size + // add the top and left values to get the actual position + // add margin later + const x = columnIndex * stepX + left + const y = rowIndex * stepY + top + + return { x, y } +} diff --git a/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts new file mode 100644 index 00000000..c3218d7f --- /dev/null +++ b/app/services/canvas/layer/build/build-layer-draw-position.pixel.service.ts @@ -0,0 +1,96 @@ +import { type ILayerGenerator } from '#app/definitions/artwork-generator' +import { FillBasisTypeEnum } from '#app/schema/fill' +import { StrokeBasisTypeEnum } from '#app/schema/stroke' + +export const shouldGetPixelHex = ({ layer }: { layer: ILayerGenerator }) => { + const { fill, stroke } = layer + return ( + fill.basis === FillBasisTypeEnum.PIXEL || + stroke.basis === StrokeBasisTypeEnum.PIXEL + ) +} + +// Function to get the hex color value at a specific pixel +export const getHexAtPixel = ({ + ctx, + x, + y, +}: { + ctx: CanvasRenderingContext2D + x: number + y: number +}) => { + // Adjust the position for the nearest pixel (see below) + const { xAdj, yAdj } = adjustPositionForNearestPixel({ x, y, ctx }) + // Get the image data at the pixel + const pixelData = buildPixelImageData({ ctx, x: xAdj, y: yAdj }) + // Extract RGB values from the pixel data + const { r, g, b } = buildPixelRGB({ data: pixelData.data }) + // Convert RGB to hex + const hex = componentToHex(r) + componentToHex(g) + componentToHex(b) + // Return the hex value in uppercase + return hex.toUpperCase() +} + +// if the x or y value are outside the canvas or on right/bottom edge +// there will be no pixel data, so we need to adjust the position +// this is a semi-temporary fix +// sometimes random positions are on the edge +// right/bottom of grid too +const adjustPositionForNearestPixel = ({ + x, + y, + ctx, +}: { + x: number + y: number + ctx: CanvasRenderingContext2D +}) => { + const { canvas } = ctx + const { width, height } = canvas + if (x < 0) { + x = 0 + } + if (x >= width) { + x = width - 1 + } + if (y < 0) { + y = 0 + } + if (y >= height) { + y = height - 1 + } + return { xAdj: x, yAdj: y } +} + +// Function to build the image data for a pixel +const buildPixelImageData = ({ + ctx, + x, + y, +}: { + ctx: CanvasRenderingContext2D + x: number + y: number +}): ImageData => { + // image data for a 1x1 pixel area, effectively getting data for a single pixel + return ctx.getImageData(x, y, 1, 1) +} + +// Function to build the RGB values from image data +const buildPixelRGB = ({ data }: { data: Uint8ClampedArray }) => { + const r = data[0] // Red value + const g = data[1] // Green value + const b = data[2] // Blue value + // The alpha value at index 3 is not included as we are only interested in the RGB values for color representation. + return { r, g, b } +} + +// Function to convert a component of RGB to hex +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString#description +function componentToHex(c: number) { + // Convert the number to hex + const hex = c.toString(16) + // Ensure 2 digits by adding a leading zero if necessary + return hex.length == 1 ? '0' + hex : hex +} diff --git a/app/services/canvas/layer/build/build-layer-draw-position.service.ts b/app/services/canvas/layer/build/build-layer-draw-position.service.ts index 0e3d9bc4..1cd4766c 100644 --- a/app/services/canvas/layer/build/build-layer-draw-position.service.ts +++ b/app/services/canvas/layer/build/build-layer-draw-position.service.ts @@ -1,8 +1,11 @@ import { type ILayerGenerator } from '#app/definitions/artwork-generator' -import { FillBasisTypeEnum } from '#app/schema/fill' import { LayoutStyleTypeEnum } from '#app/schema/layout' -import { StrokeBasisTypeEnum } from '#app/schema/stroke' import { randomInRange } from '#app/utils/random.utils' +import { getGridPosition } from './build-layer-draw-position.grid.service' +import { + getHexAtPixel, + shouldGetPixelHex, +} from './build-layer-draw-position.pixel.service' export const canvasBuildLayerDrawPositionService = ({ ctx, @@ -52,131 +55,3 @@ const getRandomPosition = ({ layer }: { layer: ILayerGenerator }) => { const y = randomInRange(top, height) return { x, y } } - -const getGridPosition = ({ - layer, - index, - ctx, -}: { - layer: ILayerGenerator - index: number - ctx: CanvasRenderingContext2D -}) => { - const { layout, container } = layer - const { rows, columns } = layout - const { width, height, top, left } = container - - // subtract 1 from denominator to get the proper step size - // for example, if columns is 10, we want 10 divide by 9 - // so that the first and last positions on the grid are the edges - // and not cells in the middle - const stepX = width / (columns - 1) - const stepY = height / (rows - 1) - - // quotient is the row index - const rowIndex = Math.floor(index / columns) - // remainder is the column index - const columnIndex = index % columns - - // calculate the x and y position based on the row and column index - // and the step size - // add the top and left values to get the actual position - // add margin later - const x = columnIndex * stepX + left - const y = rowIndex * stepY + top - - return { x, y } -} - -const shouldGetPixelHex = ({ layer }: { layer: ILayerGenerator }) => { - const { fill, stroke } = layer - return ( - fill.basis === FillBasisTypeEnum.PIXEL || - stroke.basis === StrokeBasisTypeEnum.PIXEL - ) -} - -// Function to get the hex color value at a specific pixel -const getHexAtPixel = ({ - ctx, - x, - y, -}: { - ctx: CanvasRenderingContext2D - x: number - y: number -}) => { - // Adjust the position for the nearest pixel (see below) - const { xAdj, yAdj } = adjustPositionForNearestPixel({ x, y, ctx }) - // Get the image data at the pixel - const pixelData = buildPixelImageData({ ctx, x: xAdj, y: yAdj }) - // Extract RGB values from the pixel data - const { r, g, b } = buildPixelRGB({ data: pixelData.data }) - // Convert RGB to hex - const hex = componentToHex(r) + componentToHex(g) + componentToHex(b) - // Return the hex value in uppercase - return hex.toUpperCase() -} - -// if the x or y value are outside the canvas or on right/bottom edge -// there will be no pixel data, so we need to adjust the position -// this is a semi-temporary fix -// sometimes random positions are on the edge -// right/bottom of grid too -const adjustPositionForNearestPixel = ({ - x, - y, - ctx, -}: { - x: number - y: number - ctx: CanvasRenderingContext2D -}) => { - const { canvas } = ctx - const { width, height } = canvas - if (x < 0) { - x = 0 - } - if (x >= width) { - x = width - 1 - } - if (y < 0) { - y = 0 - } - if (y >= height) { - y = height - 1 - } - return { xAdj: x, yAdj: y } -} - -// Function to build the image data for a pixel -const buildPixelImageData = ({ - ctx, - x, - y, -}: { - ctx: CanvasRenderingContext2D - x: number - y: number -}): ImageData => { - // image data for a 1x1 pixel area, effectively getting data for a single pixel - return ctx.getImageData(x, y, 1, 1) -} - -// Function to build the RGB values from image data -const buildPixelRGB = ({ data }: { data: Uint8ClampedArray }) => { - const r = data[0] // Red value - const g = data[1] // Green value - const b = data[2] // Blue value - // The alpha value at index 3 is not included as we are only interested in the RGB values for color representation. - return { r, g, b } -} - -// Function to convert a component of RGB to hex -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString#description -function componentToHex(c: number) { - // Convert the number to hex - const hex = c.toString(16) - // Ensure 2 digits by adding a leading zero if necessary - return hex.length == 1 ? '0' + hex : hex -} diff --git a/app/services/canvas/layer/draw/draw-layers.asset.service.ts b/app/services/canvas/layer/draw/draw-layers.asset.service.ts new file mode 100644 index 00000000..3982a327 --- /dev/null +++ b/app/services/canvas/layer/draw/draw-layers.asset.service.ts @@ -0,0 +1,27 @@ +import { type IAssetGenerationByType } from '#app/models/asset/asset.generation.server' +import { type ILoadedAssets } from '../../draw.load-assets.service' + +export const canvasDrawLayerAssets = ({ + ctx, + assets, + loadedAssets, + timeToDraw, +}: { + ctx: CanvasRenderingContext2D + assets: IAssetGenerationByType + loadedAssets: ILoadedAssets + timeToDraw?: boolean +}) => { + const { assetImages } = assets + + for (let i = 0; i < assetImages.length; i++) { + const image = assetImages[i] + + // may want to draw image to get pixel data on build + if (timeToDraw && image.hideOnDraw) continue + + const img = loadedAssets[image.id] + const { x, y, width, height } = image + ctx.drawImage(img, x, y, width, height) + } +} diff --git a/app/services/canvas/layer/draw/draw-layers.service.ts b/app/services/canvas/layer/draw/draw-layers.service.ts index cda10fc3..f1dbec28 100644 --- a/app/services/canvas/layer/draw/draw-layers.service.ts +++ b/app/services/canvas/layer/draw/draw-layers.service.ts @@ -1,44 +1,32 @@ import { type IGenerationLayer, type IGenerationItem, - type IGenerationAssets, } from '#app/definitions/artwork-generator' -import { loadImage } from '#app/utils/image' +import { type ILoadedAssets } from '../../draw.load-assets.service' import { drawLayerItemService } from './draw-layer-item.service' +import { canvasDrawLayerAssets } from './draw-layers.asset.service' export const canvasDrawLayersService = ({ ctx, drawLayers, + loadedAssets, }: { ctx: CanvasRenderingContext2D drawLayers: IGenerationLayer[] + loadedAssets: ILoadedAssets }) => { for (let i = 0; i < drawLayers.length; i++) { const layer = drawLayers[i] - drawLayerAssets({ ctx, assets: layer.assets }) + canvasDrawLayerAssets({ + ctx, + assets: layer.assets, + loadedAssets, + timeToDraw: true, + }) drawLayerItems({ ctx, items: layer.items }) } } -const drawLayerAssets = ({ - ctx, - assets, -}: { - ctx: CanvasRenderingContext2D - assets: IGenerationAssets -}) => { - const { image } = assets - if (image && image.src) { - console.log('assets.image: ', image) - const img = loadImage({ src: image.src }) - console.log('img: ', img) - // load image - // async - // const { x, y, width, height } = assets.image - // ctx.drawImage(img, x, y, width, height) - } -} - const drawLayerItems = ({ ctx, items, @@ -48,7 +36,6 @@ const drawLayerItems = ({ }) => { for (let i = 0; i < items.length; i++) { const layerDrawItem = items[i] - // console.log('count: ', i) drawLayerItemService({ ctx, layerDrawItem }) } } diff --git a/app/utils/routes.const.ts b/app/utils/routes.const.ts index b5a778c0..b5ec2112 100644 --- a/app/utils/routes.const.ts +++ b/app/utils/routes.const.ts @@ -58,6 +58,7 @@ export const Routes = { }, UPDATE: { FIT: `${pathBase}/asset/image/update/fit`, + HIDE_ON_DRAW: `${pathBase}/asset/image/update/hide-on-draw`, }, }, }, From 0f31b57a634b0b793aa96903c5b25b5901cf67d7 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 05:55:10 -0400 Subject: [PATCH 40/54] db migration --- .../migration.sql | 31 +++++++++++++++++++ prisma/schema.prisma | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 prisma/migrations/20240617095318_add_attributes_and_updated_at_to_design/migration.sql diff --git a/prisma/migrations/20240617095318_add_attributes_and_updated_at_to_design/migration.sql b/prisma/migrations/20240617095318_add_attributes_and_updated_at_to_design/migration.sql new file mode 100644 index 00000000..1ad9047b --- /dev/null +++ b/prisma/migrations/20240617095318_add_attributes_and_updated_at_to_design/migration.sql @@ -0,0 +1,31 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Design" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "visible" BOOLEAN NOT NULL DEFAULT true, + "selected" BOOLEAN NOT NULL DEFAULT false, + "attributes" TEXT NOT NULL DEFAULT '{}', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "nextId" TEXT, + "prevId" TEXT, + "ownerId" TEXT NOT NULL, + "artworkVersionId" TEXT, + "layerId" TEXT, + CONSTRAINT "Design_nextId_fkey" FOREIGN KEY ("nextId") REFERENCES "Design" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Design_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Design_artworkVersionId_fkey" FOREIGN KEY ("artworkVersionId") REFERENCES "ArtworkVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Design_layerId_fkey" FOREIGN KEY ("layerId") REFERENCES "Layer" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Design" ("artworkVersionId", "createdAt", "id", "layerId", "nextId", "ownerId", "prevId", "selected", "type", "visible") SELECT "artworkVersionId", "createdAt", "id", "layerId", "nextId", "ownerId", "prevId", "selected", "type", "visible" FROM "Design"; +DROP TABLE "Design"; +ALTER TABLE "new_Design" RENAME TO "Design"; +CREATE UNIQUE INDEX "Design_nextId_key" ON "Design"("nextId"); +CREATE UNIQUE INDEX "Design_prevId_key" ON "Design"("prevId"); +CREATE INDEX "Design_ownerId_idx" ON "Design"("ownerId"); +CREATE INDEX "Design_layerId_idx" ON "Design"("layerId"); +CREATE INDEX "Design_artworkVersionId_idx" ON "Design"("artworkVersionId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3e90b049..a9271211 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -284,8 +284,10 @@ model Design { type String // e.g. palette, size, fill, stroke, line, etc. visible Boolean @default(true) selected Boolean @default(false) // this is a work-around for finding the first in linked list that is visible + attributes String @default("{}") // json string of attributes specific to the type createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @default(now()) // designs can be ordered by a linked list // https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/self-relations#one-to-one-self-relations From 9a4622be82749f4d5a1b73ebaff77f554f39b8cc Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 06:14:38 -0400 Subject: [PATCH 41/54] cleaning design server --- .../design-artwork-version.server.ts | 2 +- .../design-layer/design-layer.server.ts | 2 +- app/models/design/design.get.server.ts | 30 +++++++++- app/models/design/design.server.ts | 55 +------------------ app/schema/design.ts | 46 ++++++++-------- app/schema/zod-helpers.ts | 2 + .../version/generator/build.layers.service.ts | 2 +- .../version/generator/build.verify.service.ts | 6 +- app/services/design/clone-many.service.ts | 2 +- app/services/design/delete.service.ts | 12 ++-- app/services/design/move-down.service.ts | 14 ++--- app/services/design/move-up.service.ts | 14 ++--- app/services/design/toggle-visible.service.ts | 8 +-- .../prisma-extensions-artwork-version.ts | 3 +- app/utils/prisma-extensions-artwork.ts | 3 +- app/utils/prisma-extensions-design.ts | 4 +- app/utils/prisma-extensions-fill.ts | 3 +- app/utils/prisma-extensions-layer.ts | 3 +- app/utils/prisma-extensions-layout.ts | 3 +- app/utils/prisma-extensions-line.ts | 3 +- app/utils/prisma-extensions-palette.ts | 3 +- app/utils/prisma-extensions-rotate.ts | 3 +- app/utils/prisma-extensions-size.ts | 3 +- app/utils/prisma-extensions-stroke.ts | 3 +- app/utils/prisma-extensions-template.ts | 3 +- 25 files changed, 97 insertions(+), 135 deletions(-) diff --git a/app/models/design-artwork-version/design-artwork-version.server.ts b/app/models/design-artwork-version/design-artwork-version.server.ts index 8b6ee259..49f62587 100644 --- a/app/models/design-artwork-version/design-artwork-version.server.ts +++ b/app/models/design-artwork-version/design-artwork-version.server.ts @@ -5,8 +5,8 @@ import { filterVisibleDesigns } from '#app/utils/design' import { orderLinkedItems } from '#app/utils/linked-list.utils' import { filterNonArrayRotates } from '#app/utils/rotate' import { type IArtworkVersion } from '../artwork-version/artwork-version.server' +import { findManyDesignsWithType } from '../design/design.get.server' import { - findManyDesignsWithType, type IDesignWithPalette, type IDesign, type IDesignWithRotate, diff --git a/app/models/design-layer/design-layer.server.ts b/app/models/design-layer/design-layer.server.ts index 32afef16..9ea0dd4c 100644 --- a/app/models/design-layer/design-layer.server.ts +++ b/app/models/design-layer/design-layer.server.ts @@ -4,8 +4,8 @@ import { prisma } from '#app/utils/db.server' import { filterVisibleDesigns } from '#app/utils/design' import { orderLinkedItems } from '#app/utils/linked-list.utils' import { filterNonArrayRotates } from '#app/utils/rotate' +import { findManyDesignsWithType } from '../design/design.get.server' import { - findManyDesignsWithType, type IDesignWithPalette, type IDesign, type IDesignWithRotate, diff --git a/app/models/design/design.get.server.ts b/app/models/design/design.get.server.ts index a850a399..0fcfeb7d 100644 --- a/app/models/design/design.get.server.ts +++ b/app/models/design/design.get.server.ts @@ -1,13 +1,14 @@ import { z } from 'zod' import { DesignTypeEnum } from '#app/schema/design' -import { zodStringOrNull } from '#app/schema/zod-helpers' +import { arrayOfIds, zodStringOrNull } from '#app/schema/zod-helpers' import { prisma } from '#app/utils/db.server' import { type IDesign, type IDesignWithType } from '../design/design.server' export type queryDesignWhereArgsType = z.infer const whereArgs = z.object({ - id: z.string().optional(), + id: z.union([z.string(), arrayOfIds]).optional(), type: z.nativeEnum(DesignTypeEnum).optional(), + selected: z.boolean().optional(), ownerId: z.string().optional(), artworkId: z.string().optional(), artworkVersionId: z.string().optional(), @@ -99,3 +100,28 @@ export const getDesignWithType = async ({ }) return design } + +export const findManyDesignsWithType = async ({ + where, +}: { + where: queryDesignWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const designs = await prisma.design.findMany({ + where, + include: { + palette: true, + size: true, + fill: true, + stroke: true, + line: true, + rotate: true, + layout: true, + template: true, + }, + orderBy: { + type: 'asc', + }, + }) + return designs +} diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 8247fd59..cacd69f0 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -1,10 +1,5 @@ import { type Design } from '@prisma/client' -import { - type selectArgsType, - type findDesignArgsType, - type whereArgsType, - type designTypeEnum, -} from '#app/schema/design' +import { type designTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' import { type IArtworkVersion, @@ -153,54 +148,6 @@ export interface ISelectedDesignsFiltered { template?: ITemplate } -export const findManyDesignsWithType = async ({ - where, -}: { - where: whereArgsType -}): Promise => { - const designs = await prisma.design.findMany({ - where, - include: { - palette: true, - size: true, - fill: true, - stroke: true, - line: true, - rotate: true, - layout: true, - template: true, - }, - orderBy: { - type: 'asc', - }, - }) - return designs -} - -export const findFirstDesign = async ({ - where, - select, -}: findDesignArgsType): Promise => { - const design = await prisma.design.findFirst({ - where, - select, - }) - return design -} - -export const findDesignByIdAndOwner = async ({ - id, - ownerId, - select, -}: { - id: whereArgsType['id'] - ownerId: whereArgsType['ownerId'] - select?: selectArgsType -}): Promise => { - const where = { id, ownerId } - return await findFirstDesign({ where, select }) -} - // only use in transactions export const connectPrevAndNextDesigns = ({ prevId, diff --git a/app/schema/design.ts b/app/schema/design.ts index c7b9439e..91fae498 100644 --- a/app/schema/design.ts +++ b/app/schema/design.ts @@ -72,28 +72,28 @@ export type DeleteDesignSchemaType = | typeof DeleteArtworkVersionDesignSchema | typeof DeleteLayerDesignSchema -export type selectArgsType = z.infer -const selectArgs = z.object({ - id: z.boolean().optional(), -}) +// export type selectArgsType = z.infer +// const selectArgs = z.object({ +// id: z.boolean().optional(), +// }) -export type whereArgsType = z.infer -const arrayOfIds = z.object({ in: z.array(z.string()) }) -const zodStringOrNull = z.union([z.string(), z.null()]) -const whereArgs = z.object({ - id: z.union([z.string(), arrayOfIds]).optional(), - type: z.nativeEnum(DesignTypeEnum).optional(), - visible: z.boolean().optional(), - selected: z.boolean().optional(), - ownerId: z.string().optional(), - artworkVersionId: z.string().optional(), - layerId: z.string().optional(), - prevId: zodStringOrNull.optional(), - nextId: zodStringOrNull.optional(), -}) +// export type whereArgsType = z.infer +// const arrayOfIds = z.object({ in: z.array(z.string()) }) +// const zodStringOrNull = z.union([z.string(), z.null()]) +// const whereArgs = z.object({ +// id: z.union([z.string(), arrayOfIds]).optional(), +// type: z.nativeEnum(DesignTypeEnum).optional(), +// visible: z.boolean().optional(), +// selected: z.boolean().optional(), +// ownerId: z.string().optional(), +// artworkVersionId: z.string().optional(), +// layerId: z.string().optional(), +// prevId: zodStringOrNull.optional(), +// nextId: zodStringOrNull.optional(), +// }) -export type findDesignArgsType = z.infer -export const findDesignArgs = z.object({ - where: whereArgs, - select: selectArgs.optional(), -}) +// export type findDesignArgsType = z.infer +// export const findDesignArgs = z.object({ +// where: whereArgs, +// select: selectArgs.optional(), +// }) diff --git a/app/schema/zod-helpers.ts b/app/schema/zod-helpers.ts index 51c06f8c..26cca07f 100644 --- a/app/schema/zod-helpers.ts +++ b/app/schema/zod-helpers.ts @@ -16,3 +16,5 @@ export type defaultValueStringOrNumber = { // use this for schema validation where the value is a string or null // helpful for queries on doubly-linked list items (head/tail) export const zodStringOrNull = z.union([z.string(), z.null()]) + +export const arrayOfIds = z.object({ in: z.array(z.string()) }) diff --git a/app/services/artwork/version/generator/build.layers.service.ts b/app/services/artwork/version/generator/build.layers.service.ts index f0b26dd2..6787f4c6 100644 --- a/app/services/artwork/version/generator/build.layers.service.ts +++ b/app/services/artwork/version/generator/build.layers.service.ts @@ -4,7 +4,7 @@ import { } from '#app/definitions/artwork-generator' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' import { filterAssetsVisible, groupAssetsByType } from '#app/models/asset/utils' -import { findManyDesignsWithType } from '#app/models/design/design.server' +import { findManyDesignsWithType } from '#app/models/design/design.get.server' import { getArtworkVersionVisiblePalettes } from '#app/models/design-artwork-version/design-artwork-version.server' import { getLayerVisiblePalettes } from '#app/models/design-layer/design-layer.server' import { type ILayerWithChildren } from '#app/models/layer/layer.server' diff --git a/app/services/artwork/version/generator/build.verify.service.ts b/app/services/artwork/version/generator/build.verify.service.ts index 986528e9..521d3fc1 100644 --- a/app/services/artwork/version/generator/build.verify.service.ts +++ b/app/services/artwork/version/generator/build.verify.service.ts @@ -1,9 +1,7 @@ import { type IGeneratorDesigns } from '#app/definitions/artwork-generator' import { type IArtworkVersionWithChildren } from '#app/models/artwork-version/artwork-version.server' -import { - type IDesignWithType, - findManyDesignsWithType, -} from '#app/models/design/design.server' +import { findManyDesignsWithType } from '#app/models/design/design.get.server' +import { type IDesignWithType } from '#app/models/design/design.server' import { findFirstDesignsByTypeInArray, verifySelectedDesignTypesAllPresent, diff --git a/app/services/design/clone-many.service.ts b/app/services/design/clone-many.service.ts index 9b50d0c1..ba603951 100644 --- a/app/services/design/clone-many.service.ts +++ b/app/services/design/clone-many.service.ts @@ -1,9 +1,9 @@ import { type User } from '@prisma/client' +import { findManyDesignsWithType } from '#app/models/design/design.get.server' import { type IDesignEntityId, type IDesignWithType, type IDesignsByType, - findManyDesignsWithType, } from '#app/models/design/design.server' import { DesignCloneSourceTypeEnum, diff --git a/app/services/design/delete.service.ts b/app/services/design/delete.service.ts index 576a276f..7665f8a6 100644 --- a/app/services/design/delete.service.ts +++ b/app/services/design/delete.service.ts @@ -3,9 +3,9 @@ import { deleteDesign, type IDesignDeletedResponse, } from '#app/models/design/design.delete.server' +import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - findFirstDesign, updateDesignToHead, updateDesignToTail, connectPrevAndNextDesigns, @@ -31,7 +31,7 @@ export const designDeleteService = async ({ const deleteDesignPromises = [] // Step 1: get the design - const design = await getDesign({ id, userId }) + const design = await fetchDesign({ id, userId }) const { nextId, prevId, selected } = design const type = design.type as designTypeEnum @@ -78,14 +78,14 @@ export const designDeleteService = async ({ } } -const getDesign = async ({ +const fetchDesign = async ({ id, userId, }: { id: IDesign['id'] userId: User['id'] }) => { - const design = await findFirstDesign({ + const design = await getDesign({ where: { id, ownerId: userId }, }) @@ -104,14 +104,14 @@ const getAdjacentDesigns = async ({ const { nextId, prevId } = design const nextDesign = nextId - ? await getDesign({ + ? await fetchDesign({ userId, id: nextId, }) : null const prevDesign = prevId - ? await getDesign({ + ? await fetchDesign({ userId, id: prevId, }) diff --git a/app/services/design/move-down.service.ts b/app/services/design/move-down.service.ts index 7b6c87fc..09f4c7bc 100644 --- a/app/services/design/move-down.service.ts +++ b/app/services/design/move-down.service.ts @@ -1,7 +1,7 @@ import { type User } from '@prisma/client' +import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - findFirstDesign, updateDesignRemoveNodes, updateDesignNodes, type IDesignEntityId, @@ -28,13 +28,13 @@ export const designMoveDownService = async ({ // Step 1: get the current design // make sure it is not already tail - const currentDesign = await getDesign({ id, userId }) + const currentDesign = await fetchDesign({ id, userId }) const { prevId, nextId } = currentDesign if (!nextId) throw new Error('Design is already tail') const type = currentDesign.type as designTypeEnum // Step 2: get next design - const nextDesign = await getDesign({ id: nextId, userId }) + const nextDesign = await fetchDesign({ id: nextId, userId }) const nextNextId = nextDesign.nextId // Step 3: get adjacent designs if they exist @@ -83,14 +83,14 @@ export const designMoveDownService = async ({ } } -const getDesign = async ({ +const fetchDesign = async ({ id, userId, }: { id: IDesign['id'] userId: User['id'] }) => { - const design = await findFirstDesign({ + const design = await getDesign({ where: { id, ownerId: userId }, }) @@ -109,13 +109,13 @@ const getAdjacentDesigns = async ({ prevId: string | null }) => { const nextNextDesign = nextNextId - ? await getDesign({ + ? await fetchDesign({ id: nextNextId, userId, }) : null - const prevDesign = prevId ? await getDesign({ id: prevId, userId }) : null + const prevDesign = prevId ? await fetchDesign({ id: prevId, userId }) : null return { nextNextDesign, prevDesign } } diff --git a/app/services/design/move-up.service.ts b/app/services/design/move-up.service.ts index 06a18f13..0351fb46 100644 --- a/app/services/design/move-up.service.ts +++ b/app/services/design/move-up.service.ts @@ -1,7 +1,7 @@ import { type User } from '@prisma/client' +import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - findFirstDesign, updateDesignRemoveNodes, updateDesignNodes, type IDesignEntityId, @@ -28,13 +28,13 @@ export const designMoveUpService = async ({ // Step 1: get the current design // make sure it is not already head - const currentDesign = await getDesign({ id, userId }) + const currentDesign = await fetchDesign({ id, userId }) const { prevId, nextId } = currentDesign if (!prevId) throw new Error('Design is already head') const type = currentDesign.type as designTypeEnum // Step 2: get previous design - const prevDesign = await getDesign({ id: prevId, userId }) + const prevDesign = await fetchDesign({ id: prevId, userId }) const prevPrevId = prevDesign.prevId // Step 3: get adjacent designs if they exist @@ -83,14 +83,14 @@ export const designMoveUpService = async ({ } } -const getDesign = async ({ +const fetchDesign = async ({ id, userId, }: { id: IDesign['id'] userId: User['id'] }) => { - const design = await findFirstDesign({ + const design = await getDesign({ where: { id, ownerId: userId }, }) @@ -109,13 +109,13 @@ const getAdjacentDesigns = async ({ nextId: string | null }) => { const prevPrevDesign = prevPrevId - ? await getDesign({ + ? await fetchDesign({ id: prevPrevId, userId, }) : null - const nextDesign = nextId ? await getDesign({ id: nextId, userId }) : null + const nextDesign = nextId ? await fetchDesign({ id: nextId, userId }) : null return { prevPrevDesign, nextDesign } } diff --git a/app/services/design/toggle-visible.service.ts b/app/services/design/toggle-visible.service.ts index 65f1592b..e5606c41 100644 --- a/app/services/design/toggle-visible.service.ts +++ b/app/services/design/toggle-visible.service.ts @@ -1,6 +1,6 @@ import { type User } from '@prisma/client' +import { getDesign } from '#app/models/design/design.get.server' import { - findFirstDesign, type IDesign, type IDesignEntityId, } from '#app/models/design/design.server' @@ -26,7 +26,7 @@ export const designToggleVisibleService = async ({ }): Promise => { try { // Step 1: get the design - const design = await getDesign({ id, userId }) + const design = await fetchtDesign({ id, userId }) const { visible } = design const type = design.type as designTypeEnum @@ -59,14 +59,14 @@ export const designToggleVisibleService = async ({ } } -const getDesign = async ({ +const fetchtDesign = async ({ id, userId, }: { id: IDesign['id'] userId: User['id'] }) => { - const design = await findFirstDesign({ + const design = await getDesign({ where: { id, ownerId: userId }, }) diff --git a/app/utils/prisma-extensions-artwork-version.ts b/app/utils/prisma-extensions-artwork-version.ts index 2be2f9f5..df72bde7 100644 --- a/app/utils/prisma-extensions-artwork-version.ts +++ b/app/utils/prisma-extensions-artwork-version.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/artwork' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedArtworkVersion = Prisma.Result< export const findFirstArtworkVersionInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.artworkVersion.findFirst({ where, diff --git a/app/utils/prisma-extensions-artwork.ts b/app/utils/prisma-extensions-artwork.ts index 229bb1c1..e4110a17 100644 --- a/app/utils/prisma-extensions-artwork.ts +++ b/app/utils/prisma-extensions-artwork.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/artwork' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedArtwork = Prisma.Result< export const findFirstArtworkInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.artwork.findFirst({ where, diff --git a/app/utils/prisma-extensions-design.ts b/app/utils/prisma-extensions-design.ts index 8bb05fd9..e80d67e1 100644 --- a/app/utils/prisma-extensions-design.ts +++ b/app/utils/prisma-extensions-design.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client' -import { designSchema, type whereArgsType } from '#app/schema/design' +import { designSchema } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -60,7 +60,7 @@ export type ExtendedDesign = Prisma.Result< export const findFirstDesignInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.design.findFirst({ where, diff --git a/app/utils/prisma-extensions-fill.ts b/app/utils/prisma-extensions-fill.ts index da855b24..f36e57ff 100644 --- a/app/utils/prisma-extensions-fill.ts +++ b/app/utils/prisma-extensions-fill.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedFill = Prisma.Result< export const findFirstFillInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.fill.findFirst({ where, diff --git a/app/utils/prisma-extensions-layer.ts b/app/utils/prisma-extensions-layer.ts index dc370d98..663efce7 100644 --- a/app/utils/prisma-extensions-layer.ts +++ b/app/utils/prisma-extensions-layer.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedLayer = Prisma.Result< export const findFirstLayerInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.layer.findFirst({ where, diff --git a/app/utils/prisma-extensions-layout.ts b/app/utils/prisma-extensions-layout.ts index 716c4f75..23840f45 100644 --- a/app/utils/prisma-extensions-layout.ts +++ b/app/utils/prisma-extensions-layout.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedLayout = Prisma.Result< export const findFirstLayoutInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.layout.findFirst({ where, diff --git a/app/utils/prisma-extensions-line.ts b/app/utils/prisma-extensions-line.ts index f999dd4d..ca8d78fe 100644 --- a/app/utils/prisma-extensions-line.ts +++ b/app/utils/prisma-extensions-line.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedLine = Prisma.Result< export const findFirstLineInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.line.findFirst({ where, diff --git a/app/utils/prisma-extensions-palette.ts b/app/utils/prisma-extensions-palette.ts index 106bda6d..29a0d1d5 100644 --- a/app/utils/prisma-extensions-palette.ts +++ b/app/utils/prisma-extensions-palette.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedPalette = Prisma.Result< export const findFirstPaletteInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.palette.findFirst({ where, diff --git a/app/utils/prisma-extensions-rotate.ts b/app/utils/prisma-extensions-rotate.ts index e003bace..99b4ee04 100644 --- a/app/utils/prisma-extensions-rotate.ts +++ b/app/utils/prisma-extensions-rotate.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedRotate = Prisma.Result< export const findFirstRotateInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.rotate.findFirst({ where, diff --git a/app/utils/prisma-extensions-size.ts b/app/utils/prisma-extensions-size.ts index 79c02c05..63de8e53 100644 --- a/app/utils/prisma-extensions-size.ts +++ b/app/utils/prisma-extensions-size.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedSize = Prisma.Result< export const findFirstSizeInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.size.findFirst({ where, diff --git a/app/utils/prisma-extensions-stroke.ts b/app/utils/prisma-extensions-stroke.ts index dc3b28a0..6c19bae8 100644 --- a/app/utils/prisma-extensions-stroke.ts +++ b/app/utils/prisma-extensions-stroke.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedStroke = Prisma.Result< export const findFirstStrokeInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.stroke.findFirst({ where, diff --git a/app/utils/prisma-extensions-template.ts b/app/utils/prisma-extensions-template.ts index 94b82e68..43854401 100644 --- a/app/utils/prisma-extensions-template.ts +++ b/app/utils/prisma-extensions-template.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client' -import { type whereArgsType } from '#app/schema/design' import { prismaExtended } from './db.server' // must be in /utils to actually connect to prisma @@ -49,7 +48,7 @@ export type ExtendedTemplate = Prisma.Result< export const findFirstTemplateInstance = async ({ where, }: { - where: whereArgsType + where: { id: string } }): Promise => { return await prismaExtended.template.findFirst({ where, From 5a4e69e4d2ebe2095f5ed9cf0a0f76672d0dd3fa Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 06:28:27 -0400 Subject: [PATCH 42/54] more cleanup --- app/models/design/design.server.ts | 56 ----------------------- app/models/design/design.update.server.ts | 54 ++++++++++++++++++++++ app/services/design/create.service.ts | 2 +- app/services/design/delete.service.ts | 8 ++-- app/services/design/move-down.service.ts | 8 ++-- app/services/design/move-up.service.ts | 8 ++-- 6 files changed, 70 insertions(+), 66 deletions(-) diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index cacd69f0..4b24f881 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -1,6 +1,5 @@ import { type Design } from '@prisma/client' import { type designTypeEnum } from '#app/schema/design' -import { prisma } from '#app/utils/db.server' import { type IArtworkVersion, type IArtworkVersionWithChildren, @@ -147,58 +146,3 @@ export interface ISelectedDesignsFiltered { layout?: ILayout template?: ITemplate } - -// only use in transactions -export const connectPrevAndNextDesigns = ({ - prevId, - nextId, -}: { - prevId: IDesign['id'] - nextId: IDesign['id'] -}) => { - const connectNextToPrev = prisma.design.update({ - where: { id: prevId }, - data: { nextId }, - }) - const connectPrevToNext = prisma.design.update({ - where: { id: nextId }, - data: { prevId }, - }) - return [connectNextToPrev, connectPrevToNext] -} - -export const updateDesignToHead = ({ id }: { id: IDesign['id'] }) => { - return prisma.design.update({ - where: { id }, - data: { prevId: null }, - }) -} - -export const updateDesignToTail = ({ id }: { id: IDesign['id'] }) => { - return prisma.design.update({ - where: { id }, - data: { nextId: null }, - }) -} - -export const updateDesignRemoveNodes = ({ id }: { id: IDesign['id'] }) => { - return prisma.design.update({ - where: { id }, - data: { prevId: null, nextId: null }, - }) -} - -export const updateDesignNodes = ({ - id, - nextId, - prevId, -}: { - id: string - nextId: string | null - prevId: string | null -}) => { - return prisma.design.update({ - where: { id }, - data: { prevId, nextId }, - }) -} diff --git a/app/models/design/design.update.server.ts b/app/models/design/design.update.server.ts index 1eb74770..e7ac040a 100644 --- a/app/models/design/design.update.server.ts +++ b/app/models/design/design.update.server.ts @@ -19,3 +19,57 @@ export const updateDesignVisible = ({ data: { visible }, }) } + +export const connectPrevAndNextDesigns = ({ + prevId, + nextId, +}: { + prevId: IDesign['id'] + nextId: IDesign['id'] +}) => { + const connectNextToPrev = prisma.design.update({ + where: { id: prevId }, + data: { nextId }, + }) + const connectPrevToNext = prisma.design.update({ + where: { id: nextId }, + data: { prevId }, + }) + return [connectNextToPrev, connectPrevToNext] +} + +export const updateDesignToHead = ({ id }: { id: IDesign['id'] }) => { + return prisma.design.update({ + where: { id }, + data: { prevId: null }, + }) +} + +export const updateDesignToTail = ({ id }: { id: IDesign['id'] }) => { + return prisma.design.update({ + where: { id }, + data: { nextId: null }, + }) +} + +export const updateDesignRemoveNodes = ({ id }: { id: IDesign['id'] }) => { + return prisma.design.update({ + where: { id }, + data: { prevId: null, nextId: null }, + }) +} + +export const updateDesignNodes = ({ + id, + nextId, + prevId, +}: { + id: string + nextId: string | null + prevId: string | null +}) => { + return prisma.design.update({ + where: { id }, + data: { prevId, nextId }, + }) +} diff --git a/app/services/design/create.service.ts b/app/services/design/create.service.ts index c87796db..4fbd19db 100644 --- a/app/services/design/create.service.ts +++ b/app/services/design/create.service.ts @@ -1,12 +1,12 @@ import { type User } from '@prisma/client' import { type IDesignCreatedResponse } from '#app/models/design/design.create.server' import { - connectPrevAndNextDesigns, type IDesignTypeCreateOverrides, type IDesign, type IDesignEntityId, type IDesignCreateOverrides, } from '#app/models/design/design.server' +import { connectPrevAndNextDesigns } from '#app/models/design/design.update.server' import { createDesignFill } from '#app/models/design-type/fill/fill.create.server' import { createDesignLayout } from '#app/models/design-type/layout/layout.create.server' import { createDesignLine } from '#app/models/design-type/line/line.create.server' diff --git a/app/services/design/delete.service.ts b/app/services/design/delete.service.ts index 7665f8a6..d6387a73 100644 --- a/app/services/design/delete.service.ts +++ b/app/services/design/delete.service.ts @@ -6,11 +6,13 @@ import { import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - updateDesignToHead, - updateDesignToTail, - connectPrevAndNextDesigns, type IDesignEntityId, } from '#app/models/design/design.server' +import { + connectPrevAndNextDesigns, + updateDesignToHead, + updateDesignToTail, +} from '#app/models/design/design.update.server' import { type designTypeEnum } from '#app/schema/design' import { type IUpdateSelectedDesignStrategy } from '#app/strategies/design/update-selected.strategy' import { prisma } from '#app/utils/db.server' diff --git a/app/services/design/move-down.service.ts b/app/services/design/move-down.service.ts index 09f4c7bc..b817a21e 100644 --- a/app/services/design/move-down.service.ts +++ b/app/services/design/move-down.service.ts @@ -2,11 +2,13 @@ import { type User } from '@prisma/client' import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - updateDesignRemoveNodes, - updateDesignNodes, type IDesignEntityId, } from '#app/models/design/design.server' -import { type IDesignUpdatedResponse } from '#app/models/design/design.update.server' +import { + updateDesignRemoveNodes, + type IDesignUpdatedResponse, + updateDesignNodes, +} from '#app/models/design/design.update.server' import { type designTypeEnum } from '#app/schema/design' import { type IUpdateSelectedDesignStrategy } from '#app/strategies/design/update-selected.strategy' import { prisma } from '#app/utils/db.server' diff --git a/app/services/design/move-up.service.ts b/app/services/design/move-up.service.ts index 0351fb46..7703cd9e 100644 --- a/app/services/design/move-up.service.ts +++ b/app/services/design/move-up.service.ts @@ -2,11 +2,13 @@ import { type User } from '@prisma/client' import { getDesign } from '#app/models/design/design.get.server' import { type IDesign, - updateDesignRemoveNodes, - updateDesignNodes, type IDesignEntityId, } from '#app/models/design/design.server' -import { type IDesignUpdatedResponse } from '#app/models/design/design.update.server' +import { + updateDesignRemoveNodes, + type IDesignUpdatedResponse, + updateDesignNodes, +} from '#app/models/design/design.update.server' import { type designTypeEnum } from '#app/schema/design' import { type IUpdateSelectedDesignStrategy } from '#app/strategies/design/update-selected.strategy' import { prisma } from '#app/utils/db.server' From 31d4ad21bee60ec838c416ed0af34745a984bf02 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 08:48:37 -0400 Subject: [PATCH 43/54] data migration script to populate design fills as a test for staging --- app/models/asset/image/image.server.ts | 4 +- .../asset/image/image.update.fit.server.ts | 4 +- app/models/design/design.server.ts | 68 ++++++++++- app/models/design/fill/fill.delete.server.ts | 13 +++ app/models/design/fill/fill.get.server.ts | 52 +++++++++ app/models/design/fill/fill.server.ts | 31 +++++ .../design/fill/fill.update.basis.server.ts | 53 +++++++++ app/models/design/fill/fill.update.server.ts | 20 ++++ app/models/design/fill/utils.ts | 39 +++++++ app/models/design/utils.ts | 107 ++++++++++++++++++ .../__components/sidebars.panel.designs.tsx | 4 +- app/schema/design.ts | 33 ++---- app/schema/design/__shared.ts | 7 ++ app/schema/design/fill.ts | 61 ++++++++++ app/services/design/clone-many.service.ts | 4 +- .../validate-submission.strategy.ts | 40 +++++-- app/utils/design.ts | 2 +- package.json | 1 + .../populate-design-attributes-by-type.ts | 103 +++++++++++++++++ 19 files changed, 600 insertions(+), 46 deletions(-) create mode 100644 app/models/design/fill/fill.delete.server.ts create mode 100644 app/models/design/fill/fill.get.server.ts create mode 100644 app/models/design/fill/fill.server.ts create mode 100644 app/models/design/fill/fill.update.basis.server.ts create mode 100644 app/models/design/fill/fill.update.server.ts create mode 100644 app/models/design/fill/utils.ts create mode 100644 app/models/design/utils.ts create mode 100644 app/schema/design/__shared.ts create mode 100644 app/schema/design/fill.ts create mode 100644 prisma/data-migrations/populate-design-attributes-by-type.ts diff --git a/app/models/asset/image/image.server.ts b/app/models/asset/image/image.server.ts index 7d53debd..48cbee76 100644 --- a/app/models/asset/image/image.server.ts +++ b/app/models/asset/image/image.server.ts @@ -23,8 +23,6 @@ export interface IAssetImageFileData { size: number lastModified?: number filename: string - fit?: IAssetImageFit - hideOnDraw?: boolean } // when adding attributes to an asset type, @@ -32,6 +30,8 @@ export interface IAssetImageFileData { // for when parsing the asset from the deserializer export interface IAssetAttributesImage extends IAssetImageFileData { altText?: string + fit?: IAssetImageFit + hideOnDraw?: boolean } export interface IAssetImageSrc { diff --git a/app/models/asset/image/image.update.fit.server.ts b/app/models/asset/image/image.update.fit.server.ts index f9f440d3..8638aaa8 100644 --- a/app/models/asset/image/image.update.fit.server.ts +++ b/app/models/asset/image/image.update.fit.server.ts @@ -7,7 +7,7 @@ import { prisma } from '#app/utils/db.server' import { type IAssetImageFit, type IAssetImage, - type IAssetImageFileData, + type IAssetAttributesImage, } from './image.server' import { stringifyAssetImageAttributes } from './utils' @@ -32,7 +32,7 @@ export interface IAssetImageUpdateFitSubmission { } interface IAssetImageUpdateFitData { - attributes: IAssetImageFileData + attributes: IAssetAttributesImage } export const updateAssetImageFit = ({ diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 4b24f881..efa3c63f 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -1,4 +1,5 @@ import { type Design } from '@prisma/client' +import { type DateOrString } from '#app/definitions/prisma-helper' import { type designTypeEnum } from '#app/schema/design' import { type IArtworkVersion, @@ -20,8 +21,73 @@ import { type IStrokeCreateOverrides } from '../design-type/stroke/stroke.create import { type IStroke } from '../design-type/stroke/stroke.server' import { type ITemplateCreateOverrides } from '../design-type/template/template.create.server' import { type ITemplate } from '../design-type/template/template.server' +import { type ILayerWithChildren } from '../layer/layer.server' +import { type IUser } from '../user/user.server' +import { + type IDesignAttributesFill, + type IDesignFill, +} from './fill/fill.server' + +// Omitting 'createdAt' and 'updatedAt' from the Design interface +// prisma query returns a string for these fields +// omit type string to ensure type safety with designTypeEnum +// omit attributes string so that extended design types can insert their own attributes +type BaseDesign = Omit< + Design, + 'type' | 'attributes' | 'createdAt' | 'updatedAt' +> + +export interface IDesign extends BaseDesign { + type: string + attributes: string + createdAt: DateOrString + updatedAt: DateOrString +} + +// when adding attributes to a design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export type IDesignAttributes = IDesignAttributesFill + +export interface IDesignParsed extends BaseDesign { + type: designTypeEnum + attributes: IDesignAttributes + createdAt: DateOrString + updatedAt: DateOrString +} + +// export type IDesignType = IDesignFill +// TODO: replace with this ^^ +export type IDesignByType = { + designFills: IDesignFill[] +} + +// export interface IDesignsByTypeWithType { +// type: designTypeEnum +// designs: IDesignType[] +// } +// TODO: replace with this ^^ + +export type IDesignParent = IArtworkVersionWithChildren | ILayerWithChildren -export interface IDesign extends Design {} +interface IDesignData { + visible: boolean + selected: boolean +} + +export interface IDesignSubmission extends IDesignData { + userId: IUser['id'] +} + +export interface IDesignCreateData extends IDesignData { + ownerId: IUser['id'] + type: designTypeEnum + attributes: IDesignAttributes +} + +export interface IDesignUpdateData extends IDesignData { + attributes: IDesignAttributes +} export type IDesignIdOrNull = IDesign['id'] | null | undefined diff --git a/app/models/design/fill/fill.delete.server.ts b/app/models/design/fill/fill.delete.server.ts new file mode 100644 index 00000000..30e54553 --- /dev/null +++ b/app/models/design/fill/fill.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignFill } from './fill.server' + +export interface IDesignFillDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignFill = ({ id }: { id: IDesignFill['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/fill/fill.get.server.ts b/app/models/design/fill/fill.get.server.ts new file mode 100644 index 00000000..c266cd10 --- /dev/null +++ b/app/models/design/fill/fill.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignFill } from './fill.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design fill.', + ) + } +} + +export const getDesignFill = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.FILL, + }, + }) + invariant(design, 'Design Fill Not found') + return deserializeDesign({ design }) as IDesignFill +} diff --git a/app/models/design/fill/fill.server.ts b/app/models/design/fill/fill.server.ts new file mode 100644 index 00000000..fbbd85f4 --- /dev/null +++ b/app/models/design/fill/fill.server.ts @@ -0,0 +1,31 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignFill extends IDesignParsed { + type: typeof DesignTypeEnum.FILL + attributes: IDesignAttributesFill +} + +export type IDesignFillBasis = + | 'defined' + | 'random' + | 'palette-selected' + | 'palette-random' + | 'palette-loop' + | 'palette-loop-reverse' + | 'pixel' + +export type IDesignFillStyle = 'solid' | 'none' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesFill { + basis?: IDesignFillBasis + style?: IDesignFillStyle + value?: string +} + +export interface IDesignFillSubmission + extends IDesignSubmission, + IDesignAttributesFill {} diff --git a/app/models/design/fill/fill.update.basis.server.ts b/app/models/design/fill/fill.update.basis.server.ts new file mode 100644 index 00000000..c3efc165 --- /dev/null +++ b/app/models/design/fill/fill.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignFillBasisSchema } from '#app/schema/fill' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignAttributesFill, + type IDesignFill, + type IDesignFillBasis, +} from './fill.server' +import { stringifyDesignFillAttributes } from './utils' + +export const validateEditBasisDesignFillSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignFillBasisSchema, + strategy, + }) +} + +export interface IDesignFillUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignFill['id'] + basis: IDesignFillBasis +} + +interface IDesignFillUpdateBasisData { + attributes: IDesignAttributesFill +} + +export const updateDesignFillBasis = ({ + id, + data, +}: { + id: IDesignFill['id'] + data: IDesignFillUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignFillAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/fill/fill.update.server.ts b/app/models/design/fill/fill.update.server.ts new file mode 100644 index 00000000..f8b5d9d4 --- /dev/null +++ b/app/models/design/fill/fill.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignFillSubmission, + type IDesignAttributesFill, + type IDesignFill, +} from './fill.server' + +export interface IDesignFillUpdatedResponse { + success: boolean + message?: string + updatedDesignFill?: IDesign +} + +export interface IDesignFillUpdateSubmission extends IDesignFillSubmission { + id: IDesignFill['id'] +} + +export interface IDesignFillUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesFill +} diff --git a/app/models/design/fill/utils.ts b/app/models/design/fill/utils.ts new file mode 100644 index 00000000..fd4f4a1b --- /dev/null +++ b/app/models/design/fill/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesFillSchema } from '#app/schema/design/fill' +import { type IDesignAttributesFill } from './fill.server' + +export const parseDesignFillAttributes = ( + attributes: string, +): IDesignAttributesFill => { + try { + return DesignAttributesFillSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignFillAttributes = ( + attributes: IDesignAttributesFill, +): string => { + try { + return JSON.stringify(DesignAttributesFillSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts new file mode 100644 index 00000000..af2f1721 --- /dev/null +++ b/app/models/design/utils.ts @@ -0,0 +1,107 @@ +import { ZodError } from 'zod' +import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' +import { type IDesign, type IDesignParsed } from './design.server' +import { parseDesignFillAttributes } from './fill/utils' + +export const deserializeDesigns = ({ + designs, +}: { + designs: IDesign[] +}): IDesignParsed[] => { + return designs.map(design => deserializeDesign({ design })) +} + +export const deserializeDesign = ({ + design, +}: { + design: IDesign +}): IDesignParsed => { + const type = design.type as designTypeEnum + const { attributes } = design + + const validatedDesignAttributes = validateDesignAttributes({ + type, + attributes, + }) + + return { + ...design, + type, + attributes: validatedDesignAttributes, + } +} + +export const validateDesignAttributes = ({ + type, + attributes, +}: { + type: designTypeEnum + attributes: IDesign['attributes'] +}) => { + try { + switch (type) { + case DesignTypeEnum.FILL: + return parseDesignFillAttributes(attributes) + default: + throw new Error(`Unsupported design type: ${type}`) + } + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for design type ${type}: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for design type ${type}: ${error.message}`, + ) + } + } +} + +export const filterDesignsVisible = ({ + designs, +}: { + designs: IDesignParsed[] +}): IDesignParsed[] => { + return designs.filter(design => design.visible) +} + +export const filterDesignType = ({ + designs, + type, +}: { + designs: IDesignParsed[] + type: designTypeEnum +}): IDesignParsed[] => { + return designs.filter(design => design.type === type) +} + +// export const groupDesignsByType = ({ +// designs, +// }: { +// designs: IDesignParsed[] +// }): IDesignByType => { +// const DesignImages = filterDesignType({ +// designs, +// type: DesignTypeEnum.IMAGE, +// }) as IDesignImage[] + +// return { +// DesignImages, +// } +// } + +// export const DesignsByTypeToPanelArray = ({ +// designs, +// }: { +// designs: IDesignByType +// }): IDesignsByTypeWithType[] => { +// const { DesignImages } = designs + +// return [ +// { +// type: DesignTypeEnum.IMAGE, +// designs: DesignImages, +// }, +// ] +// } diff --git a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.tsx b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.tsx index 44427997..8ed67041 100644 --- a/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.tsx +++ b/app/routes/editor+/projects+/$projectSlug_+/artworks+/$artworkSlug+/__components/sidebars.panel.designs.tsx @@ -6,7 +6,7 @@ import { type IDashboardPanelEntityActionStrategy } from '#app/strategies/compon import { type IDashboardPanelUpdateEntityOrderStrategy } from '#app/strategies/component/dashboard-panel/update-entity-order.strategy' import { designsByTypeToPanelArray, - filterAndOrderDesignsByType, + groupAndOrderDesignsByType, } from '#app/utils/design' export const PanelDesigns = ({ @@ -20,7 +20,7 @@ export const PanelDesigns = ({ strategyReorder: IDashboardPanelUpdateEntityOrderStrategy strategyActions: IDashboardPanelEntityActionStrategy }) => { - const orderedDesigns = filterAndOrderDesignsByType({ + const orderedDesigns = groupAndOrderDesignsByType({ designs: parent.designs, }) const designTypePanels = designsByTypeToPanelArray({ diff --git a/app/schema/design.ts b/app/schema/design.ts index 91fae498..db997974 100644 --- a/app/schema/design.ts +++ b/app/schema/design.ts @@ -28,6 +28,13 @@ export const DesignTypeEnum = { } as const export type designTypeEnum = ObjectValues +export const DesignParentTypeEnum = { + ARTWORK_VERSION: 'artworkVersion', + LAYER: 'layer', + // add more design parent types here +} as const +export type designParentTypeEnum = ObjectValues + export type DesignParentType = IArtworkVersionWithChildren | ILayerWithDesigns export const DesignCloneSourceTypeEnum = { @@ -71,29 +78,3 @@ export type ToggleVisibleDesignSchemaType = export type DeleteDesignSchemaType = | typeof DeleteArtworkVersionDesignSchema | typeof DeleteLayerDesignSchema - -// export type selectArgsType = z.infer -// const selectArgs = z.object({ -// id: z.boolean().optional(), -// }) - -// export type whereArgsType = z.infer -// const arrayOfIds = z.object({ in: z.array(z.string()) }) -// const zodStringOrNull = z.union([z.string(), z.null()]) -// const whereArgs = z.object({ -// id: z.union([z.string(), arrayOfIds]).optional(), -// type: z.nativeEnum(DesignTypeEnum).optional(), -// visible: z.boolean().optional(), -// selected: z.boolean().optional(), -// ownerId: z.string().optional(), -// artworkVersionId: z.string().optional(), -// layerId: z.string().optional(), -// prevId: zodStringOrNull.optional(), -// nextId: zodStringOrNull.optional(), -// }) - -// export type findDesignArgsType = z.infer -// export const findDesignArgs = z.object({ -// where: whereArgs, -// select: selectArgs.optional(), -// }) diff --git a/app/schema/design/__shared.ts b/app/schema/design/__shared.ts new file mode 100644 index 00000000..02736732 --- /dev/null +++ b/app/schema/design/__shared.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const DesignDataSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + ownerId: z.string(), +}) diff --git a/app/schema/design/fill.ts b/app/schema/design/fill.ts new file mode 100644 index 00000000..3658c803 --- /dev/null +++ b/app/schema/design/fill.ts @@ -0,0 +1,61 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' +import { HexcodeSchema } from '../colors' + +export const FillBasisTypeEnum = { + DEFINED: 'defined', // exact hex value + RANDOM: 'random', // random hex value + PALETTE_SELECTED: 'palette-selected', // first palette in array + PALETTE_RANDOM: 'palette-random', // random palette in array + PALETTE_LOOP: 'palette-loop', // loop palette array by index + PALETTE_LOOP_REVERSE: 'palette-loop-reverse', // loop reversed palette array by index + PIXEL: 'pixel', // pixel color + // add more basis types here +} as const +export const FillStyleTypeEnum = { + SOLID: 'solid', // flat color + NONE: 'none', // no fill + // add more styles here, like gradient, pattern, etc. +} as const +export type fillBasisTypeEnum = ObjectValues +export type fillStyleTypeEnum = ObjectValues + +const FillBasisSchema = z.nativeEnum(FillBasisTypeEnum) +const FillStyleSchema = z.nativeEnum(FillStyleTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesFillSchema = z.object({ + basis: FillBasisSchema.optional(), + style: FillStyleSchema.optional(), + value: HexcodeSchema.optional(), +}) + +export const NewDesignFillSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: FillBasisSchema.default(FillBasisTypeEnum.DEFINED), + style: FillStyleSchema.default(FillStyleTypeEnum.SOLID), + value: HexcodeSchema.default('000000'), +}) + +export const EditDesignFillBasisSchema = z.object({ + id: z.string(), + basis: FillBasisSchema, +}) + +export const EditDesignFillStyleSchema = z.object({ + id: z.string(), + style: FillStyleSchema, +}) + +export const EditDesignFillValueSchema = z.object({ + id: z.string(), + value: HexcodeSchema, +}) + +export const DeleteDesignFillSchema = z.object({ + id: z.string(), +}) diff --git a/app/services/design/clone-many.service.ts b/app/services/design/clone-many.service.ts index ba603951..633454b4 100644 --- a/app/services/design/clone-many.service.ts +++ b/app/services/design/clone-many.service.ts @@ -11,7 +11,7 @@ import { } from '#app/schema/design' import { type ICloneDesignsStrategy } from '#app/strategies/design/clone.strategy' import { cloneDesignTypeStrategies } from '#app/strategies/design-type/clone.strategy' -import { filterAndOrderDesignsByType } from '#app/utils/design' +import { groupAndOrderDesignsByType } from '#app/utils/design' import { cloneDesignTypesService } from './design-type/clone-many.service' export const cloneDesignsService = async ({ @@ -35,7 +35,7 @@ export const cloneDesignsService = async ({ }) // Step 2: separate designs by type and order - const sourceDesignsByType = filterAndOrderDesignsByType({ + const sourceDesignsByType = groupAndOrderDesignsByType({ designs: sourceDesigns, }) diff --git a/app/strategies/validate-submission.strategy.ts b/app/strategies/validate-submission.strategy.ts index a2655d75..68088a25 100644 --- a/app/strategies/validate-submission.strategy.ts +++ b/app/strategies/validate-submission.strategy.ts @@ -1,4 +1,3 @@ -import { type User } from '@prisma/client' import { type z } from 'zod' import { getArtwork } from '#app/models/artwork/artwork.get.server' import { getArtworkBranch } from '#app/models/artwork-branch/artwork-branch.get.server' @@ -6,11 +5,12 @@ import { getArtworkVersion } from '#app/models/artwork-version/artwork-version.g import { getAsset } from '#app/models/asset/asset.get.server' import { getDesign } from '#app/models/design/design.get.server' import { getLayer } from '#app/models/layer/layer.get.server' +import { type IUser } from '#app/models/user/user.server' import { addNotFoundIssue } from '#app/utils/conform-utils' export interface IValidateSubmissionStrategy { validateFormDataEntity(args: { - userId: User['id'] + userId: IUser['id'] data: any ctx: z.RefinementCtx }): Promise @@ -24,7 +24,7 @@ export class ValidateArtworkParentSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -44,7 +44,7 @@ export class ValidateArtworkBranchParentSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -64,7 +64,7 @@ export class ValidateArtworkVersionSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -84,7 +84,7 @@ export class ValidateArtworkVersionParentSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -104,7 +104,7 @@ export class ValidateDesignParentSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -124,7 +124,7 @@ export class ValidateLayerSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -144,7 +144,7 @@ export class ValidateLayerParentSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -164,7 +164,7 @@ export class ValidateAssetSubmissionStrategy data, ctx, }: { - userId: User['id'] + userId: IUser['id'] data: any ctx: any }): Promise { @@ -175,3 +175,23 @@ export class ValidateAssetSubmissionStrategy if (!asset) ctx.addIssue(addNotFoundIssue('Asset')) } } + +export class ValidateDesignSubmissionStrategy + implements IValidateSubmissionStrategy +{ + async validateFormDataEntity({ + userId, + data, + ctx, + }: { + userId: IUser['id'] + data: any + ctx: any + }): Promise { + const { id } = data + const asset = await getDesign({ + where: { id, ownerId: userId }, + }) + if (!asset) ctx.addIssue(addNotFoundIssue('Design')) + } +} diff --git a/app/utils/design.ts b/app/utils/design.ts index 236b4ba8..5ac2c535 100644 --- a/app/utils/design.ts +++ b/app/utils/design.ts @@ -26,7 +26,7 @@ import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { orderLinkedItems } from './linked-list.utils' import { safelyAssignValue } from './typescript-helpers' -export const filterAndOrderDesignsByType = ({ +export const groupAndOrderDesignsByType = ({ designs, }: { designs: IDesignWithType[] diff --git a/package.json b/package.json index 4ddcc910..deb5b4f6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "fly:console:prisma:studio:proxy:prod": "npm run fly:console:prisma:studio:proxy -- --env=production", "fly:console:prisma:studio:proxy:staging": "npm run fly:console:prisma:studio:proxy -- --env=staging", "fly:console:prisma:studio:proxy": "./scripts/fly.io/app-console-prisma-studio-proxy.sh", + "data:migrate": "npx vite-node ./prisma/data-migrations/populate-design-attributes-by-type.ts", "prepare": "husky install" }, "eslintIgnore": [ diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts new file mode 100644 index 00000000..b5c9f483 --- /dev/null +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -0,0 +1,103 @@ +import { getDesignsWithType } from '#app/models/design/design.get.server' +import { + type IDesignWithFill, + type IDesignWithType, +} from '#app/models/design/design.server' +import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' +import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' + +// designs will have attributes string as json now +// new prisma migration created the following columns on design table: +// - attributes, updatedAt, + +// the goal of this script: +// - for each design +// - - deserialize the attributes from the design type +// - - validate the attributes +// - - update the design with the validated attributes + +// after this script runs, there is more work to do: +// - routing, ui, and api changes to support artboard versions and branches + +// how to run: +// 1) add the folowing to package.json scripts: +// "data:migrate": "npx vite-node ./prisma/data-migrations/populate-design-attributes-by-type.ts" +// 2) run `npm run data:migrate` +// 3) remove the script from package.json + +export const populateDesignAttributesByType = async () => { + console.log('populateDesignAttributesByType begin 🎬') + + // Step 1: remove all artboard branches and versions from previous runs + await clear() + + // Step 2: get all designs + const designs = await getDesigns() + + const designUpdatePromises: Promise[] = [] + + // Step 2: update each design with the correct attributes + for (const design of designs) { + const updatePromise = updateDesignAttributesPromise(design) + designUpdatePromises.push(updatePromise) + } + + // Step 3: wait for all updates to complete + await Promise.all(designUpdatePromises) + + console.log('populateDesignAttributesByType end 🏁') +} + +const clear = async () => { + await prisma.design.updateMany({ + data: { + attributes: '{}', + }, + }) +} + +const getDesigns = async (): Promise => { + return await getDesignsWithType({ where: {} }) +} + +const updateDesignAttributesPromise = (design: IDesignWithType) => { + switch (design.type) { + case DesignTypeEnum.FILL: + return updateDesignFillAttributes(design as IDesignWithFill) + default: + return Promise.resolve() + // throw new Error(`Unsupported design type: ${design.type}`) + } +} + +const updateDesignFillAttributes = (design: IDesignWithFill) => { + const { fill } = design + const { basis, style, value } = fill + const attributes = { + basis, + style, + value, + } as IDesignAttributesFill + const jsonAttributes = stringifyDesignFillAttributes(attributes) + + return prisma.design + .update({ + where: { id: design.id }, + data: { + attributes: jsonAttributes, + }, + }) + .then(() => { + console.log(`Updated design fill attributes for design ${design.id}`) + }) + .catch(error => { + console.error( + `Failed to update design fill attributes for design ${design.id}`, + error, + ) + }) +} + +await populateDesignAttributesByType() From 1629600eeb140f7e7c37b42858dc2bdc1642d11b Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 09:26:02 -0400 Subject: [PATCH 44/54] added stroke design; updated package to run script outside of dev --- app/models/design/design.server.ts | 7 +- .../design/stroke/stroke.delete.server.ts | 13 +++ app/models/design/stroke/stroke.get.server.ts | 52 +++++++++ app/models/design/stroke/stroke.server.ts | 31 ++++++ .../stroke/stroke.update.basis.server.ts | 53 +++++++++ .../design/stroke/stroke.update.server.ts | 20 ++++ app/models/design/stroke/utils.ts | 39 +++++++ app/models/design/utils.ts | 3 + app/schema/design/stroke.ts | 60 ++++++++++ package-lock.json | 104 +----------------- package.json | 2 +- .../populate-design-attributes-by-type.ts | 33 ++++++ .../fly.io/app-console-prisma-studio-proxy.sh | 4 +- scripts/fly.io/app-console-prisma-studio.sh | 4 +- 14 files changed, 321 insertions(+), 104 deletions(-) create mode 100644 app/models/design/stroke/stroke.delete.server.ts create mode 100644 app/models/design/stroke/stroke.get.server.ts create mode 100644 app/models/design/stroke/stroke.server.ts create mode 100644 app/models/design/stroke/stroke.update.basis.server.ts create mode 100644 app/models/design/stroke/stroke.update.server.ts create mode 100644 app/models/design/stroke/utils.ts create mode 100644 app/schema/design/stroke.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index efa3c63f..58f8de65 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -27,6 +27,10 @@ import { type IDesignAttributesFill, type IDesignFill, } from './fill/fill.server' +import { + type IDesignStroke, + type IDesignAttributesStroke, +} from './stroke/stroke.server' // Omitting 'createdAt' and 'updatedAt' from the Design interface // prisma query returns a string for these fields @@ -47,7 +51,7 @@ export interface IDesign extends BaseDesign { // when adding attributes to a design type, // make sure it starts as optional or is set to a default value // for when parsing the design from the deserializer -export type IDesignAttributes = IDesignAttributesFill +export type IDesignAttributes = IDesignAttributesFill | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -60,6 +64,7 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] + designStroke: IDesignStroke[] } // export interface IDesignsByTypeWithType { diff --git a/app/models/design/stroke/stroke.delete.server.ts b/app/models/design/stroke/stroke.delete.server.ts new file mode 100644 index 00000000..dcb8ea4b --- /dev/null +++ b/app/models/design/stroke/stroke.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignStroke } from './stroke.server' + +export interface IDesignStrokeDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignStroke = ({ id }: { id: IDesignStroke['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/stroke/stroke.get.server.ts b/app/models/design/stroke/stroke.get.server.ts new file mode 100644 index 00000000..74d1ebcb --- /dev/null +++ b/app/models/design/stroke/stroke.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignStroke } from './stroke.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design stroke.', + ) + } +} + +export const getDesignStroke = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.STROKE, + }, + }) + invariant(design, 'Design Stroke Not found') + return deserializeDesign({ design }) as IDesignStroke +} diff --git a/app/models/design/stroke/stroke.server.ts b/app/models/design/stroke/stroke.server.ts new file mode 100644 index 00000000..dddb91bd --- /dev/null +++ b/app/models/design/stroke/stroke.server.ts @@ -0,0 +1,31 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignStroke extends IDesignParsed { + type: typeof DesignTypeEnum.STROKE + attributes: IDesignAttributesStroke +} + +export type IDesignStrokeBasis = + | 'defined' + | 'random' + | 'palette-selected' + | 'palette-random' + | 'palette-loop' + | 'palette-loop-reverse' + | 'pixel' + +export type IDesignStrokeStyle = 'solid' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesStroke { + basis?: IDesignStrokeBasis + style?: IDesignStrokeStyle + value?: string +} + +export interface IDesignStrokeSubmission + extends IDesignSubmission, + IDesignAttributesStroke {} diff --git a/app/models/design/stroke/stroke.update.basis.server.ts b/app/models/design/stroke/stroke.update.basis.server.ts new file mode 100644 index 00000000..33da571a --- /dev/null +++ b/app/models/design/stroke/stroke.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignStrokeBasisSchema } from '#app/schema/stroke' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignAttributesStroke, + type IDesignStroke, + type IDesignStrokeBasis, +} from './stroke.server' +import { stringifyDesignStrokeAttributes } from './utils' + +export const validateEditBasisDesignStrokeSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignStrokeBasisSchema, + strategy, + }) +} + +export interface IDesignStrokeUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignStroke['id'] + basis: IDesignStrokeBasis +} + +interface IDesignStrokeUpdateBasisData { + attributes: IDesignAttributesStroke +} + +export const updateDesignStrokeBasis = ({ + id, + data, +}: { + id: IDesignStroke['id'] + data: IDesignStrokeUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignStrokeAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/stroke/stroke.update.server.ts b/app/models/design/stroke/stroke.update.server.ts new file mode 100644 index 00000000..8f486003 --- /dev/null +++ b/app/models/design/stroke/stroke.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignStrokeSubmission, + type IDesignAttributesStroke, + type IDesignStroke, +} from './stroke.server' + +export interface IDesignStrokeUpdatedResponse { + success: boolean + message?: string + updatedDesignStroke?: IDesign +} + +export interface IDesignStrokeUpdateSubmission extends IDesignStrokeSubmission { + id: IDesignStroke['id'] +} + +export interface IDesignStrokeUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesStroke +} diff --git a/app/models/design/stroke/utils.ts b/app/models/design/stroke/utils.ts new file mode 100644 index 00000000..c2596cb5 --- /dev/null +++ b/app/models/design/stroke/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesStrokeSchema } from '#app/schema/design/stroke' +import { type IDesignAttributesStroke } from './stroke.server' + +export const parseDesignStrokeAttributes = ( + attributes: string, +): IDesignAttributesStroke => { + try { + return DesignAttributesStrokeSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignStrokeAttributes = ( + attributes: IDesignAttributesStroke, +): string => { + try { + return JSON.stringify(DesignAttributesStrokeSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index af2f1721..9f064440 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -2,6 +2,7 @@ import { ZodError } from 'zod' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' +import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ designs, @@ -42,6 +43,8 @@ export const validateDesignAttributes = ({ switch (type) { case DesignTypeEnum.FILL: return parseDesignFillAttributes(attributes) + case DesignTypeEnum.STROKE: + return parseDesignStrokeAttributes(attributes) default: throw new Error(`Unsupported design type: ${type}`) } diff --git a/app/schema/design/stroke.ts b/app/schema/design/stroke.ts new file mode 100644 index 00000000..17fa5412 --- /dev/null +++ b/app/schema/design/stroke.ts @@ -0,0 +1,60 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' +import { HexcodeSchema } from '../colors' + +export const StrokeBasisTypeEnum = { + DEFINED: 'defined', // exact hex value + RANDOM: 'random', // random hex value + PALETTE_SELECTED: 'palette-selected', // first palette in array + PALETTE_RANDOM: 'palette-random', // random palette in array + PALETTE_LOOP: 'palette-loop', // loop palette array by index + PALETTE_LOOP_REVERSE: 'palette-loop-reverse', // loop reversed palette array by index + PIXEL: 'pixel', // pixel color + // add more basis types here +} as const +export const StrokeStyleTypeEnum = { + SOLID: 'solid', // flat color + // add more styles here, like gradient, pattern, etc. +} as const +export type strokeBasisTypeEnum = ObjectValues +export type strokeStyleTypeEnum = ObjectValues + +const StrokeBasisSchema = z.nativeEnum(StrokeBasisTypeEnum) +const StrokeStyleSchema = z.nativeEnum(StrokeStyleTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesStrokeSchema = z.object({ + basis: StrokeBasisSchema.optional(), + style: StrokeStyleSchema.optional(), + value: HexcodeSchema.optional(), +}) + +export const NewDesignStrokeSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: StrokeBasisSchema.default(StrokeBasisTypeEnum.DEFINED), + style: StrokeStyleSchema.default(StrokeStyleTypeEnum.SOLID), + value: HexcodeSchema.default('000000'), +}) + +export const EditDesignStrokeBasisSchema = z.object({ + id: z.string(), + basis: StrokeBasisSchema, +}) + +export const EditDesignStrokeStyleSchema = z.object({ + id: z.string(), + style: StrokeStyleSchema, +}) + +export const EditDesignStrokeValueSchema = z.object({ + id: z.string(), + value: HexcodeSchema, +}) + +export const DeleteDesignStrokeSchema = z.object({ + id: z.string(), +}) diff --git a/package-lock.json b/package-lock.json index 668e59f5..a9d8caed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "tailwindcss": "^3.4.0", "tailwindcss-animate": "^1.0.7", "tailwindcss-radix": "^2.8.0", + "vite-node": "^1.6.0", "zod": "^3.22.4" }, "devDependencies": { @@ -137,7 +138,6 @@ "tsx": "^4.6.0", "typescript": "^5.3.2", "vite": "^5.2.11", - "vite-node": "^1.4.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^0.34.6" }, @@ -4501,7 +4501,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -4514,7 +4513,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -4527,7 +4525,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -4540,7 +4537,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -4553,7 +4549,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4566,7 +4561,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4579,7 +4573,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4592,7 +4585,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4605,7 +4597,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4618,7 +4609,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4631,7 +4621,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4644,7 +4633,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4657,7 +4645,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -4670,7 +4657,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -4683,7 +4669,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -4696,7 +4681,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -5961,8 +5945,7 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -7500,7 +7483,6 @@ "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, "engines": { "node": ">=8" } @@ -15401,8 +15383,7 @@ "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, "node_modules/pathval": { "version": "1.1.1", @@ -17450,7 +17431,6 @@ "version": "4.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -20101,7 +20081,6 @@ "version": "5.2.13", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", - "dev": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -20156,7 +20135,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", - "dev": true, "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", @@ -20200,7 +20178,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -20216,7 +20193,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -20232,7 +20208,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -20248,7 +20223,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -20264,7 +20238,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -20280,7 +20253,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -20296,7 +20268,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -20312,7 +20283,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -20328,7 +20298,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20344,7 +20313,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20360,7 +20328,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20376,7 +20343,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20392,7 +20358,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20408,7 +20373,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20424,7 +20388,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20440,7 +20403,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20456,7 +20418,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -20472,7 +20433,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -20488,7 +20448,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -20504,7 +20463,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -20520,7 +20478,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -20536,7 +20493,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -20552,7 +20508,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -20565,7 +20520,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -23990,112 +23944,96 @@ "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "dev": true, "optional": true }, "@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "dev": true, "optional": true }, "@rushstack/eslint-patch": { @@ -25053,8 +24991,7 @@ "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "@types/estree-jsx": { "version": "1.0.5", @@ -26213,8 +26150,7 @@ "cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" }, "cacache": { "version": "17.1.4", @@ -31887,8 +31823,7 @@ "pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, "pathval": { "version": "1.1.1", @@ -33215,7 +33150,6 @@ "version": "4.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, "requires": { "@rollup/rollup-android-arm-eabi": "4.18.0", "@rollup/rollup-android-arm64": "4.18.0", @@ -35030,7 +34964,6 @@ "version": "5.2.13", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz", "integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==", - "dev": true, "requires": { "esbuild": "^0.20.1", "fsevents": "~2.3.3", @@ -35042,168 +34975,144 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "dev": true, "optional": true }, "@esbuild/android-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "dev": true, "optional": true }, "@esbuild/android-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "dev": true, "optional": true }, "@esbuild/android-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "dev": true, "optional": true }, "@esbuild/darwin-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "dev": true, "optional": true }, "@esbuild/darwin-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "dev": true, "optional": true }, "@esbuild/freebsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "dev": true, "optional": true }, "@esbuild/linux-arm": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "dev": true, "optional": true }, "@esbuild/linux-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "dev": true, "optional": true }, "@esbuild/linux-ia32": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "dev": true, "optional": true }, "@esbuild/linux-loong64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "dev": true, "optional": true }, "@esbuild/linux-mips64el": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "dev": true, "optional": true }, "@esbuild/linux-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "dev": true, "optional": true }, "@esbuild/linux-riscv64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "dev": true, "optional": true }, "@esbuild/linux-s390x": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "dev": true, "optional": true }, "@esbuild/linux-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "dev": true, "optional": true }, "@esbuild/netbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "dev": true, "optional": true }, "@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "dev": true, "optional": true }, "@esbuild/sunos-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "dev": true, "optional": true }, "@esbuild/win32-arm64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "dev": true, "optional": true }, "@esbuild/win32-ia32": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "dev": true, "optional": true }, "@esbuild/win32-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "dev": true, "optional": true }, "esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "requires": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", @@ -35236,7 +35145,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", - "dev": true, "requires": { "cac": "^6.7.14", "debug": "^4.3.4", diff --git a/package.json b/package.json index deb5b4f6..44d43e09 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "tailwindcss": "^3.4.0", "tailwindcss-animate": "^1.0.7", "tailwindcss-radix": "^2.8.0", + "vite-node": "^1.6.0", "zod": "^3.22.4" }, "devDependencies": { @@ -181,7 +182,6 @@ "tsx": "^4.6.0", "typescript": "^5.3.2", "vite": "^5.2.11", - "vite-node": "^1.4.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^0.34.6" }, diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index b5c9f483..0d7e7806 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -1,10 +1,13 @@ import { getDesignsWithType } from '#app/models/design/design.get.server' import { + type IDesignWithStroke, type IDesignWithFill, type IDesignWithType, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' +import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' +import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' @@ -66,6 +69,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { switch (design.type) { case DesignTypeEnum.FILL: return updateDesignFillAttributes(design as IDesignWithFill) + case DesignTypeEnum.STROKE: + return updateDesignStrokeAttributes(design as IDesignWithStroke) default: return Promise.resolve() // throw new Error(`Unsupported design type: ${design.type}`) @@ -100,4 +105,32 @@ const updateDesignFillAttributes = (design: IDesignWithFill) => { }) } +const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { + const { stroke } = design + const { basis, style, value } = stroke + const attributes = { + basis, + style, + value, + } as IDesignAttributesStroke + const jsonAttributes = stringifyDesignStrokeAttributes(attributes) + + return prisma.design + .update({ + where: { id: design.id }, + data: { + attributes: jsonAttributes, + }, + }) + .then(() => { + console.log(`Updated design stroke attributes for design ${design.id}`) + }) + .catch(error => { + console.error( + `Failed to update design stroke attributes for design ${design.id}`, + error, + ) + }) +} + await populateDesignAttributesByType() diff --git a/scripts/fly.io/app-console-prisma-studio-proxy.sh b/scripts/fly.io/app-console-prisma-studio-proxy.sh index 07507eb9..b381927f 100755 --- a/scripts/fly.io/app-console-prisma-studio-proxy.sh +++ b/scripts/fly.io/app-console-prisma-studio-proxy.sh @@ -5,9 +5,9 @@ # chmod +x scripts/fly.io/app-console-prisma-studio-proxy.sh # run prisma studio on fly app -# npm run fly:app:console:prisma:studio +# npm run fly:console:prisma:studio # run proxy to prisma studio on fly app to local port (separate terminal) -# npm run fly:app:console:prisma:studio:proxy +# npm run fly:console:prisma:studio:proxy # Check if the script is running in production environment if [ "$NODE_ENV" = "production" ]; then diff --git a/scripts/fly.io/app-console-prisma-studio.sh b/scripts/fly.io/app-console-prisma-studio.sh index 776da176..efb19888 100755 --- a/scripts/fly.io/app-console-prisma-studio.sh +++ b/scripts/fly.io/app-console-prisma-studio.sh @@ -5,9 +5,9 @@ # chmod +x scripts/fly.io/app-console-prisma-studio.sh # run prisma studio on fly app -# npm run fly:app:console:prisma:studio +# npm run fly:console:prisma:studio # run proxy to prisma studio on fly app to local port (separate terminal) -# npm run fly:app:console:prisma:studio:proxy +# npm run fly:console:prisma:studio:proxy # Check if the script is running in production environment if [ "$NODE_ENV" = "production" ]; then From fbb6103d6b3a3dcdf6dcc404c92dc2c162f03bc1 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 10:36:20 -0400 Subject: [PATCH 45/54] tsx to run command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44d43e09..e2334f03 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "fly:console:prisma:studio:proxy:prod": "npm run fly:console:prisma:studio:proxy -- --env=production", "fly:console:prisma:studio:proxy:staging": "npm run fly:console:prisma:studio:proxy -- --env=staging", "fly:console:prisma:studio:proxy": "./scripts/fly.io/app-console-prisma-studio-proxy.sh", - "data:migrate": "npx vite-node ./prisma/data-migrations/populate-design-attributes-by-type.ts", + "data:migrate": "tsx ./prisma/data-migrations/populate-design-attributes-by-type.ts", "prepare": "husky install" }, "eslintIgnore": [ From e25129960eef7dc4fabad005d2550cdf4d656d24 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:02:04 -0400 Subject: [PATCH 46/54] needed npx --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2334f03..83b22f21 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "fly:console:prisma:studio:proxy:prod": "npm run fly:console:prisma:studio:proxy -- --env=production", "fly:console:prisma:studio:proxy:staging": "npm run fly:console:prisma:studio:proxy -- --env=staging", "fly:console:prisma:studio:proxy": "./scripts/fly.io/app-console-prisma-studio-proxy.sh", - "data:migrate": "tsx ./prisma/data-migrations/populate-design-attributes-by-type.ts", + "data:migrate": "npx tsx ./prisma/data-migrations/populate-design-attributes-by-type.ts", "prepare": "husky install" }, "eslintIgnore": [ From 43006588c3a278412e668e6dddb21f8562a477db Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:29:54 -0400 Subject: [PATCH 47/54] layout --- app/models/design/design.server.ts | 10 +++- .../design/layout/layout.delete.server.ts | 13 +++++ app/models/design/layout/layout.get.server.ts | 52 +++++++++++++++++ app/models/design/layout/layout.server.ts | 23 ++++++++ .../design/layout/layout.update.server.ts | 20 +++++++ .../layout/layout.update.style.server.ts | 53 +++++++++++++++++ app/models/design/layout/utils.ts | 39 +++++++++++++ app/models/design/utils.ts | 3 + app/schema/design/layout.ts | 57 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 34 +++++++++++ 10 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 app/models/design/layout/layout.delete.server.ts create mode 100644 app/models/design/layout/layout.get.server.ts create mode 100644 app/models/design/layout/layout.server.ts create mode 100644 app/models/design/layout/layout.update.server.ts create mode 100644 app/models/design/layout/layout.update.style.server.ts create mode 100644 app/models/design/layout/utils.ts create mode 100644 app/schema/design/layout.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 58f8de65..f6eef028 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -27,6 +27,10 @@ import { type IDesignAttributesFill, type IDesignFill, } from './fill/fill.server' +import { + type IDesignLayout, + type IDesignAttributesLayout, +} from './layout/layout.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -51,7 +55,10 @@ export interface IDesign extends BaseDesign { // when adding attributes to a design type, // make sure it starts as optional or is set to a default value // for when parsing the design from the deserializer -export type IDesignAttributes = IDesignAttributesFill | IDesignAttributesStroke +export type IDesignAttributes = + | IDesignAttributesFill + | IDesignAttributesLayout + | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -64,6 +71,7 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] + designLayoutss: IDesignLayout[] designStroke: IDesignStroke[] } diff --git a/app/models/design/layout/layout.delete.server.ts b/app/models/design/layout/layout.delete.server.ts new file mode 100644 index 00000000..429947eb --- /dev/null +++ b/app/models/design/layout/layout.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignLayout } from './layout.server' + +export interface IDesignLayoutDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignLayout = ({ id }: { id: IDesignLayout['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/layout/layout.get.server.ts b/app/models/design/layout/layout.get.server.ts new file mode 100644 index 00000000..93e702d9 --- /dev/null +++ b/app/models/design/layout/layout.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignLayout } from './layout.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design layout.', + ) + } +} + +export const getDesignLayout = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.LAYOUT, + }, + }) + invariant(design, 'Design Layout Not found') + return deserializeDesign({ design }) as IDesignLayout +} diff --git a/app/models/design/layout/layout.server.ts b/app/models/design/layout/layout.server.ts new file mode 100644 index 00000000..0bd6365e --- /dev/null +++ b/app/models/design/layout/layout.server.ts @@ -0,0 +1,23 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignLayout extends IDesignParsed { + type: typeof DesignTypeEnum.LAYOUT + attributes: IDesignAttributesLayout +} + +export type IDesignLayoutStyle = 'random' | 'grid' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesLayout { + basis?: IDesignLayoutStyle + count?: number + rows?: number + columns?: number +} + +export interface IDesignLayoutSubmission + extends IDesignSubmission, + IDesignAttributesLayout {} diff --git a/app/models/design/layout/layout.update.server.ts b/app/models/design/layout/layout.update.server.ts new file mode 100644 index 00000000..cd127b19 --- /dev/null +++ b/app/models/design/layout/layout.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignLayoutSubmission, + type IDesignAttributesLayout, + type IDesignLayout, +} from './layout.server' + +export interface IDesignLayoutUpdatedResponse { + success: boolean + message?: string + updatedDesignLayout?: IDesign +} + +export interface IDesignLayoutUpdateSubmission extends IDesignLayoutSubmission { + id: IDesignLayout['id'] +} + +export interface IDesignLayoutUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesLayout +} diff --git a/app/models/design/layout/layout.update.style.server.ts b/app/models/design/layout/layout.update.style.server.ts new file mode 100644 index 00000000..6cedd95d --- /dev/null +++ b/app/models/design/layout/layout.update.style.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignLayoutStyleSchema } from '#app/schema/design/layout' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignLayoutStyle, + type IDesignAttributesLayout, + type IDesignLayout, +} from './layout.server' +import { stringifyDesignLayoutAttributes } from './utils' + +export const validateEditStyleDesignLayoutSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignLayoutStyleSchema, + strategy, + }) +} + +export interface IDesignLayoutUpdateStyleSubmission { + userId: IUser['id'] + id: IDesignLayout['id'] + style: IDesignLayoutStyle +} + +interface IDesignLayoutUpdateStyleData { + attributes: IDesignAttributesLayout +} + +export const updateDesignLayoutStyle = ({ + id, + data, +}: { + id: IDesignLayout['id'] + data: IDesignLayoutUpdateStyleData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/layout/utils.ts b/app/models/design/layout/utils.ts new file mode 100644 index 00000000..bbb21eba --- /dev/null +++ b/app/models/design/layout/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesLayoutSchema } from '#app/schema/design/layout' +import { type IDesignAttributesLayout } from './layout.server' + +export const parseDesignLayoutAttributes = ( + attributes: string, +): IDesignAttributesLayout => { + try { + return DesignAttributesLayoutSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignLayoutAttributes = ( + attributes: IDesignAttributesLayout, +): string => { + try { + return JSON.stringify(DesignAttributesLayoutSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 9f064440..8e8704fa 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -2,6 +2,7 @@ import { ZodError } from 'zod' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' +import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -43,6 +44,8 @@ export const validateDesignAttributes = ({ switch (type) { case DesignTypeEnum.FILL: return parseDesignFillAttributes(attributes) + case DesignTypeEnum.LAYOUT: + return parseDesignLayoutAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/layout.ts b/app/schema/design/layout.ts new file mode 100644 index 00000000..42aad80c --- /dev/null +++ b/app/schema/design/layout.ts @@ -0,0 +1,57 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const LayoutStyleTypeEnum = { + RANDOM: 'random', // place count of templates randomly in the container + NONE: 'none', // set rows and columns to place templates in a grid + // add more style types here, like 'spiral', 'circle', etc. ... ok copilot +} as const +export type layoutStyleTypeEnum = ObjectValues + +const LayoutStyleSchema = z.nativeEnum(LayoutStyleTypeEnum) +const LayoutCountSchema = z.number().min(1).max(100_000) +const LayoutGridSchema = z.number().min(1).max(3_000) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesLayoutSchema = z.object({ + style: LayoutStyleSchema.optional(), + count: LayoutCountSchema.optional(), + rows: LayoutGridSchema.optional(), + columns: LayoutGridSchema.optional(), +}) + +export const NewDesignLayoutSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + style: LayoutStyleSchema.default(LayoutStyleTypeEnum.RANDOM), + count: LayoutCountSchema.default(1_000), + rows: LayoutGridSchema.default(9), + columns: LayoutGridSchema.default(9), +}) + +export const EditDesignLayoutStyleSchema = z.object({ + id: z.string(), + style: LayoutStyleSchema, +}) + +export const EditDesignLayoutCountSchema = z.object({ + id: z.string(), + count: LayoutCountSchema, +}) + +export const EditDesignLayoutRowsSchema = z.object({ + id: z.string(), + rows: LayoutGridSchema, +}) + +export const EditDesignLayoutColumnsSchema = z.object({ + id: z.string(), + columns: LayoutGridSchema, +}) + +export const DeleteDesignLayoutSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 0d7e7806..4496015d 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -3,9 +3,12 @@ import { type IDesignWithStroke, type IDesignWithFill, type IDesignWithType, + type IDesignWithLayout, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' +import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.server' +import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum } from '#app/schema/design' @@ -69,6 +72,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { switch (design.type) { case DesignTypeEnum.FILL: return updateDesignFillAttributes(design as IDesignWithFill) + case DesignTypeEnum.LAYOUT: + return updateDesignLayoutAttributes(design as IDesignWithLayout) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -105,6 +110,35 @@ const updateDesignFillAttributes = (design: IDesignWithFill) => { }) } +const updateDesignLayoutAttributes = (design: IDesignWithLayout) => { + const { layout } = design + const { style, count, rows, columns } = layout + const attributes = { + style, + count, + rows, + columns, + } as IDesignAttributesLayout + const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + + return prisma.design + .update({ + where: { id: design.id }, + data: { + attributes: jsonAttributes, + }, + }) + .then(() => { + console.log(`Updated design layout attributes for design ${design.id}`) + }) + .catch(error => { + console.error( + `Failed to update design layout attributes for design ${design.id}`, + error, + ) + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { stroke } = design const { basis, style, value } = stroke From b0600387f51404d422005e5b9455d8bf4bad27c9 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 11:58:22 -0400 Subject: [PATCH 48/54] line, refactor script --- app/models/design/design.server.ts | 8 +- app/models/design/line/line.delete.server.ts | 13 ++ app/models/design/line/line.get.server.ts | 52 ++++++++ app/models/design/line/line.server.ts | 29 ++++ .../design/line/line.update.basis.server.ts | 53 ++++++++ app/models/design/line/line.update.server.ts | 20 +++ app/models/design/line/utils.ts | 39 ++++++ app/models/design/utils.ts | 3 + app/schema/design/line.ts | 59 +++++++++ .../populate-design-attributes-by-type.ts | 125 ++++++++++-------- 10 files changed, 345 insertions(+), 56 deletions(-) create mode 100644 app/models/design/line/line.delete.server.ts create mode 100644 app/models/design/line/line.get.server.ts create mode 100644 app/models/design/line/line.server.ts create mode 100644 app/models/design/line/line.update.basis.server.ts create mode 100644 app/models/design/line/line.update.server.ts create mode 100644 app/models/design/line/utils.ts create mode 100644 app/schema/design/line.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index f6eef028..9c84ab43 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -31,6 +31,10 @@ import { type IDesignLayout, type IDesignAttributesLayout, } from './layout/layout.server' +import { + type IDesignAttributesLine, + type IDesignLine, +} from './line/line.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -58,6 +62,7 @@ export interface IDesign extends BaseDesign { export type IDesignAttributes = | IDesignAttributesFill | IDesignAttributesLayout + | IDesignAttributesLine | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -71,7 +76,8 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] - designLayoutss: IDesignLayout[] + designLayouts: IDesignLayout[] + designLines: IDesignLine[] designStroke: IDesignStroke[] } diff --git a/app/models/design/line/line.delete.server.ts b/app/models/design/line/line.delete.server.ts new file mode 100644 index 00000000..8f89f582 --- /dev/null +++ b/app/models/design/line/line.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignLine } from './line.server' + +export interface IDesignLineDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignLine = ({ id }: { id: IDesignLine['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/line/line.get.server.ts b/app/models/design/line/line.get.server.ts new file mode 100644 index 00000000..fec0b7ec --- /dev/null +++ b/app/models/design/line/line.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignLine } from './line.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design line.', + ) + } +} + +export const getDesignLine = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.LINE, + }, + }) + invariant(design, 'Design Line Not found') + return deserializeDesign({ design }) as IDesignLine +} diff --git a/app/models/design/line/line.server.ts b/app/models/design/line/line.server.ts new file mode 100644 index 00000000..ecaf000f --- /dev/null +++ b/app/models/design/line/line.server.ts @@ -0,0 +1,29 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignLine extends IDesignParsed { + type: typeof DesignTypeEnum.LINE + attributes: IDesignAttributesLine +} + +export type IDesignLineBasis = + | 'size' + | 'width' + | 'height' + | 'canvas-width' + | 'canvas-height' + +export type IDesignLineFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesLine { + basis?: IDesignLineBasis + format?: IDesignLineFormat + width?: number +} + +export interface IDesignLineSubmission + extends IDesignSubmission, + IDesignAttributesLine {} diff --git a/app/models/design/line/line.update.basis.server.ts b/app/models/design/line/line.update.basis.server.ts new file mode 100644 index 00000000..8d4a7776 --- /dev/null +++ b/app/models/design/line/line.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignLineBasisSchema } from '#app/schema/design/line' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignLineBasis, + type IDesignAttributesLine, + type IDesignLine, +} from './line.server' +import { stringifyDesignLineAttributes } from './utils' + +export const validateEditBasisDesignLineSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignLineBasisSchema, + strategy, + }) +} + +export interface IDesignLineUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignLine['id'] + basis: IDesignLineBasis +} + +interface IDesignLineUpdateBasisData { + attributes: IDesignAttributesLine +} + +export const updateDesignLineBasis = ({ + id, + data, +}: { + id: IDesignLine['id'] + data: IDesignLineUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignLineAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/line/line.update.server.ts b/app/models/design/line/line.update.server.ts new file mode 100644 index 00000000..48219c4d --- /dev/null +++ b/app/models/design/line/line.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignLineSubmission, + type IDesignAttributesLine, + type IDesignLine, +} from './line.server' + +export interface IDesignLineUpdatedResponse { + success: boolean + message?: string + updatedDesignLine?: IDesign +} + +export interface IDesignLineUpdateSubmission extends IDesignLineSubmission { + id: IDesignLine['id'] +} + +export interface IDesignLineUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesLine +} diff --git a/app/models/design/line/utils.ts b/app/models/design/line/utils.ts new file mode 100644 index 00000000..ed87539d --- /dev/null +++ b/app/models/design/line/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesLineSchema } from '#app/schema/design/line' +import { type IDesignAttributesLine } from './line.server' + +export const parseDesignLineAttributes = ( + attributes: string, +): IDesignAttributesLine => { + try { + return DesignAttributesLineSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignLineAttributes = ( + attributes: IDesignAttributesLine, +): string => { + try { + return JSON.stringify(DesignAttributesLineSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 8e8704fa..5eba4c67 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -3,6 +3,7 @@ import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' +import { parseDesignLineAttributes } from './line/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -46,6 +47,8 @@ export const validateDesignAttributes = ({ return parseDesignFillAttributes(attributes) case DesignTypeEnum.LAYOUT: return parseDesignLayoutAttributes(attributes) + case DesignTypeEnum.LINE: + return parseDesignLineAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/line.ts b/app/schema/design/line.ts new file mode 100644 index 00000000..79bd027b --- /dev/null +++ b/app/schema/design/line.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const LineFormatTypeEnum = { + PIXEL: 'pixel', // exact pixel value + PERCENT: 'percent', // percent of basis length + // add more format types here +} as const +export const LineBasisTypeEnum = { + SIZE: 'size', + WIDTH: 'width', + HEIGHT: 'height', + CANVAS_WIDTH: 'canvas-width', + CANVAS_HEIGHT: 'canvas-height', + // add more styles here, like gradient, pattern, etc. +} as const +export type lineFormatTypeEnum = ObjectValues +export type lineBasisTypeEnum = ObjectValues + +const LineFormatSchema = z.nativeEnum(LineFormatTypeEnum) +const LineBasisSchema = z.nativeEnum(LineBasisTypeEnum) +const LineWidthSchema = z.number().positive() + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesLineSchema = z.object({ + basis: LineBasisSchema.optional(), + format: LineFormatSchema.optional(), + width: LineWidthSchema.optional(), +}) + +export const NewDesignLineSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: LineBasisSchema.default(LineBasisTypeEnum.SIZE), + format: LineFormatSchema.default(LineFormatTypeEnum.PERCENT), + width: LineWidthSchema.default(3), +}) + +export const EditDesignLineBasisSchema = z.object({ + id: z.string(), + basis: LineBasisSchema, +}) + +export const EditDesignLineFormatSchema = z.object({ + id: z.string(), + format: LineFormatSchema, +}) + +export const EditDesignLineWidthSchema = z.object({ + id: z.string(), + width: LineWidthSchema, +}) + +export const DeleteDesignLineSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 4496015d..335b888e 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -4,14 +4,18 @@ import { type IDesignWithFill, type IDesignWithType, type IDesignWithLayout, + type IDesignWithLine, + type IDesign, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.server' import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' +import { type IDesignAttributesLine } from '#app/models/design/line/line.server' +import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' -import { DesignTypeEnum } from '#app/schema/design' +import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' // designs will have attributes string as json now @@ -74,6 +78,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignFillAttributes(design as IDesignWithFill) case DesignTypeEnum.LAYOUT: return updateDesignLayoutAttributes(design as IDesignWithLayout) + case DesignTypeEnum.LINE: + return updateDesignLineAttributes(design as IDesignWithLine) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -82,89 +88,98 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { } } -const updateDesignFillAttributes = (design: IDesignWithFill) => { - const { fill } = design - const { basis, style, value } = fill - const attributes = { - basis, - style, - value, - } as IDesignAttributesFill - const jsonAttributes = stringifyDesignFillAttributes(attributes) - +const prismaUpdatePromise = ({ + id, + type, + attributes, +}: { + id: IDesign['id'] + type: designTypeEnum + attributes: string +}) => { return prisma.design .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, + where: { id }, + data: { attributes }, }) .then(() => { - console.log(`Updated design fill attributes for design ${design.id}`) + console.log(`Updated design ${type} attributes for design ${id}`) }) .catch(error => { console.error( - `Failed to update design fill attributes for design ${design.id}`, + `Failed to update design ${type} attributes for design ${id}`, error, ) }) } +const updateDesignFillAttributes = (design: IDesignWithFill) => { + const { id, fill } = design + const { basis, style, value } = fill + const json = { + basis, + style, + value, + } as IDesignAttributesFill + const attributes = stringifyDesignFillAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.FILL, + attributes, + }) +} + const updateDesignLayoutAttributes = (design: IDesignWithLayout) => { - const { layout } = design + const { id, layout } = design const { style, count, rows, columns } = layout - const attributes = { + const json = { style, count, rows, columns, } as IDesignAttributesLayout - const jsonAttributes = stringifyDesignLayoutAttributes(attributes) + const attributes = stringifyDesignLayoutAttributes(json) - return prisma.design - .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, - }) - .then(() => { - console.log(`Updated design layout attributes for design ${design.id}`) - }) - .catch(error => { - console.error( - `Failed to update design layout attributes for design ${design.id}`, - error, - ) - }) + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.LAYOUT, + attributes, + }) +} + +const updateDesignLineAttributes = (design: IDesignWithLine) => { + const { id, line } = design + const { basis, format, width } = line + const json = { + basis, + format, + width, + } as IDesignAttributesLine + const attributes = stringifyDesignLineAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.LINE, + attributes, + }) } const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { - const { stroke } = design + const { id, stroke } = design const { basis, style, value } = stroke - const attributes = { + const json = { basis, style, value, } as IDesignAttributesStroke - const jsonAttributes = stringifyDesignStrokeAttributes(attributes) + const attributes = stringifyDesignStrokeAttributes(json) - return prisma.design - .update({ - where: { id: design.id }, - data: { - attributes: jsonAttributes, - }, - }) - .then(() => { - console.log(`Updated design stroke attributes for design ${design.id}`) - }) - .catch(error => { - console.error( - `Failed to update design stroke attributes for design ${design.id}`, - error, - ) - }) + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.STROKE, + attributes, + }) } await populateDesignAttributesByType() From 32ae57ee27cb6ac202d21a9eeb0bbb18a1b8a003 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:16:52 -0400 Subject: [PATCH 49/54] palette --- app/models/design/design.server.ts | 6 +++ .../design/palette/palette.delete.server.ts | 13 +++++ .../design/palette/palette.get.server.ts | 52 +++++++++++++++++++ app/models/design/palette/palette.server.ts | 18 +++++++ .../design/palette/palette.update.server.ts | 21 ++++++++ app/models/design/palette/utils.ts | 39 ++++++++++++++ app/models/design/utils.ts | 3 ++ app/schema/design/palette.ts | 27 ++++++++++ .../populate-design-attributes-by-type.ts | 20 +++++++ 9 files changed, 199 insertions(+) create mode 100644 app/models/design/palette/palette.delete.server.ts create mode 100644 app/models/design/palette/palette.get.server.ts create mode 100644 app/models/design/palette/palette.server.ts create mode 100644 app/models/design/palette/palette.update.server.ts create mode 100644 app/models/design/palette/utils.ts create mode 100644 app/schema/design/palette.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 9c84ab43..e9820eb3 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -35,6 +35,10 @@ import { type IDesignAttributesLine, type IDesignLine, } from './line/line.server' +import { + type IDesignAttributesPalette, + type IDesignPalette, +} from './palette/palette.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -63,6 +67,7 @@ export type IDesignAttributes = | IDesignAttributesFill | IDesignAttributesLayout | IDesignAttributesLine + | IDesignAttributesPalette | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -78,6 +83,7 @@ export type IDesignByType = { designFills: IDesignFill[] designLayouts: IDesignLayout[] designLines: IDesignLine[] + designPalettes: IDesignPalette[] designStroke: IDesignStroke[] } diff --git a/app/models/design/palette/palette.delete.server.ts b/app/models/design/palette/palette.delete.server.ts new file mode 100644 index 00000000..7ddae355 --- /dev/null +++ b/app/models/design/palette/palette.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignPalette } from './palette.server' + +export interface IDesignPaletteDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignPalette = ({ id }: { id: IDesignPalette['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/palette/palette.get.server.ts b/app/models/design/palette/palette.get.server.ts new file mode 100644 index 00000000..9700ac93 --- /dev/null +++ b/app/models/design/palette/palette.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignPalette } from './palette.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design palette.', + ) + } +} + +export const getDesignPalette = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.PALETTE, + }, + }) + invariant(design, 'Design Palette Not found') + return deserializeDesign({ design }) as IDesignPalette +} diff --git a/app/models/design/palette/palette.server.ts b/app/models/design/palette/palette.server.ts new file mode 100644 index 00000000..715dca0b --- /dev/null +++ b/app/models/design/palette/palette.server.ts @@ -0,0 +1,18 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignPalette extends IDesignParsed { + type: typeof DesignTypeEnum.PALETTE + attributes: IDesignAttributesPalette +} + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesPalette { + value?: string +} + +export interface IDesignPaletteSubmission + extends IDesignSubmission, + IDesignAttributesPalette {} diff --git a/app/models/design/palette/palette.update.server.ts b/app/models/design/palette/palette.update.server.ts new file mode 100644 index 00000000..06959333 --- /dev/null +++ b/app/models/design/palette/palette.update.server.ts @@ -0,0 +1,21 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignPaletteSubmission, + type IDesignAttributesPalette, + type IDesignPalette, +} from './palette.server' + +export interface IDesignPaletteUpdatedResponse { + success: boolean + message?: string + updatedDesignPalette?: IDesign +} + +export interface IDesignPaletteUpdateSubmission + extends IDesignPaletteSubmission { + id: IDesignPalette['id'] +} + +export interface IDesignPaletteUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesPalette +} diff --git a/app/models/design/palette/utils.ts b/app/models/design/palette/utils.ts new file mode 100644 index 00000000..5a07e7d0 --- /dev/null +++ b/app/models/design/palette/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesPaletteSchema } from '#app/schema/design/palette' +import { type IDesignAttributesPalette } from './palette.server' + +export const parseDesignPaletteAttributes = ( + attributes: string, +): IDesignAttributesPalette => { + try { + return DesignAttributesPaletteSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignPaletteAttributes = ( + attributes: IDesignAttributesPalette, +): string => { + try { + return JSON.stringify(DesignAttributesPaletteSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 5eba4c67..219cc7e4 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -4,6 +4,7 @@ import { type IDesign, type IDesignParsed } from './design.server' import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' +import { parseDesignPaletteAttributes } from './palette/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -49,6 +50,8 @@ export const validateDesignAttributes = ({ return parseDesignLayoutAttributes(attributes) case DesignTypeEnum.LINE: return parseDesignLineAttributes(attributes) + case DesignTypeEnum.PALETTE: + return parseDesignPaletteAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/palette.ts b/app/schema/design/palette.ts new file mode 100644 index 00000000..08ea6ff4 --- /dev/null +++ b/app/schema/design/palette.ts @@ -0,0 +1,27 @@ +import { z } from 'zod' +import { HexcodeSchema } from '../colors' + +const PaletteValueSchema = HexcodeSchema + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesPaletteSchema = z.object({ + value: PaletteValueSchema.optional(), +}) + +export const NewDesignPaletteSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + value: PaletteValueSchema.default('000000'), +}) + +export const EditDesignPaletteValueSchema = z.object({ + id: z.string(), + width: PaletteValueSchema, +}) + +export const DeleteDesignPaletteSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 335b888e..825c7ed5 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -6,6 +6,7 @@ import { type IDesignWithLayout, type IDesignWithLine, type IDesign, + type IDesignWithPalette, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -13,6 +14,8 @@ import { type IDesignAttributesLayout } from '#app/models/design/layout/layout.s import { stringifyDesignLayoutAttributes } from '#app/models/design/layout/utils' import { type IDesignAttributesLine } from '#app/models/design/line/line.server' import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' +import { type IDesignAttributesPalette } from '#app/models/design/palette/palette.server' +import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -80,6 +83,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignLayoutAttributes(design as IDesignWithLayout) case DesignTypeEnum.LINE: return updateDesignLineAttributes(design as IDesignWithLine) + case DesignTypeEnum.PALETTE: + return updateDesignPaletteAttributes(design as IDesignWithPalette) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -165,6 +170,21 @@ const updateDesignLineAttributes = (design: IDesignWithLine) => { }) } +const updateDesignPaletteAttributes = (design: IDesignWithPalette) => { + const { id, palette } = design + const { value } = palette + const json = { + value, + } as IDesignAttributesPalette + const attributes = stringifyDesignPaletteAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.PALETTE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From 63bf53c5ffff353f64d7604cd4d4362a439f90dd Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:30:47 -0400 Subject: [PATCH 50/54] rotate --- app/models/design/design.server.ts | 6 ++ .../design/rotate/rotate.delete.server.ts | 13 ++++ app/models/design/rotate/rotate.get.server.ts | 52 ++++++++++++++ app/models/design/rotate/rotate.server.ts | 42 ++++++++++++ .../rotate/rotate.update.basis.server.ts | 53 +++++++++++++++ .../design/rotate/rotate.update.server.ts | 20 ++++++ app/models/design/rotate/utils.ts | 39 +++++++++++ app/models/design/utils.ts | 3 + app/schema/design/rotate.ts | 67 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 21 ++++++ 10 files changed, 316 insertions(+) create mode 100644 app/models/design/rotate/rotate.delete.server.ts create mode 100644 app/models/design/rotate/rotate.get.server.ts create mode 100644 app/models/design/rotate/rotate.server.ts create mode 100644 app/models/design/rotate/rotate.update.basis.server.ts create mode 100644 app/models/design/rotate/rotate.update.server.ts create mode 100644 app/models/design/rotate/utils.ts create mode 100644 app/schema/design/rotate.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index e9820eb3..a6d472e5 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -39,6 +39,10 @@ import { type IDesignAttributesPalette, type IDesignPalette, } from './palette/palette.server' +import { + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate/rotate.server' import { type IDesignStroke, type IDesignAttributesStroke, @@ -68,6 +72,7 @@ export type IDesignAttributes = | IDesignAttributesLayout | IDesignAttributesLine | IDesignAttributesPalette + | IDesignAttributesRotate | IDesignAttributesStroke export interface IDesignParsed extends BaseDesign { @@ -84,6 +89,7 @@ export type IDesignByType = { designLayouts: IDesignLayout[] designLines: IDesignLine[] designPalettes: IDesignPalette[] + designRotates: IDesignRotate[] designStroke: IDesignStroke[] } diff --git a/app/models/design/rotate/rotate.delete.server.ts b/app/models/design/rotate/rotate.delete.server.ts new file mode 100644 index 00000000..9ff2458f --- /dev/null +++ b/app/models/design/rotate/rotate.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignRotate } from './rotate.server' + +export interface IDesignRotateDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignRotate = ({ id }: { id: IDesignRotate['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/rotate/rotate.get.server.ts b/app/models/design/rotate/rotate.get.server.ts new file mode 100644 index 00000000..a4190b63 --- /dev/null +++ b/app/models/design/rotate/rotate.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignRotate } from './rotate.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design line.', + ) + } +} + +export const getDesignRotate = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.ROTATE, + }, + }) + invariant(design, 'Design Rotate Not found') + return deserializeDesign({ design }) as IDesignRotate +} diff --git a/app/models/design/rotate/rotate.server.ts b/app/models/design/rotate/rotate.server.ts new file mode 100644 index 00000000..97014b34 --- /dev/null +++ b/app/models/design/rotate/rotate.server.ts @@ -0,0 +1,42 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignRotate extends IDesignParsed { + type: typeof DesignTypeEnum.ROTATE + attributes: IDesignAttributesRotate +} + +export type IDesignRotateArrayBasis = + | 'visible-random' + | 'visible-loop' + | 'visible-loop-reverse' + +export type IDesignRotateIndividualBasis = + | 'defined' + | 'random' + | 'N' + | 'NE' + | 'E' + | 'SE' + | 'S' + | 'SW' + | 'W' + | 'NW' + +export type IDesignRotateBasis = + | IDesignRotateIndividualBasis + | IDesignRotateArrayBasis + +export type IDesignRotateFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesRotate { + basis?: IDesignRotateBasis + value?: number +} + +export interface IDesignRotateSubmission + extends IDesignSubmission, + IDesignAttributesRotate {} diff --git a/app/models/design/rotate/rotate.update.basis.server.ts b/app/models/design/rotate/rotate.update.basis.server.ts new file mode 100644 index 00000000..afd75bf2 --- /dev/null +++ b/app/models/design/rotate/rotate.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignRotateBasisSchema } from '#app/schema/design/rotate' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignRotateBasis, + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate.server' +import { stringifyDesignRotateAttributes } from './utils' + +export const validateEditBasisDesignRotateSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignRotateBasisSchema, + strategy, + }) +} + +export interface IDesignRotateUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignRotate['id'] + basis: IDesignRotateBasis +} + +interface IDesignRotateUpdateBasisData { + attributes: IDesignAttributesRotate +} + +export const updateDesignRotateBasis = ({ + id, + data, +}: { + id: IDesignRotate['id'] + data: IDesignRotateUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignRotateAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/rotate/rotate.update.server.ts b/app/models/design/rotate/rotate.update.server.ts new file mode 100644 index 00000000..febd5da6 --- /dev/null +++ b/app/models/design/rotate/rotate.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignRotateSubmission, + type IDesignAttributesRotate, + type IDesignRotate, +} from './rotate.server' + +export interface IDesignRotateUpdatedResponse { + success: boolean + message?: string + updatedDesignRotate?: IDesign +} + +export interface IDesignRotateUpdateSubmission extends IDesignRotateSubmission { + id: IDesignRotate['id'] +} + +export interface IDesignRotateUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesRotate +} diff --git a/app/models/design/rotate/utils.ts b/app/models/design/rotate/utils.ts new file mode 100644 index 00000000..657f8c28 --- /dev/null +++ b/app/models/design/rotate/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesRotateSchema } from '#app/schema/design/rotate' +import { type IDesignAttributesRotate } from './rotate.server' + +export const parseDesignRotateAttributes = ( + attributes: string, +): IDesignAttributesRotate => { + try { + return DesignAttributesRotateSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignRotateAttributes = ( + attributes: IDesignAttributesRotate, +): string => { + try { + return JSON.stringify(DesignAttributesRotateSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index 219cc7e4..e615038b 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -5,6 +5,7 @@ import { parseDesignFillAttributes } from './fill/utils' import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' import { parseDesignPaletteAttributes } from './palette/utils' +import { parseDesignRotateAttributes } from './rotate/utils' import { parseDesignStrokeAttributes } from './stroke/utils' export const deserializeDesigns = ({ @@ -52,6 +53,8 @@ export const validateDesignAttributes = ({ return parseDesignLineAttributes(attributes) case DesignTypeEnum.PALETTE: return parseDesignPaletteAttributes(attributes) + case DesignTypeEnum.ROTATE: + return parseDesignRotateAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) default: diff --git a/app/schema/design/rotate.ts b/app/schema/design/rotate.ts new file mode 100644 index 00000000..a606e3eb --- /dev/null +++ b/app/schema/design/rotate.ts @@ -0,0 +1,67 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +// use this for determining if build should iterate through rotates array +export const RotateArrayBasisTypeEnum = { + VISIBLE_RANDOM: 'visible-random', + VISIBLE_LOOP: 'visible-loop', + VISIBLE_LOOP_REVERSE: 'visible-loop-reverse', +} as const +export type rotateArrayBasisTypeEnum = ObjectValues< + typeof RotateArrayBasisTypeEnum +> + +export const RotateIndividualBasisTypeEnum = { + DEFINED: 'defined', // exact rotation value + RANDOM: 'random', // random rotation value + N: 'N', // 0 degrees + NE: 'NE', // 45 degrees + E: 'E', // 90 degrees + SE: 'SE', // 135 degrees + S: 'S', // 180 degrees + SW: 'SW', // 225 degrees + W: 'W', // 270 degrees + NW: 'NW', // 315 degrees +} as const +export type rotateIndividualBasisTypeEnum = ObjectValues< + typeof RotateIndividualBasisTypeEnum +> + +export const RotateBasisTypeEnum = { + ...RotateIndividualBasisTypeEnum, + ...RotateArrayBasisTypeEnum, + // add more basis types here +} as const +export type rotateBasisTypeEnum = ObjectValues + +const RotateBasisSchema = z.nativeEnum(RotateBasisTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesRotateSchema = z.object({ + basis: RotateBasisSchema.optional(), + value: z.number().optional(), +}) + +export const NewDesignRotateSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: RotateBasisSchema.default(RotateBasisTypeEnum.DEFINED), + value: z.number().default(0), +}) + +export const EditDesignRotateBasisSchema = z.object({ + id: z.string(), + basis: RotateBasisSchema, +}) + +export const EditDesignRotateValueSchema = z.object({ + id: z.string(), + value: z.number(), +}) + +export const DeleteDesignRotateSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 825c7ed5..e1d8ce51 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -7,6 +7,7 @@ import { type IDesignWithLine, type IDesign, type IDesignWithPalette, + type IDesignWithRotate, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -16,6 +17,8 @@ import { type IDesignAttributesLine } from '#app/models/design/line/line.server' import { stringifyDesignLineAttributes } from '#app/models/design/line/utils' import { type IDesignAttributesPalette } from '#app/models/design/palette/palette.server' import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' +import { type IDesignAttributesRotate } from '#app/models/design/rotate/rotate.server' +import { stringifyDesignRotateAttributes } from '#app/models/design/rotate/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -85,6 +88,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignLineAttributes(design as IDesignWithLine) case DesignTypeEnum.PALETTE: return updateDesignPaletteAttributes(design as IDesignWithPalette) + case DesignTypeEnum.ROTATE: + return updateDesignRotateAttributes(design as IDesignWithRotate) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -185,6 +190,22 @@ const updateDesignPaletteAttributes = (design: IDesignWithPalette) => { }) } +const updateDesignRotateAttributes = (design: IDesignWithRotate) => { + const { id, rotate } = design + const { basis, value } = rotate + const json = { + basis, + value, + } as IDesignAttributesRotate + const attributes = stringifyDesignRotateAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.ROTATE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From c4312da439db70d4f848d5f2c4609d223cfb2bee Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 12:59:24 -0400 Subject: [PATCH 51/54] size --- app/models/design/size/size.delete.server.ts | 13 ++++ app/models/design/size/size.get.server.ts | 52 ++++++++++++++++ app/models/design/size/size.server.ts | 28 +++++++++ .../design/size/size.update.basis.server.ts | 53 +++++++++++++++++ app/models/design/size/size.update.server.ts | 20 +++++++ app/models/design/size/utils.ts | 39 ++++++++++++ app/schema/design/size.ts | 59 +++++++++++++++++++ .../populate-design-attributes-by-type.ts | 22 +++++++ 8 files changed, 286 insertions(+) create mode 100644 app/models/design/size/size.delete.server.ts create mode 100644 app/models/design/size/size.get.server.ts create mode 100644 app/models/design/size/size.server.ts create mode 100644 app/models/design/size/size.update.basis.server.ts create mode 100644 app/models/design/size/size.update.server.ts create mode 100644 app/models/design/size/utils.ts create mode 100644 app/schema/design/size.ts diff --git a/app/models/design/size/size.delete.server.ts b/app/models/design/size/size.delete.server.ts new file mode 100644 index 00000000..c6b0e974 --- /dev/null +++ b/app/models/design/size/size.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignSize } from './size.server' + +export interface IDesignSizeDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignSize = ({ id }: { id: IDesignSize['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/size/size.get.server.ts b/app/models/design/size/size.get.server.ts new file mode 100644 index 00000000..de64b466 --- /dev/null +++ b/app/models/design/size/size.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignSize } from './size.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design size.', + ) + } +} + +export const getDesignSize = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.SIZE, + }, + }) + invariant(design, 'Design Size Not found') + return deserializeDesign({ design }) as IDesignSize +} diff --git a/app/models/design/size/size.server.ts b/app/models/design/size/size.server.ts new file mode 100644 index 00000000..ec73bef0 --- /dev/null +++ b/app/models/design/size/size.server.ts @@ -0,0 +1,28 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignSize extends IDesignParsed { + type: typeof DesignTypeEnum.SIZE + attributes: IDesignAttributesSize +} + +export type IDesignSizeBasis = + | 'width' + | 'height' + | 'canvas-width' + | 'canvas-height' + +export type IDesignSizeFormat = 'pixel' | 'percent' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesSize { + basis?: IDesignSizeBasis + format?: IDesignSizeFormat + value?: number +} + +export interface IDesignSizeSubmission + extends IDesignSubmission, + IDesignAttributesSize {} diff --git a/app/models/design/size/size.update.basis.server.ts b/app/models/design/size/size.update.basis.server.ts new file mode 100644 index 00000000..0decacd1 --- /dev/null +++ b/app/models/design/size/size.update.basis.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignSizeBasisSchema } from '#app/schema/design/size' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignSizeBasis, + type IDesignAttributesSize, + type IDesignSize, +} from './size.server' +import { stringifyDesignSizeAttributes } from './utils' + +export const validateEditBasisDesignSizeSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignSizeBasisSchema, + strategy, + }) +} + +export interface IDesignSizeUpdateBasisSubmission { + userId: IUser['id'] + id: IDesignSize['id'] + basis: IDesignSizeBasis +} + +interface IDesignSizeUpdateBasisData { + attributes: IDesignAttributesSize +} + +export const updateDesignSizeBasis = ({ + id, + data, +}: { + id: IDesignSize['id'] + data: IDesignSizeUpdateBasisData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignSizeAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/size/size.update.server.ts b/app/models/design/size/size.update.server.ts new file mode 100644 index 00000000..a07c4f32 --- /dev/null +++ b/app/models/design/size/size.update.server.ts @@ -0,0 +1,20 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignSizeSubmission, + type IDesignAttributesSize, + type IDesignSize, +} from './size.server' + +export interface IDesignSizeUpdatedResponse { + success: boolean + message?: string + updatedDesignSize?: IDesign +} + +export interface IDesignSizeUpdateSubmission extends IDesignSizeSubmission { + id: IDesignSize['id'] +} + +export interface IDesignSizeUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesSize +} diff --git a/app/models/design/size/utils.ts b/app/models/design/size/utils.ts new file mode 100644 index 00000000..be06aa4d --- /dev/null +++ b/app/models/design/size/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesSizeSchema } from '#app/schema/design/size' +import { type IDesignAttributesSize } from './size.server' + +export const parseDesignSizeAttributes = ( + attributes: string, +): IDesignAttributesSize => { + try { + return DesignAttributesSizeSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignSizeAttributes = ( + attributes: IDesignAttributesSize, +): string => { + try { + return JSON.stringify(DesignAttributesSizeSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/schema/design/size.ts b/app/schema/design/size.ts new file mode 100644 index 00000000..a0a7b068 --- /dev/null +++ b/app/schema/design/size.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const SizeFormatTypeEnum = { + PIXEL: 'pixel', // exact pixel value + PERCENT: 'percent', // percent of basis length + // add more format types here +} as const +export const SizeBasisTypeEnum = { + WIDTH: 'width', + HEIGHT: 'height', + CANVAS_WIDTH: 'canvas-width', + CANVAS_HEIGHT: 'canvas-height', + // add more styles here, like gradient, pattern, etc. +} as const + +export type sizeFormatTypeEnum = ObjectValues +export type sizeBasisTypeEnum = ObjectValues + +const SizeFormatSchema = z.nativeEnum(SizeFormatTypeEnum) +const SizeBasisSchema = z.nativeEnum(SizeBasisTypeEnum) +const SizeValueSchema = z.number().positive() + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesSizeSchema = z.object({ + basis: SizeBasisSchema.optional(), + format: SizeFormatSchema.optional(), + value: SizeValueSchema.optional(), +}) + +export const NewDesignSizeSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + basis: SizeBasisSchema.default(SizeBasisTypeEnum.WIDTH), + format: SizeFormatSchema.default(SizeFormatTypeEnum.PERCENT), + value: SizeValueSchema.default(10), +}) + +export const EditDesignSizeBasisSchema = z.object({ + id: z.string(), + basis: SizeBasisSchema, +}) + +export const EditDesignSizeFormatSchema = z.object({ + id: z.string(), + format: SizeFormatSchema, +}) + +export const EditDesignSizeValueSchema = z.object({ + id: z.string(), + value: SizeValueSchema, +}) + +export const DeleteDesignSizeSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index e1d8ce51..f79f74e6 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -8,6 +8,7 @@ import { type IDesign, type IDesignWithPalette, type IDesignWithRotate, + type IDesignWithSize, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -19,6 +20,8 @@ import { type IDesignAttributesPalette } from '#app/models/design/palette/palett import { stringifyDesignPaletteAttributes } from '#app/models/design/palette/utils' import { type IDesignAttributesRotate } from '#app/models/design/rotate/rotate.server' import { stringifyDesignRotateAttributes } from '#app/models/design/rotate/utils' +import { type IDesignAttributesSize } from '#app/models/design/size/size.server' +import { stringifyDesignSizeAttributes } from '#app/models/design/size/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' @@ -90,6 +93,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignPaletteAttributes(design as IDesignWithPalette) case DesignTypeEnum.ROTATE: return updateDesignRotateAttributes(design as IDesignWithRotate) + case DesignTypeEnum.SIZE: + return updateDesignSizeAttributes(design as IDesignWithSize) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) default: @@ -206,6 +211,23 @@ const updateDesignRotateAttributes = (design: IDesignWithRotate) => { }) } +const updateDesignSizeAttributes = (design: IDesignWithSize) => { + const { id, size } = design + const { basis, format, value } = size + const json = { + basis, + format, + value, + } as IDesignAttributesSize + const attributes = stringifyDesignSizeAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.SIZE, + attributes, + }) +} + const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { const { id, stroke } = design const { basis, style, value } = stroke From 01da27646756ff26c04bc377bca5ee6604b5d822 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 13:10:29 -0400 Subject: [PATCH 52/54] template --- app/models/design/design.server.ts | 12 +++++ .../design/template/template.delete.server.ts | 13 +++++ .../design/template/template.get.server.ts | 52 ++++++++++++++++++ app/models/design/template/template.server.ts | 20 +++++++ .../design/template/template.update.server.ts | 21 ++++++++ .../template/template.update.style.server.ts | 53 +++++++++++++++++++ app/models/design/template/utils.ts | 39 ++++++++++++++ app/models/design/utils.ts | 6 +++ app/schema/design/template.ts | 32 +++++++++++ .../populate-design-attributes-by-type.ts | 20 +++++++ 10 files changed, 268 insertions(+) create mode 100644 app/models/design/template/template.delete.server.ts create mode 100644 app/models/design/template/template.get.server.ts create mode 100644 app/models/design/template/template.server.ts create mode 100644 app/models/design/template/template.update.server.ts create mode 100644 app/models/design/template/template.update.style.server.ts create mode 100644 app/models/design/template/utils.ts create mode 100644 app/schema/design/template.ts diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index a6d472e5..45cb7a9f 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -43,10 +43,18 @@ import { type IDesignAttributesRotate, type IDesignRotate, } from './rotate/rotate.server' +import { + type IDesignAttributesSize, + type IDesignSize, +} from './size/size.server' import { type IDesignStroke, type IDesignAttributesStroke, } from './stroke/stroke.server' +import { + type IDesignAttributesTemplate, + type IDesignTemplate, +} from './template/template.server' // Omitting 'createdAt' and 'updatedAt' from the Design interface // prisma query returns a string for these fields @@ -73,7 +81,9 @@ export type IDesignAttributes = | IDesignAttributesLine | IDesignAttributesPalette | IDesignAttributesRotate + | IDesignAttributesSize | IDesignAttributesStroke + | IDesignAttributesTemplate export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -90,7 +100,9 @@ export type IDesignByType = { designLines: IDesignLine[] designPalettes: IDesignPalette[] designRotates: IDesignRotate[] + designSizes: IDesignSize[] designStroke: IDesignStroke[] + designTemplates: IDesignTemplate[] } // export interface IDesignsByTypeWithType { diff --git a/app/models/design/template/template.delete.server.ts b/app/models/design/template/template.delete.server.ts new file mode 100644 index 00000000..0a12e8a7 --- /dev/null +++ b/app/models/design/template/template.delete.server.ts @@ -0,0 +1,13 @@ +import { prisma } from '#app/utils/db.server' +import { type IDesignTemplate } from './template.server' + +export interface IDesignTemplateDeletedResponse { + success: boolean + message?: string +} + +export const deleteDesignTemplate = ({ id }: { id: IDesignTemplate['id'] }) => { + return prisma.design.delete({ + where: { id }, + }) +} diff --git a/app/models/design/template/template.get.server.ts b/app/models/design/template/template.get.server.ts new file mode 100644 index 00000000..1b60e5d8 --- /dev/null +++ b/app/models/design/template/template.get.server.ts @@ -0,0 +1,52 @@ +import { invariant } from '@epic-web/invariant' +import { z } from 'zod' +import { DesignTypeEnum } from '#app/schema/design' +import { prisma } from '#app/utils/db.server' +import { deserializeDesign } from '../utils' +import { type IDesignTemplate } from './template.server' + +export type queryWhereArgsType = z.infer +const whereArgs = z.object({ + id: z.string().optional(), + ownerId: z.string().optional(), + artworkVersionId: z.string().optional(), + layerId: z.string().optional(), +}) + +// TODO: Add schemas for each type of query and parse with zod +// aka if by id that should be present, if by slug that should be present +// owner id should be present unless admin (not set up yet) +const validateQueryWhereArgsPresent = (where: queryWhereArgsType) => { + const nullValuesAllowed: string[] = [] + const missingValues: Record = {} + for (const [key, value] of Object.entries(where)) { + const valueIsNull = value === null || value === undefined + const nullValueAllowed = nullValuesAllowed.includes(key) + if (valueIsNull && !nullValueAllowed) { + missingValues[key] = value + } + } + + if (Object.keys(missingValues).length > 0) { + console.log('Missing values:', missingValues) + throw new Error( + 'Null or undefined values are not allowed in query parameters for design template.', + ) + } +} + +export const getDesignTemplate = async ({ + where, +}: { + where: queryWhereArgsType +}): Promise => { + validateQueryWhereArgsPresent(where) + const design = await prisma.design.findFirst({ + where: { + ...where, + type: DesignTypeEnum.TEMPLATE, + }, + }) + invariant(design, 'Design Template Not found') + return deserializeDesign({ design }) as IDesignTemplate +} diff --git a/app/models/design/template/template.server.ts b/app/models/design/template/template.server.ts new file mode 100644 index 00000000..6c9f3e12 --- /dev/null +++ b/app/models/design/template/template.server.ts @@ -0,0 +1,20 @@ +import { type DesignTypeEnum } from '#app/schema/design' +import { type IDesignSubmission, type IDesignParsed } from '../design.server' + +export interface IDesignTemplate extends IDesignParsed { + type: typeof DesignTypeEnum.TEMPLATE + attributes: IDesignAttributesTemplate +} + +export type IDesignTemplateStyle = 'triangle' + +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export interface IDesignAttributesTemplate { + style?: IDesignTemplateStyle +} + +export interface IDesignTemplateSubmission + extends IDesignSubmission, + IDesignAttributesTemplate {} diff --git a/app/models/design/template/template.update.server.ts b/app/models/design/template/template.update.server.ts new file mode 100644 index 00000000..7324a0dd --- /dev/null +++ b/app/models/design/template/template.update.server.ts @@ -0,0 +1,21 @@ +import { type IDesign, type IDesignUpdateData } from '../design.server' +import { + type IDesignTemplateSubmission, + type IDesignAttributesTemplate, + type IDesignTemplate, +} from './template.server' + +export interface IDesignTemplateUpdatedResponse { + success: boolean + message?: string + updatedDesignTemplate?: IDesign +} + +export interface IDesignTemplateUpdateSubmission + extends IDesignTemplateSubmission { + id: IDesignTemplate['id'] +} + +export interface IDesignTemplateUpdateData extends IDesignUpdateData { + attributes: IDesignAttributesTemplate +} diff --git a/app/models/design/template/template.update.style.server.ts b/app/models/design/template/template.update.style.server.ts new file mode 100644 index 00000000..6a96ad53 --- /dev/null +++ b/app/models/design/template/template.update.style.server.ts @@ -0,0 +1,53 @@ +import { type IntentActionArgs } from '#app/definitions/intent-action-args' +import { type IUser } from '#app/models/user/user.server' +import { EditDesignTemplateStyleSchema } from '#app/schema/design/template' +import { ValidateDesignSubmissionStrategy } from '#app/strategies/validate-submission.strategy' +import { validateEntitySubmission } from '#app/utils/conform-utils' +import { prisma } from '#app/utils/db.server' +import { + type IDesignAttributesTemplate, + type IDesignTemplate, + type IDesignTemplateStyle, +} from './template.server' +import { stringifyDesignTemplateAttributes } from './utils' + +export const validateEditStyleDesignTemplateSubmission = async ({ + userId, + formData, +}: IntentActionArgs) => { + const strategy = new ValidateDesignSubmissionStrategy() + + return await validateEntitySubmission({ + userId, + formData, + schema: EditDesignTemplateStyleSchema, + strategy, + }) +} + +export interface IDesignTemplateUpdateStyleSubmission { + userId: IUser['id'] + id: IDesignTemplate['id'] + style: IDesignTemplateStyle +} + +interface IDesignTemplateUpdateStyleData { + attributes: IDesignAttributesTemplate +} + +export const updateDesignTemplateStyle = ({ + id, + data, +}: { + id: IDesignTemplate['id'] + data: IDesignTemplateUpdateStyleData +}) => { + const { attributes } = data + const jsonAttributes = stringifyDesignTemplateAttributes(attributes) + return prisma.design.update({ + where: { id }, + data: { + attributes: jsonAttributes, + }, + }) +} diff --git a/app/models/design/template/utils.ts b/app/models/design/template/utils.ts new file mode 100644 index 00000000..3fa5be17 --- /dev/null +++ b/app/models/design/template/utils.ts @@ -0,0 +1,39 @@ +import { ZodError } from 'zod' +import { DesignAttributesTemplateSchema } from '#app/schema/design/template' +import { type IDesignAttributesTemplate } from './template.server' + +export const parseDesignTemplateAttributes = ( + attributes: string, +): IDesignAttributesTemplate => { + try { + return DesignAttributesTemplateSchema.parse(JSON.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} + +export const stringifyDesignTemplateAttributes = ( + attributes: IDesignAttributesTemplate, +): string => { + try { + return JSON.stringify(DesignAttributesTemplateSchema.parse(attributes)) + } catch (error: any) { + if (error instanceof ZodError) { + throw new Error( + `Validation failed for asset image: ${error.errors.map(e => e.message).join(', ')}`, + ) + } else { + throw new Error( + `Unexpected error during validation for asset image: ${error.message}`, + ) + } + } +} diff --git a/app/models/design/utils.ts b/app/models/design/utils.ts index e615038b..8d2b7bed 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -6,7 +6,9 @@ import { parseDesignLayoutAttributes } from './layout/utils' import { parseDesignLineAttributes } from './line/utils' import { parseDesignPaletteAttributes } from './palette/utils' import { parseDesignRotateAttributes } from './rotate/utils' +import { parseDesignSizeAttributes } from './size/utils' import { parseDesignStrokeAttributes } from './stroke/utils' +import { parseDesignTemplateAttributes } from './template/utils' export const deserializeDesigns = ({ designs, @@ -55,8 +57,12 @@ export const validateDesignAttributes = ({ return parseDesignPaletteAttributes(attributes) case DesignTypeEnum.ROTATE: return parseDesignRotateAttributes(attributes) + case DesignTypeEnum.SIZE: + return parseDesignSizeAttributes(attributes) case DesignTypeEnum.STROKE: return parseDesignStrokeAttributes(attributes) + case DesignTypeEnum.TEMPLATE: + return parseDesignTemplateAttributes(attributes) default: throw new Error(`Unsupported design type: ${type}`) } diff --git a/app/schema/design/template.ts b/app/schema/design/template.ts new file mode 100644 index 00000000..8e65bca9 --- /dev/null +++ b/app/schema/design/template.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import { type ObjectValues } from '#app/utils/typescript-helpers' + +export const TemplateStyleTypeEnum = { + TRIANGLE: 'triangle', // my only shape so far + // add more styles here +} as const +export type templateStyleTypeEnum = ObjectValues +const TemplateStyleSchema = z.nativeEnum(TemplateStyleTypeEnum) + +// use this to (de)serealize data to/from the db +// when adding attributes to an design type, +// make sure it starts as optional or is set to a default value +// for when parsing the design from the deserializer +export const DesignAttributesTemplateSchema = z.object({ + style: TemplateStyleSchema.optional(), +}) + +export const NewDesignTemplateSchema = z.object({ + visible: z.boolean(), + selected: z.boolean(), + style: TemplateStyleSchema.default(TemplateStyleTypeEnum.TRIANGLE), +}) + +export const EditDesignTemplateStyleSchema = z.object({ + id: z.string(), + style: TemplateStyleSchema, +}) + +export const DeleteDesignTemplateSchema = z.object({ + id: z.string(), +}) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index f79f74e6..640901fe 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -9,6 +9,7 @@ import { type IDesignWithPalette, type IDesignWithRotate, type IDesignWithSize, + type IDesignWithTemplate, } from '#app/models/design/design.server' import { type IDesignAttributesFill } from '#app/models/design/fill/fill.server' import { stringifyDesignFillAttributes } from '#app/models/design/fill/utils' @@ -24,6 +25,8 @@ import { type IDesignAttributesSize } from '#app/models/design/size/size.server' import { stringifyDesignSizeAttributes } from '#app/models/design/size/utils' import { type IDesignAttributesStroke } from '#app/models/design/stroke/stroke.server' import { stringifyDesignStrokeAttributes } from '#app/models/design/stroke/utils' +import { type IDesignAttributesTemplate } from '#app/models/design/template/template.server' +import { stringifyDesignTemplateAttributes } from '#app/models/design/template/utils' import { DesignTypeEnum, type designTypeEnum } from '#app/schema/design' import { prisma } from '#app/utils/db.server' @@ -97,6 +100,8 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { return updateDesignSizeAttributes(design as IDesignWithSize) case DesignTypeEnum.STROKE: return updateDesignStrokeAttributes(design as IDesignWithStroke) + case DesignTypeEnum.TEMPLATE: + return updateDesignTemplateAttributes(design as IDesignWithTemplate) default: return Promise.resolve() // throw new Error(`Unsupported design type: ${design.type}`) @@ -245,4 +250,19 @@ const updateDesignStrokeAttributes = (design: IDesignWithStroke) => { }) } +const updateDesignTemplateAttributes = (design: IDesignWithTemplate) => { + const { id, template } = design + const { style } = template + const json = { + style, + } as IDesignAttributesTemplate + const attributes = stringifyDesignTemplateAttributes(json) + + return prismaUpdatePromise({ + id, + type: DesignTypeEnum.TEMPLATE, + attributes, + }) +} + await populateDesignAttributesByType() From 26b16e1b9095b2e35a4b122b260b232f344b3cfc Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 13:12:03 -0400 Subject: [PATCH 53/54] do throw if a new design type --- prisma/data-migrations/populate-design-attributes-by-type.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 640901fe..40e8b09d 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -103,8 +103,7 @@ const updateDesignAttributesPromise = (design: IDesignWithType) => { case DesignTypeEnum.TEMPLATE: return updateDesignTemplateAttributes(design as IDesignWithTemplate) default: - return Promise.resolve() - // throw new Error(`Unsupported design type: ${design.type}`) + throw new Error(`Unsupported design type: ${design.type}`) } } From 77178a6a8ba5e3d7763eb01f7b53ecf4402eded5 Mon Sep 17 00:00:00 2001 From: Pat Needham Date: Mon, 17 Jun 2024 14:04:46 -0400 Subject: [PATCH 54/54] bugfix --- app/schema/design/layout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/schema/design/layout.ts b/app/schema/design/layout.ts index 42aad80c..9e35fcf8 100644 --- a/app/schema/design/layout.ts +++ b/app/schema/design/layout.ts @@ -3,7 +3,7 @@ import { type ObjectValues } from '#app/utils/typescript-helpers' export const LayoutStyleTypeEnum = { RANDOM: 'random', // place count of templates randomly in the container - NONE: 'none', // set rows and columns to place templates in a grid + GRID: 'grid', // set rows and columns to place templates in a grid // add more style types here, like 'spiral', 'circle', etc. ... ok copilot } as const export type layoutStyleTypeEnum = ObjectValues