type Props = {
src: string
- type: string
+ type?: string
}
const props = defineProps()
diff --git a/domains/grid/composables/useForm.ts b/domains/grid/composables/useForm.ts
new file mode 100644
index 00000000..8351b66e
--- /dev/null
+++ b/domains/grid/composables/useForm.ts
@@ -0,0 +1,57 @@
+import { computedAsync } from '@vueuse/core'
+import { ZodEffects, ZodError, type ZodObject } from 'zod'
+
+export const useForm = (
+ schema?: ZodObject | ZodEffects>,
+ initialValues: Record = {}
+) => {
+ const inputValues = ref(initialValues)
+ const inputErrors = ref | undefined>()
+
+ const canSubmit = computedAsync(async () => {
+ try {
+ await schema?.parseAsync(inputValues.value)
+ return true
+ } catch {
+ return false
+ }
+ }, false)
+
+ const getFieldErrorMessage = (field: string) => {
+ return inputErrors.value?.[field]?._errors[0]
+ }
+
+ const handleFieldChange = (customEvent: CustomEvent, field: string) => {
+ const value = customEvent.detail?.value
+
+ inputValues.value[field] = value
+ }
+
+ const handleFormErrors = (error: unknown) => {
+ if (error instanceof ZodError) {
+ inputErrors.value = error?.format()
+ }
+ }
+
+ watch(
+ [inputValues],
+ async () => {
+ try {
+ inputErrors.value = undefined
+ await schema?.parseAsync(inputValues.value)
+ } catch (error: unknown) {
+ handleFormErrors(error)
+ }
+ },
+ { deep: true }
+ )
+
+ return {
+ inputValues,
+ inputErrors,
+ canSubmit,
+ getFieldErrorMessage,
+ handleFieldChange,
+ handleFormErrors,
+ }
+}
diff --git a/domains/grid/composables/useGrid.ts b/domains/grid/composables/useGrid.ts
index 356a8f32..1b203995 100644
--- a/domains/grid/composables/useGrid.ts
+++ b/domains/grid/composables/useGrid.ts
@@ -1,5 +1,3 @@
-import type { GridWidget } from '@/types/grid'
-
export const useGrid = () => {
const {
isConnected,
@@ -23,16 +21,14 @@ export const useGrid = () => {
isConnectedUserViewingOwnProfile.value
)
- const getGridById = (grids: Grid[], id?: string) =>
- grids.find(grid => grid.id === id)
+ const getGridById = (grid: Grid[], id?: string) =>
+ grid.find(item => item.id === id)
- const getSelectedGridWidgets = (grids: Grid[]): GridWidget[] =>
- getGridById(grids, selectedGridId.value)?.grid || []
+ const getSelectedGridWidgets = (grid: Grid[]): GridWidget[] =>
+ getGridById(grid, selectedGridId.value)?.grid || []
- const updateSelectedGrid = (
- gridWidgets: GridWidget[]
- ): Grid[] => {
- const updatedGrids = tempGrid.value.map(item => {
+ const updateSelectedGrid = (gridWidgets: GridWidget[]): Grid[] => {
+ const updatedGrid = tempGrid.value.map(item => {
if (item.id === selectedGridId.value) {
return {
id: item.id,
@@ -45,7 +41,7 @@ export const useGrid = () => {
return item
})
- return updatedGrids
+ return updatedGrid
}
const initSelectedGridId = () => {
@@ -77,7 +73,7 @@ export const useGrid = () => {
address?: Address,
withAddContentPlaceholder?: boolean
) => {
- let grid: Grid[] = []
+ let grid: Grid[] = []
if (!address) {
return []
@@ -88,6 +84,17 @@ export const useGrid = () => {
const userGrid = await getUserGrid(address)
grid = buildGrid(userGrid, isMobile.value, withAddContentPlaceholder)
+ if (grid.length === 0) {
+ grid = [
+ {
+ id: 'main',
+ title: 'Main',
+ grid: [],
+ gridColumns: GRID_COLUMNS_MIN,
+ },
+ ]
+ }
+
if (gridLog.enabled) {
gridLog('Initialize user grid', userGrid)
}
@@ -125,10 +132,7 @@ export const useGrid = () => {
initSelectedGridId()
},
- addGridWidget: (
- widget: GridWidgetWithoutCords,
- grid?: Grid
- ) => {
+ addGridWidget: (widget: GridWidgetWithoutCords, grid?: Grid) => {
if (!canEditGrid.value) {
console.warn('User cannot edit grid')
return
@@ -139,6 +143,10 @@ export const useGrid = () => {
return
}
+ if (gridLog.enabled) {
+ gridLog('Add grid widget', widget)
+ }
+
placeWidgetInGrid(widget, grid.grid, grid.gridColumns)
},
@@ -165,6 +173,11 @@ export const useGrid = () => {
...gridWidgets[widgetIndex],
...widget,
}
+
+ if (gridLog.enabled) {
+ gridLog('Update grid widget', gridWidgets[widgetIndex])
+ }
+
tempGrid.value = updateSelectedGrid(gridWidgets)
},
@@ -178,12 +191,16 @@ export const useGrid = () => {
return
}
+ if (gridLog.enabled) {
+ gridLog('Remove grid widget', id)
+ }
+
tempGrid.value = updateSelectedGrid(
getSelectedGridWidgets(tempGrid.value).filter(item => item.i !== id)
)
},
- saveGrid: async (grid?: Grid[]) => {
+ saveGrid: async (grid?: Grid[]) => {
if (!canEditGrid.value) {
console.warn('User cannot edit grid')
return
@@ -194,9 +211,10 @@ export const useGrid = () => {
}
const config = gridToConfig(grid)
+ const validation = await gridConfigSchema.array().safeParseAsync(config)
- if (!isConfigValid(config)) {
- console.warn('Invalid schema')
+ if (!validation.success) {
+ console.warn('Invalid schema', validation.error)
return
}
@@ -236,7 +254,7 @@ export const useGrid = () => {
}
},
- addGrid: (grid: Grid) => {
+ addGrid: (grid: Grid) => {
if (!canEditGrid.value) {
console.warn('User cannot edit grid')
return
@@ -245,10 +263,7 @@ export const useGrid = () => {
tempGrid.value.push(grid)
},
- updateGrid: (
- id?: string,
- grid?: PartialBy, 'id' | 'grid'>
- ) => {
+ updateGrid: (id?: string, grid?: Partial) => {
if (!canEditGrid.value) {
console.warn('User cannot edit grid')
return
diff --git a/domains/grid/shared/config.ts b/domains/grid/shared/config.ts
index 961aae53..b15f7b18 100644
--- a/domains/grid/shared/config.ts
+++ b/domains/grid/shared/config.ts
@@ -1,71 +1,40 @@
-export enum GRID_WIDGET_TYPE {
+import { type ZodEffects, type ZodObject, z } from 'zod'
+
+export const GRID_WIDGET_TYPE = z.enum([
// custom
- TITLE_LINK = 'TITLE_LINK',
- TEXT = 'TEXT',
- IFRAME = 'IFRAME',
- IMAGE = 'IMAGE',
+ 'TEXT',
+ 'IFRAME',
+ 'IMAGE',
// social media
- X = 'X',
- INSTAGRAM = 'INSTAGRAM',
- WARPCAST = 'WARPCAST',
+ 'X',
+ 'INSTAGRAM',
+ 'WARPCAST',
// music
- SPOTIFY = 'SPOTIFY',
- SOUNDCLOUD = 'SOUNDCLOUD',
+ 'SPOTIFY',
+ 'SOUNDCLOUD',
// video
- YOUTUBE = 'YOUTUBE',
+ 'YOUTUBE',
// static widgets for visual purposes
- ADD_CONTENT = 'ADD_CONTENT',
-}
+ 'ADD_CONTENT',
+])
-export const WIDGET_TYPE_PROPERTIES: Record<
- GridWidgetType,
- GridWidgetProperty[]
+// map zod schema to widget type
+export const WIDGET_SCHEMA_MAP: Partial<
+ Record | ZodEffects>>
> = {
- [GRID_WIDGET_TYPE.TITLE_LINK]: [
- { key: 'title', type: 'string' },
- { key: 'src', type: 'url', optional: true },
- { key: 'textColor', type: 'color', optional: true },
- { key: 'backgroundColor', type: 'color', optional: true },
- ],
- [GRID_WIDGET_TYPE.TEXT]: [
- { key: 'title', type: 'string' },
- { key: 'text', type: 'string' },
- { key: 'titleColor', type: 'color' },
- { key: 'textColor', type: 'color' },
- { key: 'backgroundColor', type: 'color', optional: true },
- ],
- [GRID_WIDGET_TYPE.X]: [
- { key: 'src', type: 'url' },
- { key: 'type', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.INSTAGRAM]: [
- { key: 'src', type: 'url' },
- { key: 'type', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.IFRAME]: [
- { key: 'src', type: 'url' },
- { key: 'allow', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.IMAGE]: [{ key: 'src', type: 'url' }],
- [GRID_WIDGET_TYPE.ADD_CONTENT]: [],
- [GRID_WIDGET_TYPE.YOUTUBE]: [
- { key: 'src', type: 'url' },
- { key: 'allow', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.SPOTIFY]: [
- { key: 'src', type: 'url' },
- { key: 'allow', type: 'string' },
- { key: 'type', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.SOUNDCLOUD]: [
- { key: 'src', type: 'url' },
- { key: 'allow', type: 'string' },
- ],
- [GRID_WIDGET_TYPE.WARPCAST]: [{ key: 'src', type: 'url' }],
+ [GRID_WIDGET_TYPE.enum.TEXT]: textWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.X]: xWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.INSTAGRAM]: instagramWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.IFRAME]: iframeWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.IMAGE]: imageWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.YOUTUBE]: youtubeWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.SPOTIFY]: spotifyWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.SOUNDCLOUD]: soundCloudWidgetSchema,
+ [GRID_WIDGET_TYPE.enum.WARPCAST]: warpcastWidgetSchema,
}
// grid breakpoint where the grid switches into mobile mode
diff --git a/domains/grid/utils/__tests__/buildGrid.spec.ts b/domains/grid/utils/__tests__/buildGrid.spec.ts
index 901fc3f6..a7be65cb 100644
--- a/domains/grid/utils/__tests__/buildGrid.spec.ts
+++ b/domains/grid/utils/__tests__/buildGrid.spec.ts
@@ -4,20 +4,20 @@ import { buildGrid } from '../buildGrid'
describe('buildGrid', () => {
it('should return an empty grid when given an empty grid', () => {
- const grid: Grid[] = []
+ const grid: Grid[] = []
const result = buildGrid(grid)
expect(result).toEqual([])
})
it('should re-order grid based on x/y cords', () => {
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -26,7 +26,7 @@ describe('buildGrid', () => {
},
{
i: '2',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -35,7 +35,7 @@ describe('buildGrid', () => {
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -54,7 +54,7 @@ describe('buildGrid', () => {
grid: [
{
i: '2',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -63,7 +63,7 @@ describe('buildGrid', () => {
},
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -72,7 +72,7 @@ describe('buildGrid', () => {
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
properties: {},
@@ -87,14 +87,14 @@ describe('buildGrid', () => {
describe('withAddContentPlaceholder', () => {
it('should remove "add content" placeholder from grid', () => {
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
x: 0,
@@ -103,7 +103,7 @@ describe('buildGrid', () => {
},
{
i: '2',
- type: GRID_WIDGET_TYPE.ADD_CONTENT,
+ type: GRID_WIDGET_TYPE.enum.ADD_CONTENT,
w: 1,
h: 1,
x: 0,
@@ -122,7 +122,7 @@ describe('buildGrid', () => {
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
x: 0,
@@ -136,14 +136,14 @@ describe('buildGrid', () => {
})
it('should add placeholder', () => {
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
x: 0,
@@ -162,7 +162,7 @@ describe('buildGrid', () => {
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 1,
x: 0,
@@ -171,7 +171,7 @@ describe('buildGrid', () => {
},
{
i: 'placeholder',
- type: GRID_WIDGET_TYPE.ADD_CONTENT,
+ type: GRID_WIDGET_TYPE.enum.ADD_CONTENT,
w: 1,
h: 1,
x: 0,
@@ -198,37 +198,45 @@ describe('buildGrid', () => {
| |
| D | <-- height 1
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '4',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -247,7 +255,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -256,7 +264,7 @@ describe('buildGrid', () => {
y: 2,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -265,7 +273,7 @@ describe('buildGrid', () => {
y: 3,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -274,7 +282,7 @@ describe('buildGrid', () => {
y: 5,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -292,37 +300,45 @@ describe('buildGrid', () => {
|-------| | C | <-- C: height 2
| D | | | <-- D: height 1
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '4',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -341,7 +357,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -350,7 +366,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -359,7 +375,7 @@ describe('buildGrid', () => {
y: 1,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -368,7 +384,7 @@ describe('buildGrid', () => {
y: 2,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -385,37 +401,45 @@ describe('buildGrid', () => {
| | |-------| | |
|-------| | D | |-------|
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 1,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '4',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -434,7 +458,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.TITLE_LINK,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -443,7 +467,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -452,7 +476,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -461,7 +485,7 @@ describe('buildGrid', () => {
y: 1,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -483,30 +507,36 @@ describe('buildGrid', () => {
|-------|
| C | <-- height 1
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 2,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -525,7 +555,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
originalWidth: 2,
},
@@ -535,7 +565,7 @@ describe('buildGrid', () => {
y: 2,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -544,7 +574,7 @@ describe('buildGrid', () => {
y: 3,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -561,30 +591,36 @@ describe('buildGrid', () => {
|-------| |-------|
| B | | C | <-- height 1 each
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 2,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -603,7 +639,7 @@ describe('buildGrid', () => {
y: 0,
w: 2,
h: 2,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -612,7 +648,7 @@ describe('buildGrid', () => {
y: 2,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -621,7 +657,7 @@ describe('buildGrid', () => {
y: 2,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -645,37 +681,45 @@ describe('buildGrid', () => {
|-------|
| D | <-- height 1
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 2,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '4',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 2,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -694,7 +738,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -703,7 +747,7 @@ describe('buildGrid', () => {
y: 1,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: { prop1: 'value1', prop2: 'value2' },
originalWidth: 2,
},
@@ -713,7 +757,7 @@ describe('buildGrid', () => {
y: 3,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -722,7 +766,7 @@ describe('buildGrid', () => {
y: 4,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
originalWidth: 2,
},
@@ -744,37 +788,45 @@ describe('buildGrid', () => {
|-------|-------------|
| D | D | <-- height 1 (width spans 2 columns)
*/
- const grid: Grid[] = [
+ const grid: Grid[] = [
{
id: '1',
title: 'Test Grid',
grid: [
{
i: '1',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '2',
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 2,
h: 2,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '3',
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
w: 1,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
i: '4',
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
w: 2,
h: 1,
+ x: 0,
+ y: 0,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
@@ -793,7 +845,7 @@ describe('buildGrid', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -802,7 +854,7 @@ describe('buildGrid', () => {
y: 1,
w: 2,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -811,7 +863,7 @@ describe('buildGrid', () => {
y: 3,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: { prop1: 'value1', prop2: 'value2' },
},
{
@@ -820,7 +872,7 @@ describe('buildGrid', () => {
y: 4,
w: 2,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: { prop1: 'value1', prop2: 'value2' },
},
],
diff --git a/domains/grid/utils/__tests__/compareGridWidgets.spec.ts b/domains/grid/utils/__tests__/compareGridWidgets.spec.ts
index db699371..69cd8d9f 100644
--- a/domains/grid/utils/__tests__/compareGridWidgets.spec.ts
+++ b/domains/grid/utils/__tests__/compareGridWidgets.spec.ts
@@ -10,7 +10,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -23,7 +23,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -42,7 +42,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -55,7 +55,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -78,7 +78,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -91,7 +91,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 2,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -114,7 +114,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -127,7 +127,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://example.com',
},
@@ -150,7 +150,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -163,7 +163,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.org',
},
@@ -187,7 +187,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -210,7 +210,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -234,7 +234,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -245,7 +245,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://example.org/image.png',
},
@@ -256,7 +256,7 @@ describe('compareGridWidgets', () => {
y: 1,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://example.org/image.png',
},
@@ -269,7 +269,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://example.org',
},
@@ -280,7 +280,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -291,7 +291,7 @@ describe('compareGridWidgets', () => {
y: 1,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://example.org/image.png',
},
@@ -318,7 +318,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
@@ -329,7 +329,7 @@ describe('compareGridWidgets', () => {
y: 1,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.ADD_CONTENT,
+ type: GRID_WIDGET_TYPE.enum.ADD_CONTENT,
properties: {},
},
]
@@ -340,7 +340,7 @@ describe('compareGridWidgets', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://example.com',
},
diff --git a/domains/grid/utils/__tests__/compareGrids.spec.ts b/domains/grid/utils/__tests__/compareGrids.spec.ts
index 92e96785..5f94d49a 100644
--- a/domains/grid/utils/__tests__/compareGrids.spec.ts
+++ b/domains/grid/utils/__tests__/compareGrids.spec.ts
@@ -3,7 +3,7 @@ import { compareGrids } from '../compareGrids'
describe('compareGrids', () => {
it('should return no changes for identical grids', () => {
- const gridA: Grid[] = [
+ const gridA: Grid[] = [
{
id: 'grid-1',
title: 'Grid 1',
@@ -14,14 +14,14 @@ describe('compareGrids', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {},
},
],
gridColumns: 2,
},
]
- const gridB: Grid[] = [
+ const gridB: Grid[] = [
{
id: 'grid-1',
title: 'Grid 1',
@@ -32,7 +32,7 @@ describe('compareGrids', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {},
},
],
@@ -45,10 +45,10 @@ describe('compareGrids', () => {
})
it('should return changes for grids with different titles', () => {
- const gridA: Grid[] = [
+ const gridA: Grid[] = [
{ id: 'grid-1', title: 'Grid 1', grid: [], gridColumns: 2 },
]
- const gridB: Grid[] = [
+ const gridB: Grid[] = [
{ id: 'grid-2', title: 'Grid 2', grid: [], gridColumns: 2 },
]
@@ -62,10 +62,10 @@ describe('compareGrids', () => {
})
it('should return changes for grids with different column numbers', () => {
- const gridA: Grid[] = [
+ const gridA: Grid[] = [
{ id: 'grid-1', title: 'Grid 1', grid: [], gridColumns: 2 },
]
- const gridB: Grid[] = [
+ const gridB: Grid[] = [
{ id: 'grid-1', title: 'Grid 1', grid: [], gridColumns: 3 },
]
@@ -79,7 +79,7 @@ describe('compareGrids', () => {
})
it('should return changes for grids with different widgets', () => {
- const gridA: Grid[] = [
+ const gridA: Grid[] = [
{
id: 'grid-1',
title: 'Grid 1',
@@ -90,14 +90,14 @@ describe('compareGrids', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {},
},
],
gridColumns: 2,
},
]
- const gridB: Grid[] = [
+ const gridB: Grid[] = [
{
id: 'grid-3',
title: 'Grid 1',
@@ -108,7 +108,7 @@ describe('compareGrids', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {},
},
],
@@ -126,8 +126,8 @@ describe('compareGrids', () => {
})
it('should return changes when one grid is empty and the other is not', () => {
- const gridA: Grid[] = []
- const gridB: Grid[] = [
+ const gridA: Grid[] = []
+ const gridB: Grid[] = [
{
id: 'grid-1',
title: 'Grid 1',
@@ -138,7 +138,7 @@ describe('compareGrids', () => {
y: 0,
w: 1,
h: 1,
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {},
},
],
@@ -156,8 +156,8 @@ describe('compareGrids', () => {
})
it('should return no changes for two empty grids', () => {
- const gridA: Grid[] = []
- const gridB: Grid[] = []
+ const gridA: Grid[] = []
+ const gridB: Grid[] = []
const result = compareGrids(gridA, gridB)
expect(result).toEqual([])
diff --git a/domains/grid/utils/__tests__/configToGrid.ts b/domains/grid/utils/__tests__/configToGrid.spec.ts
similarity index 94%
rename from domains/grid/utils/__tests__/configToGrid.ts
rename to domains/grid/utils/__tests__/configToGrid.spec.ts
index 81db49c1..fb9285c8 100644
--- a/domains/grid/utils/__tests__/configToGrid.ts
+++ b/domains/grid/utils/__tests__/configToGrid.spec.ts
@@ -48,7 +48,7 @@ describe('configToGrid', () => {
title: 'single',
grid: [
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
width: 1,
height: 1,
properties: {
@@ -56,7 +56,7 @@ describe('configToGrid', () => {
},
},
{
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
width: 1,
height: 1,
properties: {
@@ -66,7 +66,7 @@ describe('configToGrid', () => {
},
},
{
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
width: 1,
height: 1,
properties: {
diff --git a/domains/grid/utils/__tests__/createGridId.spec.ts b/domains/grid/utils/__tests__/createGridId.spec.ts
index 8d6d1b5d..8d610a14 100644
--- a/domains/grid/utils/__tests__/createGridId.spec.ts
+++ b/domains/grid/utils/__tests__/createGridId.spec.ts
@@ -4,35 +4,35 @@ import { createGridId } from '../createGridId'
describe('createGridId', () => {
it('should generate a unique ID based on the title', () => {
const gridItem = { title: 'Test Title', gridColumns: 2 }
- const config: Grid[] = []
+ const grid: Grid[] = []
- const id = createGridId(gridItem, config)
+ const id = createGridId(gridItem, grid)
expect(id).toBe('test-title')
})
it('should append a number to the ID if the title is not unique', () => {
const gridItem = { title: 'Test Title', gridColumns: 2 }
- const config: Grid[] = [
+ const grid: Grid[] = [
{ id: 'test-title', title: 'Test Title', grid: [], gridColumns: 2 },
]
- const id = createGridId(gridItem, config)
+ const id = createGridId(gridItem, grid)
expect(id).toBe('test-title-0')
})
it('should handle an empty title', () => {
const gridItem = { title: '', gridColumns: 2 }
- const config: Grid[] = []
+ const grid: Grid[] = []
- const id = createGridId(gridItem, config)
+ const id = createGridId(gridItem, grid)
expect(id).toBe('')
})
it('should handle special characters in the title', () => {
const gridItem = { title: 'Test @ Title!', gridColumns: 2 }
- const config: Grid[] = []
+ const grid: Grid[] = []
- const id = createGridId(gridItem, config)
+ const id = createGridId(gridItem, grid)
expect(id).toBe('test-title')
})
})
diff --git a/domains/grid/utils/__tests__/createWidgetObject.spec.ts b/domains/grid/utils/__tests__/createWidgetObject.spec.ts
index 5eb34427..4089882d 100644
--- a/domains/grid/utils/__tests__/createWidgetObject.spec.ts
+++ b/domains/grid/utils/__tests__/createWidgetObject.spec.ts
@@ -8,7 +8,7 @@ vi.mock('/domains/grid/utils/generateItemId', () => ({
describe('createWidgetObject', () => {
it('should create a widget object with default values', () => {
const newWidget = {
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
}
const result = createWidgetObject(newWidget)
@@ -23,7 +23,7 @@ describe('createWidgetObject', () => {
it('should create a widget object with passed values', () => {
const newWidget = {
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
w: 2,
h: 2,
properties: {
diff --git a/domains/grid/utils/__tests__/gridParser.spec.ts b/domains/grid/utils/__tests__/gridParser.spec.ts
index 9c3a372e..13b282af 100644
--- a/domains/grid/utils/__tests__/gridParser.spec.ts
+++ b/domains/grid/utils/__tests__/gridParser.spec.ts
@@ -12,10 +12,10 @@ describe('Widget Input Parsing', () => {
it.each([
[
'X (Twitter) Post URL',
- GRID_WIDGET_TYPE.X,
+ GRID_WIDGET_TYPE.enum.X,
'https://x.com/feindura/status/1804519711377436675',
{
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: 'https://twitter.com/feindura/status/1804519711377436675?ref_src=twsrc%5Etfw',
type: 'post',
@@ -24,10 +24,10 @@ describe('Widget Input Parsing', () => {
],
[
'X (Twitter) Post Embed Code',
- GRID_WIDGET_TYPE.X,
+ GRID_WIDGET_TYPE.enum.X,
'Get to know the @lukso_io ecosystem in 3min 🎧 pic.twitter.com/UUh23NgdfQ
— Fabian Vogelsteller (@feindura) June 22, 2024
',
{
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: 'https://twitter.com/feindura/status/1804519711377436675?ref_src=twsrc%5Etfw',
type: 'post',
@@ -36,10 +36,10 @@ describe('Widget Input Parsing', () => {
],
[
'X (Twitter) Timeline URL',
- GRID_WIDGET_TYPE.X,
+ GRID_WIDGET_TYPE.enum.X,
'https://twitter.com/lukso_io',
{
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: 'https://twitter.com/lukso_io?ref_src=twsrc%5Etfw',
type: 'timeline',
@@ -48,10 +48,10 @@ describe('Widget Input Parsing', () => {
],
[
'X (Twitter) Timeline Embed Code',
- GRID_WIDGET_TYPE.X,
+ GRID_WIDGET_TYPE.enum.X,
'Tweets by lukso_io ',
{
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: 'https://twitter.com/lukso_io?ref_src=twsrc%5Etfw',
type: 'timeline',
@@ -60,10 +60,10 @@ describe('Widget Input Parsing', () => {
],
[
'Instagram Post URL',
- GRID_WIDGET_TYPE.INSTAGRAM,
+ GRID_WIDGET_TYPE.enum.INSTAGRAM,
'https://www.instagram.com/p/C98OXs6yhAq/?utm_source=ig_embed&utm_campaign=loading',
{
- type: GRID_WIDGET_TYPE.INSTAGRAM,
+ type: GRID_WIDGET_TYPE.enum.INSTAGRAM,
properties: {
src: 'https://www.instagram.com/p/C98OXs6yhAq/?utm_source=ig_embed&utm_campaign=loading',
type: 'p',
@@ -72,10 +72,10 @@ describe('Widget Input Parsing', () => {
],
[
'Instagram Post Embed Code',
- GRID_WIDGET_TYPE.INSTAGRAM,
+ GRID_WIDGET_TYPE.enum.INSTAGRAM,
'
',
{
- type: GRID_WIDGET_TYPE.INSTAGRAM,
+ type: GRID_WIDGET_TYPE.enum.INSTAGRAM,
properties: {
src: 'https://www.instagram.com/p/C98OXs6yhAq/?utm_source=ig_embed&utm_campaign=loading',
type: 'p',
@@ -84,10 +84,10 @@ describe('Widget Input Parsing', () => {
],
[
'Instagram Reel URL',
- GRID_WIDGET_TYPE.INSTAGRAM,
+ GRID_WIDGET_TYPE.enum.INSTAGRAM,
'https://www.instagram.com/reel/DAlOgHkuyxd/?utm_source=ig_embed&utm_campaign=loading',
{
- type: GRID_WIDGET_TYPE.INSTAGRAM,
+ type: GRID_WIDGET_TYPE.enum.INSTAGRAM,
properties: {
src: 'https://www.instagram.com/reel/DAlOgHkuyxd/?utm_source=ig_embed&utm_campaign=loading',
type: 'reel',
@@ -96,10 +96,10 @@ describe('Widget Input Parsing', () => {
],
[
'Instagram Reel Embed Code',
- GRID_WIDGET_TYPE.INSTAGRAM,
+ GRID_WIDGET_TYPE.enum.INSTAGRAM,
'
',
{
- type: GRID_WIDGET_TYPE.INSTAGRAM,
+ type: GRID_WIDGET_TYPE.enum.INSTAGRAM,
properties: {
src: 'https://www.instagram.com/reel/DAlOgHkuyxd/?utm_source=ig_embed&utm_campaign=loading',
type: 'reel',
@@ -108,10 +108,10 @@ describe('Widget Input Parsing', () => {
],
[
'YouTube URL',
- GRID_WIDGET_TYPE.YOUTUBE,
+ GRID_WIDGET_TYPE.enum.YOUTUBE,
'https://www.youtube.com/watch?v=Vw4JE64hsO8',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://www.youtube.com/embed/Vw4JE64hsO8',
allow: YOUTUBE_IFRAME_ALLOW,
@@ -120,10 +120,10 @@ describe('Widget Input Parsing', () => {
],
[
'YouTube Embed Code',
- GRID_WIDGET_TYPE.YOUTUBE,
+ GRID_WIDGET_TYPE.enum.YOUTUBE,
'',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://www.youtube.com/embed/Vw4JE64hsO8',
allow: YOUTUBE_IFRAME_ALLOW,
@@ -132,10 +132,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Track URL',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'https://open.spotify.com/track/7xGfFoTpQ2E7fRF5lN10tr',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/track/7xGfFoTpQ2E7fRF5lN10tr?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -145,10 +145,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Track Embed Code',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/track/2BHj31ufdEqVK5CkYDp9mA?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -158,10 +158,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Track Embed Code with theme',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/track/48K735Rd3UQExzjXH004k1?utm_source=generator&theme=0',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -172,10 +172,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Playlist URL',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'https://open.spotify.com/playlist/7KFoK4LJ23EncELJwYmTDG',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/playlist/7KFoK4LJ23EncELJwYmTDG?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -185,10 +185,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Playlist Embed Code',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/playlist/7KFoK4LJ23EncELJwYmTDG?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -198,10 +198,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Artist URL',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'https://open.spotify.com/artist/4KY9rCrokaoFzvMfX98u1q',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/artist/4KY9rCrokaoFzvMfX98u1q?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -211,10 +211,10 @@ describe('Widget Input Parsing', () => {
],
[
'Spotify Artist Embed Code',
- GRID_WIDGET_TYPE.SPOTIFY,
+ GRID_WIDGET_TYPE.enum.SPOTIFY,
'',
{
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: 'https://open.spotify.com/embed/artist/4KY9rCrokaoFzvMfX98u1q?utm_source=generator',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -224,10 +224,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Track Share URL',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
'https://soundcloud.com/occams-laser/with-you',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1856391039&show_artwork=true',
allow: SOUNDCLOUD_IFRAME_ALLOW,
@@ -237,10 +237,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Set Share URL',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
'https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F505007376&show_artwork=true',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F505007376&show_artwork=true',
allow: SPOTIFY_IFRAME_ALLOW,
@@ -250,10 +250,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Users Embed Code',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
'https://soundcloud.com/fabian-vogelsteller',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Fusers%2F227118126&show_artwork=true',
allow: SOUNDCLOUD_IFRAME_ALLOW,
@@ -263,10 +263,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Track Embed Code',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
' ',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/tracks/1856391039&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true',
allow: SOUNDCLOUD_IFRAME_ALLOW,
@@ -276,10 +276,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Track Embed Code with URL encoded characters',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
'',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1856391039&show_artwork=true',
allow: SOUNDCLOUD_IFRAME_ALLOW,
@@ -289,10 +289,10 @@ describe('Widget Input Parsing', () => {
],
[
'SoundCloud Playlist Embed Code',
- GRID_WIDGET_TYPE.SOUNDCLOUD,
+ GRID_WIDGET_TYPE.enum.SOUNDCLOUD,
' ',
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://w.soundcloud.com/player/?url=https://api.soundcloud.com/playlists/1850298147&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true',
allow: SOUNDCLOUD_IFRAME_ALLOW,
diff --git a/domains/grid/utils/__tests__/gridToConfig.spec.ts b/domains/grid/utils/__tests__/gridToConfig.spec.ts
index d68fdb0d..d3348443 100644
--- a/domains/grid/utils/__tests__/gridToConfig.spec.ts
+++ b/domains/grid/utils/__tests__/gridToConfig.spec.ts
@@ -13,7 +13,7 @@ describe('gridToConfig', () => {
title: 'single',
grid: [
{
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: 'https://via.placeholder.com/150',
},
@@ -24,7 +24,7 @@ describe('gridToConfig', () => {
y: 0,
},
{
- type: GRID_WIDGET_TYPE.TEXT,
+ type: GRID_WIDGET_TYPE.enum.TEXT,
properties: {
title: 'Hey',
text: 'Customize your grid grid!',
@@ -37,7 +37,7 @@ describe('gridToConfig', () => {
y: 1,
},
{
- type: GRID_WIDGET_TYPE.IMAGE,
+ type: GRID_WIDGET_TYPE.enum.IMAGE,
properties: {
src: 'https://via.placeholder.com/150',
},
diff --git a/domains/grid/utils/__tests__/isConfigValid.spec.ts b/domains/grid/utils/__tests__/isConfigValid.spec.ts
deleted file mode 100644
index 6e9f4d71..00000000
--- a/domains/grid/utils/__tests__/isConfigValid.spec.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { isConfigValid } from '../isConfigValid'
-import { GRID_WIDGET_TYPE } from './../../shared/config'
-
-describe('isConfigValid', () => {
- it('should return false when gridConfig is invalid', () => {
- expect(isConfigValid()).toBe(false)
- expect(isConfigValid(null)).toBe(false)
- expect(isConfigValid({})).toBe(false)
- })
-
- it('should return true when gridConfig is empty', () => {
- expect(isConfigValid([])).toBe(true)
- })
-
- it('should return true for valid gridConfig', () => {
- const config = [
- {
- title: 'default',
- grid: [
- {
- type: GRID_WIDGET_TYPE.IFRAME,
- width: 1,
- height: 1,
- properties: {},
- },
- ],
- },
- {
- title: 'grid-1',
- grid: [
- {
- type: GRID_WIDGET_TYPE.IFRAME,
- width: 1,
- height: 1,
- properties: {},
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(true)
- })
-
- it('should return false for gridConfig with missing title', () => {
- const config = [
- {
- grid: [
- {
- type: 'invlidType',
- width: 1,
- height: 1,
- properties: {},
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(false)
- })
-
- it('should return false for gridConfig with invalid type', () => {
- const config = [
- {
- title: 'someName',
- grid: [
- {
- type: 'invlidType',
- width: 1,
- height: 1,
- properties: {},
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(false)
- })
-
- it('should return false for gridConfig with non-number width', () => {
- const config = [
- {
- title: 'someName',
- grid: [
- {
- type: GRID_WIDGET_TYPE.IFRAME,
- width: '1',
- height: 1,
- properties: {},
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(false)
- })
-
- it('should return false for gridConfig with non-number height', () => {
- const config = [
- {
- title: 'someName',
- grid: [
- {
- type: GRID_WIDGET_TYPE.IFRAME,
- width: 1,
- height: '1',
- properties: {},
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(false)
- })
-
- it('should return false for gridConfig with non-object properties', () => {
- const config = [
- {
- title: 'someName',
- grid: [
- {
- type: GRID_WIDGET_TYPE.IFRAME,
- width: 1,
- height: 1,
- },
- ],
- },
- ]
- expect(isConfigValid(config)).toBe(false)
- })
-})
diff --git a/domains/grid/utils/__tests__/purifyGridConfig.spec.ts b/domains/grid/utils/__tests__/purifyGridConfig.spec.ts
new file mode 100644
index 00000000..bbfd4d08
--- /dev/null
+++ b/domains/grid/utils/__tests__/purifyGridConfig.spec.ts
@@ -0,0 +1,272 @@
+import { describe, expect, it, vi } from 'vitest'
+import { purifyGridConfig } from '../purifyGridConfig'
+
+describe('purifyGridConfig', () => {
+ it('should return an empty array for empty configuration', async () => {
+ const result = await purifyGridConfig([])
+ expect(result).toEqual([])
+ })
+
+ it('should return valid grid', async () => {
+ const result = await purifyGridConfig([
+ {
+ title: 'grid-1',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ {
+ title: 'grid-2',
+ gridColumns: 3,
+ grid: [
+ {
+ type: 'IMAGE',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'grid-1',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ {
+ title: 'grid-2',
+ gridColumns: 3,
+ grid: [
+ {
+ type: 'IMAGE',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+
+ it('should return default column number when value is missing', async () => {
+ const result = await purifyGridConfig([
+ {
+ title: 'grid-1',
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'grid-1',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+
+ it('should return default title when value is missing', async () => {
+ const result = await purifyGridConfig([
+ {
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'Main',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+
+ it('should return empty widget array when value is missing', async () => {
+ const result = await purifyGridConfig([
+ {
+ title: 'grid-1',
+ gridColumns: 2,
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'grid-1',
+ gridColumns: 2,
+ grid: [],
+ },
+ ])
+ })
+
+ it('should recreate grid from invalid element', async () => {
+ const result = await purifyGridConfig([
+ {
+ asdf: 'asdf',
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'Main',
+ gridColumns: 2,
+ grid: [],
+ },
+ ])
+ })
+
+ it('should recreate widget width and height if missing', async () => {
+ const result = await purifyGridConfig([
+ {
+ grid: [
+ {
+ type: 'IFRAME',
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'Main',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+
+ it('should remove widget without type', async () => {
+ const result = await purifyGridConfig([
+ {
+ grid: [
+ {
+ type: 'IFRAME',
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ {
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'Main',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+
+ it('should remove widget without properties', async () => {
+ const result = await purifyGridConfig([
+ {
+ grid: [
+ {
+ type: 'IFRAME',
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ {
+ type: 'IMAGE',
+ },
+ ],
+ },
+ ])
+ expect(result).toEqual([
+ {
+ title: 'Main',
+ gridColumns: 2,
+ grid: [
+ {
+ type: 'IFRAME',
+ width: 1,
+ height: 1,
+ properties: {
+ src: 'https://example.com',
+ },
+ },
+ ],
+ },
+ ])
+ })
+})
diff --git a/domains/grid/utils/buildGrid.ts b/domains/grid/utils/buildGrid.ts
index b2304731..0a3ba3a5 100644
--- a/domains/grid/utils/buildGrid.ts
+++ b/domains/grid/utils/buildGrid.ts
@@ -1,23 +1,25 @@
export const buildGrid = (
- grid: Grid[] | Grid[],
+ grid: Grid[],
isMobile?: boolean,
withAddContentPlaceholder?: boolean
-): Grid[] => {
+): Grid[] => {
const _buildGrid = (
- grid: GridWidgetWithoutCords[],
- updatedGrid: GridWidget[],
+ gridWidgets: GridWidget[],
+ updatedGridWidgets: GridWidget[],
gridColumns: number
) => {
// remove "add widget" placeholder from grid
- let _grid = grid.filter(item => item.type !== GRID_WIDGET_TYPE.ADD_CONTENT)
+ let _grid = gridWidgets.filter(
+ item => item.type !== GRID_WIDGET_TYPE.enum.ADD_CONTENT
+ )
// if items already have x/y cords we re-order grid to reflect that
_grid = _grid.slice().sort((a, b) => {
if (
- a.x === undefined ||
- b.x === undefined ||
- a.y === undefined ||
- b.y === undefined
+ typeof a.x !== 'number' ||
+ typeof a.y !== 'number' ||
+ typeof b.x !== 'number' ||
+ typeof b.y !== 'number'
) {
return 0
}
@@ -34,17 +36,17 @@ export const buildGrid = (
_grid.push(
createWidgetObject({
i: 'placeholder',
- type: GRID_WIDGET_TYPE.ADD_CONTENT,
+ type: GRID_WIDGET_TYPE.enum.ADD_CONTENT,
isResizable: false,
- })
+ }) as GridWidget
)
}
for (const widget of _grid) {
- placeWidgetInGrid(widget, updatedGrid, gridColumns)
+ placeWidgetInGrid(widget, updatedGridWidgets, gridColumns)
}
- return updatedGrid
+ return updatedGridWidgets
}
return grid.map(item => {
diff --git a/domains/grid/utils/compareGridWidgets.ts b/domains/grid/utils/compareGridWidgets.ts
index f0fa688b..f2ed869b 100644
--- a/domains/grid/utils/compareGridWidgets.ts
+++ b/domains/grid/utils/compareGridWidgets.ts
@@ -19,9 +19,11 @@ export const compareGridWidgets = (
'properties',
]
const gridA =
- _gridA?.filter(item => item.type !== GRID_WIDGET_TYPE.ADD_CONTENT) || []
+ _gridA?.filter(item => item.type !== GRID_WIDGET_TYPE.enum.ADD_CONTENT) ||
+ []
const gridB =
- _gridB?.filter(item => item.type !== GRID_WIDGET_TYPE.ADD_CONTENT) || []
+ _gridB?.filter(item => item.type !== GRID_WIDGET_TYPE.enum.ADD_CONTENT) ||
+ []
const maxLength = Math.max(gridA.length, gridB.length)
for (let i = 0; i < maxLength; i++) {
diff --git a/domains/grid/utils/compareGrids.ts b/domains/grid/utils/compareGrids.ts
index 469afb87..85891b2b 100644
--- a/domains/grid/utils/compareGrids.ts
+++ b/domains/grid/utils/compareGrids.ts
@@ -5,16 +5,13 @@
* @param gridB
* @returns
*/
-export const compareGrids = (
- gridA: Grid[],
- gridB: Grid[]
-): GridChange[] => {
+export const compareGrids = (gridA: Grid[], gridB: Grid[]): GridChange[] => {
const result: GridChange[] = []
const maxLength = Math.max(gridA.length, gridB.length)
for (let i = 0; i < maxLength; i++) {
- const oldGrid = gridA[i] as Grid | undefined
- const newGrid = gridB[i] as Grid | undefined
+ const oldGrid = gridA[i] as Grid | undefined
+ const newGrid = gridB[i] as Grid | undefined
// compare differences in title or grid array
const comparedWidgets = compareGridWidgets(oldGrid?.grid, newGrid?.grid)
diff --git a/domains/grid/utils/configToGrid.ts b/domains/grid/utils/configToGrid.ts
index 8523665b..e500f152 100644
--- a/domains/grid/utils/configToGrid.ts
+++ b/domains/grid/utils/configToGrid.ts
@@ -1,29 +1,25 @@
-import type { GridConfigItem } from '@/types/grid'
-
/**
* Convert Grid config to Grid layout
*
* @param config
- * @param columns
*/
-export const configToGrid = (
- config: PartialBy, 'id'>[]
-): Grid[] => {
- const grid: Grid[] = []
+export const configToGrid = (config: GridConfig[]): Grid[] => {
+ const grid: Grid[] = []
- for (const gridItem of config) {
+ for (const configItem of config) {
grid.push({
- id: createGridId(gridItem, grid),
- title: gridItem.title,
- grid: gridItem.grid.map(widget => {
- return createWidgetObject({
- type: widget.type,
- properties: widget.properties,
- w: widget.width,
- h: widget.height,
- })
- }),
- gridColumns: getGridColumns(gridItem.gridColumns),
+ id: createGridId(configItem, grid),
+ title: configItem.title || DEFAULT_GRID_TITLE,
+ grid:
+ configItem.grid?.map(widget => {
+ return createWidgetObject({
+ type: widget.type,
+ properties: widget.properties,
+ w: widget.width,
+ h: widget.height,
+ }) as GridWidget
+ }) || [],
+ gridColumns: getGridColumns(configItem.gridColumns),
})
}
diff --git a/domains/grid/utils/createGridId.ts b/domains/grid/utils/createGridId.ts
index b823ae4a..ca750712 100644
--- a/domains/grid/utils/createGridId.ts
+++ b/domains/grid/utils/createGridId.ts
@@ -1,24 +1,18 @@
-import type { GridWidget } from '@/types/grid'
-
/**
* Create a unique id for a grid item based on title
*
* @param gridItem
- * @param config
- * @returns
+ * @param grid
*/
-export const createGridId = (
- gridItem: PartialBy, 'id' | 'grid'>,
- config: Grid[] | Grid[]
+export const createGridId = (
+ gridItem: Partial,
+ grid: Grid[]
): string => {
- // generate id based on title using slug() util
- // look in config if title exist, if title is not unique, add a number to the end
-
- const baseId = slug(gridItem.title)
+ const baseId = slug(gridItem?.title)
let uniqueId = baseId
let counter = 0
- while (config.some(item => item.id === uniqueId)) {
+ while (grid.some(item => item.id === uniqueId)) {
uniqueId = `${baseId}-${counter}`
counter++
}
diff --git a/domains/grid/utils/createWidgetObject.ts b/domains/grid/utils/createWidgetObject.ts
index d1ee270c..a6d3f571 100644
--- a/domains/grid/utils/createWidgetObject.ts
+++ b/domains/grid/utils/createWidgetObject.ts
@@ -10,7 +10,7 @@ export const createWidgetObject = (
newWidget: Partial
): GridWidgetWithoutCords => {
assertNotUndefined(newWidget.type, 'Widget `type` field is undefined')
- assert(newWidget.type in GRID_WIDGET_TYPE, 'Invalid widget `type` filed')
+ assert(newWidget.type in GRID_WIDGET_TYPE.enum, 'Invalid widget `type` filed')
// TODO add validation for properties based on widget type
diff --git a/domains/grid/utils/getUserGrid.ts b/domains/grid/utils/getUserGrid.ts
index aa66bb2d..1fcaa2ef 100644
--- a/domains/grid/utils/getUserGrid.ts
+++ b/domains/grid/utils/getUserGrid.ts
@@ -3,20 +3,9 @@
*
* @param address
*/
-export const getUserGrid = async (
- address: Address
-): Promise[]> => {
- let config: PartialBy, 'id'>[] = []
+export const getUserGrid = async (address: Address): Promise => {
const userConfig = await getGridConfig(address)
+ const purifiedConfig = await purifyGridConfig(userConfig)
- // if user config is invalid we load default one
- if (isConfigValid(userConfig)) {
- config = userConfig as Grid[]
- } else {
- if (gridLog.enabled) {
- gridLog('Invalid config', userConfig)
- }
- }
-
- return configToGrid(config)
+ return configToGrid(purifiedConfig)
}
diff --git a/domains/grid/utils/gridConfig.ts b/domains/grid/utils/gridConfig.ts
index a6d67d08..9b2cd2b6 100644
--- a/domains/grid/utils/gridConfig.ts
+++ b/domains/grid/utils/gridConfig.ts
@@ -43,7 +43,7 @@ export const getGridConfig = async (address: Address) => {
.call()
if (!getDataValue) {
- return
+ return []
}
// decode config
@@ -60,7 +60,7 @@ export const getGridConfig = async (address: Address) => {
const { url } = decodedJsonUrl.value as VerifiableURI
// fetch config file from IPFS
- const config = await fetcher[], Record>({
+ const config = await fetcher>({
url: resolveUrl(url),
method: 'GET',
})
@@ -69,7 +69,7 @@ export const getGridConfig = async (address: Address) => {
gridLog('Grid config from IPFS', config)
}
- return config
+ return config || []
}
/**
@@ -79,10 +79,7 @@ export const getGridConfig = async (address: Address) => {
* @param config
* @param saveCallback
*/
-export const saveConfig = async (
- address: Address,
- config: PartialBy, 'id'>[]
-) => {
+export const saveConfig = async (address: Address, config: GridConfig[]) => {
// convert config to blob
const blob = new Blob([JSON.stringify(config, null, 2)], {
type: 'application/json',
diff --git a/domains/grid/utils/gridParser.ts b/domains/grid/utils/gridParser.ts
index d4461480..49e389a0 100644
--- a/domains/grid/utils/gridParser.ts
+++ b/domains/grid/utils/gridParser.ts
@@ -3,15 +3,15 @@ export const parsePlatformInput = async (
input: string
): Promise => {
switch (platform) {
- case GRID_WIDGET_TYPE.X:
+ case GRID_WIDGET_TYPE.enum.X:
return parseXWidgetInput(input)
- case GRID_WIDGET_TYPE.INSTAGRAM:
+ case GRID_WIDGET_TYPE.enum.INSTAGRAM:
return parseInstagramWidgetInput(input)
- case GRID_WIDGET_TYPE.YOUTUBE:
+ case GRID_WIDGET_TYPE.enum.YOUTUBE:
return parseYoutubeWidgetInput(input)
- case GRID_WIDGET_TYPE.SPOTIFY:
+ case GRID_WIDGET_TYPE.enum.SPOTIFY:
return parseSpotifyWidgetInput(input)
- case GRID_WIDGET_TYPE.SOUNDCLOUD:
+ case GRID_WIDGET_TYPE.enum.SOUNDCLOUD:
return await parseSoundCloudWidgetInput(input)
default:
throw new Error('Invalid platform')
@@ -30,7 +30,7 @@ const parseYoutubeWidgetInput = (input: string): GridWidgetExtended | never => {
if (youtubeUrlMatch) {
return {
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: `https://www.youtube.com/embed/${youtubeUrlMatch[1]}`,
allow: YOUTUBE_IFRAME_ALLOW,
@@ -42,7 +42,7 @@ const parseYoutubeWidgetInput = (input: string): GridWidgetExtended | never => {
if (youtubeEmbedMatch) {
return {
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: `https://www.youtube.com/embed/${youtubeEmbedMatch[1]}`,
allow: YOUTUBE_IFRAME_ALLOW,
@@ -63,7 +63,7 @@ const parseXWidgetInput = (input: string): GridWidgetExtended | never => {
if (postUser && postId) {
return {
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: `https://twitter.com/${postUser}/status/${postId}?ref_src=twsrc%5Etfw`,
type: 'post',
@@ -75,7 +75,7 @@ const parseXWidgetInput = (input: string): GridWidgetExtended | never => {
if (timelineUser) {
return {
- type: GRID_WIDGET_TYPE.X,
+ type: GRID_WIDGET_TYPE.enum.X,
properties: {
src: `https://twitter.com/${timelineUser}?ref_src=twsrc%5Etfw`,
type: 'timeline',
@@ -98,7 +98,7 @@ const parseSpotifyWidgetInput = (input: string): GridWidgetExtended | never => {
const theme = groups.theme ? `&theme=${groups.theme}` : ''
return {
- type: GRID_WIDGET_TYPE.SPOTIFY,
+ type: GRID_WIDGET_TYPE.enum.SPOTIFY,
properties: {
src: `https://open.spotify.com/embed/${groups.type}/${groups.id}?utm_source=generator${theme}`,
type: groups.type,
@@ -146,7 +146,7 @@ const parseSoundCloudWidgetInputFromEmbed = (
if (match) {
return {
- type: GRID_WIDGET_TYPE.IFRAME,
+ type: GRID_WIDGET_TYPE.enum.IFRAME,
properties: {
src: match,
allow: SOUNDCLOUD_IFRAME_ALLOW,
@@ -186,7 +186,7 @@ const parseInstagramWidgetInput = (
if (id && type) {
return {
- type: GRID_WIDGET_TYPE.INSTAGRAM,
+ type: GRID_WIDGET_TYPE.enum.INSTAGRAM,
properties: {
src: `https://www.instagram.com/${type}/${id}/${params}`,
type: type,
diff --git a/domains/grid/utils/gridToConfig.ts b/domains/grid/utils/gridToConfig.ts
index e6ba8782..96f21c34 100644
--- a/domains/grid/utils/gridToConfig.ts
+++ b/domains/grid/utils/gridToConfig.ts
@@ -3,13 +3,11 @@
*
* @param _grid
*/
-export const gridToConfig = (
- _grid: Grid[]
-): PartialBy, 'id'>[] => {
- const convertGrid = (grid: GridWidget[]): GridConfigItem[] => {
+export const gridToConfig = (_grid: Grid[]): GridConfig[] => {
+ const convertGrid = (gridWidgets: GridWidget[]): GridConfigWidget[] => {
// remove "add content" widget from grid before saving
- const gridWithoutAddContentWidget = grid.filter(
- item => item.type !== GRID_WIDGET_TYPE.ADD_CONTENT
+ const gridWithoutAddContentWidget = gridWidgets.filter(
+ item => item.type !== GRID_WIDGET_TYPE.enum.ADD_CONTENT
)
// sort by y and then x to get the correct order
diff --git a/domains/grid/utils/isConfigValid.ts b/domains/grid/utils/isConfigValid.ts
deleted file mode 100644
index 625d1ee9..00000000
--- a/domains/grid/utils/isConfigValid.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Check if the grid config is valid
- *
- * @param grid
- * @returns
- */
-export function isConfigValid(gridConfig?: any): boolean {
- if (!Array.isArray(gridConfig)) {
- return false
- }
-
- if (gridConfig.length === 0) {
- return true
- }
-
- for (const item of gridConfig) {
- if (typeof item.title !== 'string') {
- return false
- }
-
- if (!Array.isArray(item.grid)) {
- return false
- }
-
- for (const gridItem of item.grid) {
- if (
- !Object.values(GRID_WIDGET_TYPE).includes(gridItem.type) ||
- typeof gridItem.width !== 'number' ||
- typeof gridItem.height !== 'number' ||
- typeof gridItem.properties !== 'object'
- ) {
- return false
- }
- }
- }
-
- return true
-}
diff --git a/domains/grid/utils/placeWidgetInGrid.ts b/domains/grid/utils/placeWidgetInGrid.ts
index 01043e0a..e8c385c2 100644
--- a/domains/grid/utils/placeWidgetInGrid.ts
+++ b/domains/grid/utils/placeWidgetInGrid.ts
@@ -7,10 +7,10 @@
*/
export const placeWidgetInGrid = (
widget: GridWidgetWithoutCords,
- grid: GridWidget[],
+ gridWidgets: GridWidget[],
gridColumns: number
): void => {
- const columnHeights = getColumnHeightsFromGrid(grid, gridColumns)
+ const columnHeights = getColumnHeightsFromGrid(gridWidgets, gridColumns)
const w = Math.min(widget.w, gridColumns)
const { x, y } = findBestPosition(
@@ -27,5 +27,5 @@ export const placeWidgetInGrid = (
originalWidth: w < widget.w ? widget.w : undefined,
}
- grid.push(newWidget)
+ gridWidgets.push(newWidget)
}
diff --git a/domains/grid/utils/purifyGridConfig.ts b/domains/grid/utils/purifyGridConfig.ts
new file mode 100644
index 00000000..eb2ad03a
--- /dev/null
+++ b/domains/grid/utils/purifyGridConfig.ts
@@ -0,0 +1,72 @@
+import { z } from 'zod'
+
+const filterValidWidgetSchema = z.array(z.unknown()).transform(
+ async widgetItems =>
+ await widgetItems.reduce>(
+ async (accPromise, widgetItem) => {
+ const acc = await accPromise
+ const widgetValidation =
+ await gridConfigWidgetSchema.safeParseAsync(widgetItem)
+
+ if (widgetValidation.success) {
+ acc.push(widgetValidation.data)
+ } else {
+ console.warn('Invalid widget', widgetValidation.error, widgetItem)
+ }
+
+ return acc
+ },
+ Promise.resolve([])
+ )
+)
+
+const filterValidConfigSchema = z.array(z.unknown()).transform(
+ async configItems =>
+ await configItems.reduce>(
+ async (accPromise, configItem) => {
+ const acc = await accPromise
+
+ // first we validate the grid config item
+ const configValidation = await gridConfigSchema
+ .omit({ grid: true })
+ .safeParseAsync(configItem)
+ const validGridConfig = configValidation.data as GridConfig
+
+ if (configValidation.success) {
+ const widgets = (configItem as GridConfig)?.grid || []
+ // then we validate the widgets
+ const widgetValidation =
+ await filterValidWidgetSchema.safeParseAsync(widgets)
+
+ if (widgetValidation.success) {
+ validGridConfig.grid = widgetValidation.data as GridConfigWidget[]
+ }
+
+ acc.push(validGridConfig)
+ }
+
+ return acc
+ },
+ Promise.resolve([])
+ )
+)
+
+/**
+ * Purify grid config to only include valid items based on the schema
+ * - config item fields (title, gridColumns) are recreated from defaults
+ * - some widget fields are recreated (width, height)
+ * - widgets that miss important fields (type, properties) are removed
+ *
+ * @param config
+ */
+export const purifyGridConfig = async (
+ config: unknown
+): Promise => {
+ const validation = await filterValidConfigSchema.safeParseAsync(config)
+
+ if (!validation.success) {
+ console.warn('Invalid grid config', validation.error, config)
+ }
+
+ return validation.data as GridConfig[]
+}
diff --git a/domains/schema/gridConfigSchema.ts b/domains/schema/gridConfigSchema.ts
new file mode 100644
index 00000000..826090d1
--- /dev/null
+++ b/domains/schema/gridConfigSchema.ts
@@ -0,0 +1,17 @@
+import { z } from 'zod'
+
+export const gridConfigWidgetSchema = z.object({
+ width: z.number().default(1),
+ height: z.number().default(1),
+ type: GRID_WIDGET_TYPE,
+ properties: gridWidgetProperties,
+})
+
+export const gridConfigSchema = z.object({
+ title: z.string().default(DEFAULT_GRID_TITLE),
+ grid: z.array(gridConfigWidgetSchema).default([]),
+ gridColumns: z.number().default(GRID_COLUMNS_MIN),
+})
+
+export type GridConfigWidget = z.input
+export type GridConfig = z.input
diff --git a/domains/schema/gridSchema.ts b/domains/schema/gridSchema.ts
new file mode 100644
index 00000000..732e6d20
--- /dev/null
+++ b/domains/schema/gridSchema.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod'
+
+export const gridWidgetProperties = z.union([
+ iframeWidgetSchema,
+ imageWidgetSchema,
+ textWidgetSchema,
+ instagramWidgetSchema,
+ soundCloudWidgetSchema,
+ spotifyWidgetSchema,
+ warpcastWidgetSchema,
+ xWidgetSchema,
+ youtubeWidgetSchema,
+])
+
+export const gridWidgetSchema = z.object({
+ w: z.number().positive(),
+ h: z.number().positive(),
+ x: z.number().positive(),
+ y: z.number().positive(),
+ i: z.string().or(z.number()),
+ type: GRID_WIDGET_TYPE,
+ originalWidth: z.number().optional(),
+ properties: gridWidgetProperties.or(z.object({})),
+ isResizable: z.boolean().optional(),
+ isDraggable: z.boolean().optional(),
+ static: z.boolean().optional(),
+})
+
+export const gridWidgetWithoutCordsSchema = gridWidgetSchema.omit({
+ x: true,
+ y: true,
+})
+
+export const gridSchema = z.object({
+ title: z.string(),
+ grid: z.array(gridWidgetSchema),
+ gridColumns: z.number(),
+ id: z.string(),
+})
+
+export type GridWidget = z.input
+export type GridWidgetWithoutCords = z.input<
+ typeof gridWidgetWithoutCordsSchema
+>
+export type Grid = z.input
+export type GridWidgetProperties = z.input
diff --git a/domains/schema/iframeWidgetSchema.ts b/domains/schema/iframeWidgetSchema.ts
new file mode 100644
index 00000000..55fe6d4c
--- /dev/null
+++ b/domains/schema/iframeWidgetSchema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+export const iframeWidgetSchema = z.object({
+ src: z.string().transform(urlTransform),
+ allow: z.string().optional(),
+})
+
+export type IframeWidgetProperties = z.input
diff --git a/domains/schema/imageWidgetSchema.ts b/domains/schema/imageWidgetSchema.ts
new file mode 100644
index 00000000..1f63be6e
--- /dev/null
+++ b/domains/schema/imageWidgetSchema.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod'
+
+export const imageWidgetSchema = z.object({
+ src: z.string().transform(urlTransform),
+})
+
+export type ImageWidgetProperties = z.input
diff --git a/domains/schema/instagramWidgetSchema.ts b/domains/schema/instagramWidgetSchema.ts
new file mode 100644
index 00000000..fa0c3956
--- /dev/null
+++ b/domains/schema/instagramWidgetSchema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod'
+
+export const instagramWidgetSchema = z
+ .object({
+ src: z.string(),
+ type: z.string().optional(),
+ })
+ .transform((values, ctx) =>
+ platformTransform(values, ctx, GRID_WIDGET_TYPE.enum.INSTAGRAM)
+ )
+
+export type InstagramWidgetProperties = z.input
diff --git a/domains/schema/soundCloudWidgetSchema.ts b/domains/schema/soundCloudWidgetSchema.ts
new file mode 100644
index 00000000..c018f3f0
--- /dev/null
+++ b/domains/schema/soundCloudWidgetSchema.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod'
+
+export const soundCloudWidgetSchema = z
+ .object({
+ src: z.string(),
+ type: z.string().optional(),
+ allow: z.string().optional(),
+ })
+ .transform((values, ctx) =>
+ platformTransform(values, ctx, GRID_WIDGET_TYPE.enum.SOUNDCLOUD)
+ )
+
+export type SoundCloudWidgetProperties = z.input
diff --git a/domains/schema/spotifyWidgetSchema.ts b/domains/schema/spotifyWidgetSchema.ts
new file mode 100644
index 00000000..e92aebf7
--- /dev/null
+++ b/domains/schema/spotifyWidgetSchema.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod'
+
+export const spotifyWidgetSchema = z
+ .object({
+ src: z.string(),
+ type: z.string().optional(),
+ allow: z.string().optional(),
+ theme: z.string().optional(),
+ })
+ .transform((values, ctx) =>
+ platformTransform(values, ctx, GRID_WIDGET_TYPE.enum.SPOTIFY)
+ )
+
+export type SpotifyWidgetProperties = z.input
diff --git a/domains/schema/textWidgetSchema.ts b/domains/schema/textWidgetSchema.ts
new file mode 100644
index 00000000..d1ba0592
--- /dev/null
+++ b/domains/schema/textWidgetSchema.ts
@@ -0,0 +1,11 @@
+import { z } from 'zod'
+
+export const textWidgetSchema = z.object({
+ titleColor: z.string().default('#243542').superRefine(hexColorValidator),
+ textColor: z.string().default('#243542').superRefine(hexColorValidator),
+ backgroundColor: z.string().default('#f9f9f9').superRefine(hexColorValidator),
+ title: z.string().optional(),
+ text: z.string().optional(),
+})
+
+export type TextWidgetProperties = z.input
diff --git a/domains/schema/transforms/platformTransform.ts b/domains/schema/transforms/platformTransform.ts
new file mode 100644
index 00000000..5872f7cc
--- /dev/null
+++ b/domains/schema/transforms/platformTransform.ts
@@ -0,0 +1,32 @@
+import { z } from 'zod'
+
+const { formatMessage } = useIntl()
+
+/**
+ *
+ *
+ * @param values
+ * @param ctx
+ * @param type
+ */
+export const platformTransform = async (
+ values: {
+ src: string
+ },
+ ctx: z.RefinementCtx,
+ type: GridWidgetType
+) => {
+ try {
+ const parsedValues = await parsePlatformInput(type, values.src)
+ return parsedValues.properties
+ } catch (error: unknown) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: formatMessage('errors_invalid_input', {
+ name: capitalize(type),
+ }),
+ path: ['src'],
+ })
+ return z.NEVER
+ }
+}
diff --git a/domains/schema/transforms/urlTransform.ts b/domains/schema/transforms/urlTransform.ts
new file mode 100644
index 00000000..268f13ed
--- /dev/null
+++ b/domains/schema/transforms/urlTransform.ts
@@ -0,0 +1,22 @@
+import { z } from 'zod'
+
+/**
+ * Validate url
+ *
+ * @param value
+ * @param ctx
+ */
+export const urlTransform = async (value: string, ctx: z.RefinementCtx) => {
+ const { formatMessage } = useIntl()
+
+ try {
+ new URL(value)
+ return encodeURI(value)
+ } catch (error) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: formatMessage('errors_invalid_url'),
+ })
+ return z.NEVER
+ }
+}
diff --git a/domains/schema/validators/hexColorValidator.ts b/domains/schema/validators/hexColorValidator.ts
new file mode 100644
index 00000000..69729725
--- /dev/null
+++ b/domains/schema/validators/hexColorValidator.ts
@@ -0,0 +1,26 @@
+import tinycolor from 'tinycolor2'
+import { z } from 'zod'
+
+/**
+ * Validate hex color
+ *
+ * @param value
+ * @param ctx
+ */
+export const hexColorValidator = async (
+ value: string,
+ ctx: z.RefinementCtx
+) => {
+ const { formatMessage } = useIntl()
+ const tColor = tinycolor(value)
+
+ if (!tColor.isValid()) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: formatMessage('errors_invalid_hex_color'),
+ })
+ return false
+ }
+
+ return true
+}
diff --git a/domains/schema/warpcastWidgetSchema.ts b/domains/schema/warpcastWidgetSchema.ts
new file mode 100644
index 00000000..4c0745fa
--- /dev/null
+++ b/domains/schema/warpcastWidgetSchema.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod'
+
+export const warpcastWidgetSchema = z.object({
+ src: z.string().transform(urlTransform),
+ allow: z.string().optional(),
+})
+
+export type WarpcastWidgetProperties = z.input
diff --git a/domains/schema/xWidgetSchema.ts b/domains/schema/xWidgetSchema.ts
new file mode 100644
index 00000000..f6bf72b7
--- /dev/null
+++ b/domains/schema/xWidgetSchema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod'
+
+export const xWidgetSchema = z
+ .object({
+ src: z.string(),
+ type: z.string().optional(),
+ })
+ .transform((values, ctx) =>
+ platformTransform(values, ctx, GRID_WIDGET_TYPE.enum.X)
+ )
+
+export type XWidgetProperties = z.input
diff --git a/domains/schema/youtubeWidgetSchema.ts b/domains/schema/youtubeWidgetSchema.ts
new file mode 100644
index 00000000..3247955a
--- /dev/null
+++ b/domains/schema/youtubeWidgetSchema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod'
+
+export const youtubeWidgetSchema = z
+ .object({
+ src: z.string(),
+ allow: z.string().optional(),
+ })
+ .transform((values, ctx) =>
+ platformTransform(values, ctx, GRID_WIDGET_TYPE.enum.YOUTUBE)
+ )
+
+export type YoutubeWidgetProperties = z.input
diff --git a/package.json b/package.json
index 3a1d3c24..7d19316b 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@formatjs/intl": "^2.9.9",
"@google/model-viewer": "^3.4.0",
"@lukso/lsp-smart-contracts": "0.15.0-rc.5",
- "@lukso/web-components": "1.91.0",
+ "@lukso/web-components": "1.93.0",
"@nuxt/test-utils": "^3.13.1",
"@nuxtjs/algolia": "^1.10.2",
"@nuxtjs/device": "^3.1.1",
@@ -135,7 +135,8 @@
"web3-bzz": "1.10.4",
"web3-utils": "~1.10.4",
"web3-validator": "^2.0.4",
- "yata-fetch": "2.1.5"
+ "yata-fetch": "2.1.5",
+ "zod": "^3.23.8"
},
"resolutions": {
"@tanstack/query-core": "^5.20.5",
diff --git a/stores/grid.ts b/stores/grid.ts
index ac24b9f0..c2caac66 100644
--- a/stores/grid.ts
+++ b/stores/grid.ts
@@ -3,8 +3,8 @@ export const useGridStore = defineStore(
() => {
const isEditingGrid = ref(false)
const hasUnsavedGrid = ref(false)
- const viewedGrid = ref[]>([])
- const tempGrid = ref[]>([])
+ const viewedGrid = ref([])
+ const tempGrid = ref([])
const isSavingGrid = ref(false)
const selectedGridId = ref()
const gridRowHeightRatio = ref(DEFAULT_GRID_ROW_HEIGHT_RATIO)
diff --git a/translations/en_US.json b/translations/en_US.json
index a5033321..4a4491f4 100644
--- a/translations/en_US.json
+++ b/translations/en_US.json
@@ -43,7 +43,7 @@
"pages_terms_content": "PLEASE READ THESE TERMS OF USE BEFORE USING THE WEBSITE.
\nAcceptance of the Terms of Use
\n\nThese terms of use are entered into by and between you and the Universal Everything GmbH (\"Company\", \"we\" or \"us\"). The following terms and conditions, together with any documents they expressly incorporate by reference (collectively, these \"Terms of Use\"), govern your access to and use of lukso.network, including any content, functionality and services offered on or through lukso.network, new-creative-economies.foundation, and lukso.tech (together, the \"Website\").
\nNothing in these Terms creates any relationship of employment, agency, or partnership between the involved parties.
\nThis Website is provided by:
\n\nUniversal Everything GmbH
Köpenicker Chaussee 3a
10317 Berlin
\n\nPlease read the Terms of Use carefully before you start to use the Website. By using the Website or by clicking to accept or agree to the Terms of Use when this option is made available to you, you accept and agree to be bound and abide by these Terms of Use in addition to
\n- our Privacy Policy, incorporated herein by reference; and
- our Cookie Policy, incorporated herein by reference (note: no cookies are used on deposit.lukso.network).
\nIf you do not agree to these Terms of Use or the Privacy Policy, you must not access or use the Website.
- \n\n
Who May Use the Website
This Website is offered and available to users who are 18 years of age or older. The Website is not intended for children under 18 years of age. By using this Website, you represent and warrant that you (i) are 18 years of age or older, (ii) are not barred to use the Website under any applicable law, and (iii) are using the Website only for your own personal use. If you do not meet these requirements, you must not access or use the Website.
- \n\n
Changes to the Terms of Use
We may revise and update these Terms of Use from time to time in our sole discretion. All changes are effective immediately when we post them.
Your continued use of the Website following the posting of revised Terms of Use means that you accept and agree to the changes. You are expected to check this page frequently so you are aware of any changes, as they are binding on you.
- \n\n
Accessing the Website and Account Security
We reserve the right to withdraw or amend this Website, and any service or material we provide on the Website, in our sole discretion without notice. We do not guarantee that our site or any content on it, will always be available or be interrupted. We will not be liable if for any reason all or any part of the Website is unavailable at any time or for any period. From time to time, we may restrict access to some parts of the Website, or the entire Website, to users.
You are responsible for:
- Making all arrangements necessary for you to have access to the Website.
- Ensuring that all persons who access the Website through your internet connection are aware of these Terms of Use and comply with them.
To access the Website or some of the resources it offers, you may be asked to provide certain registration details or other information. It is a condition of your use of the Website that all the information you provide on the Website is correct, current and complete. You agree that all information you provide to register using this Website or otherwise, including, but not limited to, using any interactive features on the Website, is governed by our Privacy Policy, and you consent to all actions we take with respect to your information consistent with our Privacy Policy.
You should use particular caution when inputting personal information on to the Website on a public or shared computer so that others are not able to view or record your personal information.
- \n\n
Intellectual Property Rights
The Website and its entire contents, features and functionality (including but not limited to all information, software, text, displays, images, video and audio, and the design, selection and arrangement thereof), are owned by the Company, its licensors or other providers of such material and are protected by copyright, trademark, patent, trade secret and other intellectual property or proprietary rights laws.
Unless otherwise marked: (a) all material, data, and information on the Website, such as data files, text, music, audio files or other sounds, photographs, videos, or other images, but excluding any software or computer code (collectively, the “Non- Code Content”) is licensed under the Creative Commons Attribution 4.0 International License; and (b) all software or computer code (collectively, the “Code Content”) is licensed under the MIT License.
- \n\n
Trademarks
The Company name, the terms LUKSO, the Company logo and all related names, logos, product and service names, designs and slogans are trademarks of the Company or its affiliates or licensors. You must not use such marks without the prior written permission of the Company. All other names, logos, product and service names, designs and slogans on this Website are the trademarks of their respective owners.
- \n\n
Prohibited Uses
You may use the Website only for lawful purposes and in accordance with these Terms of Use. You agree not to use the Website:
- In any way that violates any applicable federal, state, local or international law or regulation (including, without limitation, any laws regarding the export of data or software to and from the US or other countries).
- For the purpose of exploiting, harming or attempting to exploit or harm minors in any way by exposing them to inappropriate content, asking for personally identifiable information or otherwise.
- To send, knowingly receive, upload, download, use or re-use any material which does not comply with these Terms of Use.
- To transmit, or procure the sending of, any advertising or promotional material without our prior written consent, including any \"junk mail\", \"chain letter\" or \"spam\" or any other similar solicitation.
- To impersonate or attempt to impersonate the Company, a Company employee, another user or any other person or entity (including, without limitation, by using e-mail addresses or screen names associated with any of the foregoing).
- To engage in any other conduct that restricts or inhibits anyone's use or enjoyment of the Website, or which, as determined by us, may harm the Company or users of the Website or expose them to liability.
Additionally, you agree not to:
- Use the Website in any manner that could disable, overburden, damage, or impair the site or interfere with any other party's use of the Website, including their ability to engage in real time activities through the Website.
- Use any robot, spider or other automatic device, process or means to access the Website for any purpose, including monitoring or copying any of the material on the Website.
- Use any manual process to monitor or copy any of the material on the Website or for any other unauthorized purpose without our prior written consent.
- Use any device, software or routine that interferes with the proper working of the Website.
- Introduce any viruses, trojan horses, worms, logic bombs or other material which is malicious or technologically harmful.
- Attempt to gain unauthorized access to, interfere with, damage or disrupt any parts of the Website, the server on which the Website is stored, or any server, computer or database connected to the Website.
- Attack the Website via a denial-of-service attack or a distributed denial-of-service attack.
- Otherwise attempt to interfere with the proper working of the Website.
- \n\n
Reliance on Information Posted
The information presented on or through the Website is made available solely for general information purposes. We do not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. We disclaim all liability and responsibility arising from any reliance placed on such materials by you or any other visitor to the Website, or by anyone who may be informed of any of its contents.
This Website includes content provided by third parties, including materials provided by other users, bloggers and third-party licensors, syndicators, aggregators and/or reporting services. All statements and/or opinions expressed in these materials, and all articles and responses to questions and other content, other than the content provided by the Company, are solely the opinions and the responsibility of the person or entity providing those materials. These materials do not necessarily reflect the opinion of the Company. We are not responsible, or liable to you or any third party, for the content or accuracy of any materials provided by any third parties.
- \n\n
Changes to the Website
We may update the content on this Website from time to time, but its content is not necessarily complete or up-to-date. Any of the material on the Website may be out of date at any given time, and we are under no obligation to update such material.
- \n\n
Information About You and Your Visits to the Website
All information we collect on this Website is subject to our Privacy Policy. By using the Website, you consent to all actions taken by us with respect to your information in compliance with the Privacy Policy.
- \n\n
Online Purchases and Other Terms and Conditions
Additional terms and conditions may also apply to specific portions, services or features of the Website, including the registration and sponsorship for conference events. All such additional terms and conditions are hereby incorporated by this reference into these Terms of Use. In the event of terms that are directly conflicting between these Terms of Use and terms of conditions for the registration or sponsorship of a conference event, the terms and conditions for the event shall be controlled.
- \n\n
Linking to the Website and Social Media Features
You may link to our homepage, provided you do so in a way that is fair and legal and does not damage our reputation or take advantage of it, but you must not establish a link in such a way as to suggest any form of association, approval or endorsement on our part without our express written consent.
- \n\n
Links from the Website
If the Website contains links to other sites and resources provided by third parties, these links are provided for your convenience only. This includes links contained in advertisements, including banner advertisements and sponsored links. We have no control over the contents of those sites or resources, and accept no responsibility for them or for any loss or damage that may arise from your use of them. If you decide to access any of the third party Website linked to this Website, you do so entirely at your own risk and subject to the terms and conditions of use for such Website. We reserve the right to withdraw linking permission without notice.
- \n\n
Geographic Restrictions
The owner of the Website is based in Switzerland. We make no claims that the Website or any of its content is accessible or appropriate outside of Switzerland. Access to the Website may not be legal by certain persons or in certain countries. If you access the Website from outside Switzerland, you do so on your own initiative and are responsible for compliance with local laws.
- \n\n
Disclaimer of Warranties
You understand that we cannot and do not guarantee or warrant that files available for downloading from the internet or the Website will be free of viruses or other destructive code. You are responsible for implementing sufficient procedures and checkpoints to satisfy your particular requirements for anti-virus protection and accuracy of data input and output, and for maintaining a means external to our site for any reconstruction of any lost data. WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE CAUSED BY A DISTRIBUTED DENIAL-OF-SERVICE ATTACK, VIRUSES OR OTHER TECHNOLOGICALLY HARMFUL MATERIAL THAT MAY INFECT YOUR COMPUTER EQUIPMENT, COMPUTER PROGRAMS, DATA OR OTHER PROPRIETARY MATERIAL DUE TO YOUR USE OF THE Website OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE Website OR TO YOUR DOWNLOADING OF ANY MATERIAL POSTED ON IT, OR ON ANY Website LINKED TO IT.
YOUR USE OF THE WEBSITE, ITS CONTENT AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE Website IS AT YOUR OWN RISK. THE WEBSITE, ITS CONTENT AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE Website ARE PROVIDED ON AN \"AS IS\" AND \"AS AVAILABLE\" BASIS, WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. NEITHER THE COMPANY NOR ANY PERSON ASSOCIATED WITH THE COMPANY MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY OR AVAILABILITY OF THE Website. WITHOUT LIMITING THE FOREGOING, NEITHER THE COMPANY NOR ANYONE ASSOCIATED WITH THE COMPANY REPRESENTS OR WARRANTS THAT THE WEBSITE, ITS CONTENT OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE Website WILL BE ACCURATE, RELIABLE, ERROR-FREE OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT OUR SITE OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS OR THAT THE Website OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE Website WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS.
THE COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT AND FITNESS FOR PARTICULAR PURPOSE.
SOME JURISDICTIONS DO NOT ALLOW EXCLUSION OF WARRANTIES OR LIMITATIONS ON THE DURATION OF IMPLIED WARRANTIES, SO THE ABOVE DISCLAIMER MAY NOT APPLY TO YOU IN THEIR ENTIRETIES BUT WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW.
- \n\n
Limitation on Liability
OUR LIABILITY IS EXCLUDED TO THE MAXIMUM EXTENT PERMITTED BY LAW. IN PARTICULAR, BUT WITHOUT LIMITATION, WE ARE NOT LIABLE FOR
- INDIRECT OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS, SAVINGS OR CLAIMS BY THIRD PARTIES
- ACTS OR OMISSIONS OF OUR AUXILIARY PERSONS AND SUBCONTRACTORS,
- SIMPLE NEGLIGENCE.
FURTHERMORE, THE WEBSITE IS MADE AVAILABLE \"AS IS\" AND, WE EXCLUDE ALL CONDITIONS, LIABILITIES, WARRANTIES, REPRESENTATIONS OR OTHER TERMS WHICH MAY APPLY TO OUR WEBSITE OR ANY CONTENT ON IT, WHETHER EXPRESS OR IMPLIED.
WE ALSO WILL NOT BE LIABLE TO ANY USER FOR ANY LOSS OR DAMAGE, WHETHER IN CONTRACT, TORT, BREACH OF STATUTORY DUTY, OR OTHERWISE, EVEN IF FORESEEABLE, ARISING UNDER OR IN CONNECTION WITH:
- USE OF, OR INABILITY TO USE, OUR WEBSITE; OR
- USE OF OR RELIANCE ON ANY CONTENT DISPLAYED ON OUR WEBSITE.
WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE CAUSED BY A VIRUS, DISTRIBUTED DENIAL-OF-SERVICE ATTACK, OR OTHER TECHNOLOGICALLY HARMFUL MATERIAL THAT MAY INFECT YOUR COMPUTER EQUIPMENT, COMPUTER PROGRAMS, DATA OR OTHER PROPRIETARY MATERIAL DUE TO YOUR USE OF OUR WEBSITE OR TO YOUR DOWNLOADING OF ANY CONTENT ON IT, OR ON ANY WEBSITE LINKED TO IT.
OUR WEBSITE MAY CONTAIN THIRD-PARTY CONTENT OR LINKS TO THIRD PARTY Website OR APPLICATIONS FOR YOUR CONVENIENCE ONLY. YOU UNDERSTAND THAT YOUR USE OF ANY THIRD PARTY WEBSITE OR APPLICATION IS SUBJECT TO ANY TERMS OF USE AND/OR PRIVACY NOTICES PROVIDED BY SUCH THIRD PARTY. WE ASSUME NO RESPONSIBILITY FOR THIRD-PARTY CONTENT OR LINKS TO THIRD PARTY Website OR APPLICATIONS TO WHICH WE LINK FROM OUR WEBSITE. SUCH LINKS SHOULD NOT BE INTERPRETED AS ENDORSEMENT BY US OF THOSE LINKED Website OR APPLICATIONS. WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE THAT MAY ARISE FROM YOUR USE OF THEM.
THE EXCLUSION OF LIABILITY UNDER THESE TERMS OF USE SHALL NOT APPLY IF THE DAMAGE IS CAUSED WILLFULLY OR BY GROSS NEGLIGENCE BY US OR UNLESS WE SHOULD BE LIABLE OTHERWISE PURSUANT TO MANDATORY LEGAL PROVISIONS SUCH AS PRODUCT LIABILITY PROVISIONS. MOREOVER, THE EXCLUSION OF LIABLITY DOES NOT APPLY TO DAMAGE FROM INJURY TO LIFE, BODY OR HEALTH (PERSONAL INJURIES).
- \n\n
Governing Law and Jurisdiction
All matters relating to the Website and these Terms of Use and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of Switzerland without giving effect to any choice or conflict of law provision or rule (whether of Switzerland or any other jurisdiction).
Any legal suit, action or proceeding arising out of, or related to, these Terms of Use or the Website shall be instituted exclusively in the Switzerland in the Kanton of Zug although we retain the right to bring any suit, action or proceeding against you for breach of these Terms of Use in your country of residence or any other relevant country. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.
- \n\n
Waiver and Severability
No waiver of by the Company of any term or condition set forth in these Terms of Use shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of the Company to assert a right or provision under these Terms of Use shall not constitute a waiver of such right or provision.
If any provision of these Terms of Use is held by a court or other tribunal of competent jurisdiction to be invalid, illegal or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of the Terms of Use will continue in full force and effect.
- \n\n
Entire Agreement
The Terms of Use, our Privacy Policy and terms of conditions for the registration of events constitute the sole and entire agreement between you and the Company with respect to the Website and supersede all prior and contemporaneous understandings, agreements, representations and warranties, both written and oral, with respect to the Website.
- \n\n
Your Comments and Concerns
This Website is operated by the Company. All other feedback, comments, requests for technical support and other communications relating to the Website should be directed to: support@universaleverything.io
",
"add_widget_warpcast_description": "Allows to embed Warpcast feed or post.",
"filters_no_options": "No options...",
- "grid_enable_edit_mode": "Enable Grid edit mode",
+ "grid_enable_edit_mode": "Enable edit mode",
"asset_creators_verified": "Verified creator",
"create_profile_box_button": "Create your profile",
"header_title": "UNIVERSAL\nPROFILES",
diff --git a/types/grid.ts b/types/grid.ts
index 23701d73..73202493 100644
--- a/types/grid.ts
+++ b/types/grid.ts
@@ -1,38 +1,13 @@
-import type { LayoutItem } from 'grid-layout-plus'
-
-export type Grid = {
- title: string
- grid: T[]
- id: string
- gridColumns: number
-}
-
-export type GridConfigItem = {
- width: number
- height: number
- type: GridWidgetType
- properties: GridWidgetProperties
-}
+import type { z } from 'zod'
+// TODO refactor parser into zod schema
export type GridWidgetExtended = {
type: GridWidgetType
- properties: GridWidgetProperties
+ properties: Record
originalWidth?: number
}
-export type GridWidget = LayoutItem & GridWidgetExtended
-
-export type GridWidgetWithoutCords = PartialBy
-
-export type GridWidgetType = keyof typeof GRID_WIDGET_TYPE
-
-export type GridWidgetProperties = Record
-
-export type GridWidgetProperty = {
- key: string
- type: 'string' | 'number' | 'boolean' | 'color' | 'url'
- optional?: boolean
-}
+export type GridWidgetType = z.input
export type GridWidgetChange = {
oldWidget: GridWidget | null
@@ -40,6 +15,6 @@ export type GridWidgetChange = {
}
export type GridChange = {
- oldGrid: Grid
- newGrid: Grid
+ oldGrid: Grid
+ newGrid: Grid
}
diff --git a/yarn.lock b/yarn.lock
index 5b4dbea4..55b1c186 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4723,9 +4723,9 @@ __metadata:
languageName: node
linkType: hard
-"@lukso/web-components@npm:1.91.0":
- version: 1.91.0
- resolution: "@lukso/web-components@npm:1.91.0"
+"@lukso/web-components@npm:1.93.0":
+ version: 1.93.0
+ resolution: "@lukso/web-components@npm:1.93.0"
dependencies:
clsx: "npm:^2.0.0"
ethereum-blockies-base64: "npm:^1.0.2"
@@ -4733,7 +4733,7 @@ __metadata:
tailwind-variants: "npm:^0.2.1"
tippy.js: "npm:^6.3.7"
web3-utils: "npm:^4.3.1"
- checksum: 5d27750457228c6acaad3182aa81fb745fa3b297cf7a720e4d306346ee2156179b28c9cd6ab12e4aa72ac95160e9b66e976b5eb903cc57d05125494c504e04cd
+ checksum: 03e9b3894ca2b749a2098e5c619afa499d34871ddf7da6854a2be5ed0777a6402694bc198dec195fa8cc58c6c058bae7d752d50e5c33efe95b78c50fe8d143e9
languageName: node
linkType: hard
@@ -24238,7 +24238,7 @@ __metadata:
"@formatjs/intl": "npm:^2.9.9"
"@google/model-viewer": "npm:^3.4.0"
"@lukso/lsp-smart-contracts": "npm:0.15.0-rc.5"
- "@lukso/web-components": "npm:1.91.0"
+ "@lukso/web-components": "npm:1.93.0"
"@nuxt/test-utils": "npm:^3.13.1"
"@nuxtjs/algolia": "npm:^1.10.2"
"@nuxtjs/device": "npm:^3.1.1"
@@ -24348,6 +24348,7 @@ __metadata:
workbox-precaching: "npm:^7.0.0"
workbox-routing: "npm:^7.0.0"
yata-fetch: "npm:2.1.5"
+ zod: "npm:^3.23.8"
languageName: unknown
linkType: soft
@@ -26599,3 +26600,10 @@ __metadata:
checksum: 73622ca36a916f785cf528fe612a884b3e0f183dbe6b33365a7d0fc92abdbedf7804c5e2bd8df0a278e1472106d46674281397a3dd800fa9031dc3429758c6ac
languageName: node
linkType: hard
+
+"zod@npm:^3.23.8":
+ version: 3.23.8
+ resolution: "zod@npm:3.23.8"
+ checksum: 846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1
+ languageName: node
+ linkType: hard