diff --git a/app/models/design/design.server.ts b/app/models/design/design.server.ts index 58f8de65..45cb7a9f 100644 --- a/app/models/design/design.server.ts +++ b/app/models/design/design.server.ts @@ -27,10 +27,34 @@ import { type IDesignAttributesFill, type IDesignFill, } from './fill/fill.server' +import { + type IDesignLayout, + type IDesignAttributesLayout, +} from './layout/layout.server' +import { + type IDesignAttributesLine, + type IDesignLine, +} from './line/line.server' +import { + type IDesignAttributesPalette, + type IDesignPalette, +} from './palette/palette.server' +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 @@ -51,7 +75,15 @@ 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 + | IDesignAttributesLine + | IDesignAttributesPalette + | IDesignAttributesRotate + | IDesignAttributesSize + | IDesignAttributesStroke + | IDesignAttributesTemplate export interface IDesignParsed extends BaseDesign { type: designTypeEnum @@ -64,7 +96,13 @@ export interface IDesignParsed extends BaseDesign { // TODO: replace with this ^^ export type IDesignByType = { designFills: IDesignFill[] + designLayouts: IDesignLayout[] + designLines: IDesignLine[] + designPalettes: IDesignPalette[] + designRotates: IDesignRotate[] + designSizes: IDesignSize[] designStroke: IDesignStroke[] + designTemplates: IDesignTemplate[] } // export interface IDesignsByTypeWithType { 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/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/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/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/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/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 9f064440..8d2b7bed 100644 --- a/app/models/design/utils.ts +++ b/app/models/design/utils.ts @@ -2,7 +2,13 @@ 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 { 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, @@ -43,8 +49,20 @@ export const validateDesignAttributes = ({ switch (type) { case DesignTypeEnum.FILL: return parseDesignFillAttributes(attributes) + case DesignTypeEnum.LAYOUT: + return parseDesignLayoutAttributes(attributes) + case DesignTypeEnum.LINE: + return parseDesignLineAttributes(attributes) + case DesignTypeEnum.PALETTE: + 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/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/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/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/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/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/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/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": [ diff --git a/prisma/data-migrations/populate-design-attributes-by-type.ts b/prisma/data-migrations/populate-design-attributes-by-type.ts index 0d7e7806..640901fe 100644 --- a/prisma/data-migrations/populate-design-attributes-by-type.ts +++ b/prisma/data-migrations/populate-design-attributes-by-type.ts @@ -3,12 +3,31 @@ import { type IDesignWithStroke, type IDesignWithFill, type IDesignWithType, + type IDesignWithLayout, + type IDesignWithLine, + type IDesign, + 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' +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 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 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 } from '#app/schema/design' +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' // designs will have attributes string as json now @@ -69,68 +88,181 @@ 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.LINE: + return updateDesignLineAttributes(design as IDesignWithLine) + case DesignTypeEnum.PALETTE: + 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) + case DesignTypeEnum.TEMPLATE: + return updateDesignTemplateAttributes(design as IDesignWithTemplate) 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) - +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 { id, layout } = design + const { style, count, rows, columns } = layout + const json = { + style, + count, + rows, + columns, + } as IDesignAttributesLayout + const attributes = stringifyDesignLayoutAttributes(json) + + 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 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 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 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 { 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, + }) +} + +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()