@fils/sanity-components 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/video/CreateVideoInput.d.ts +8 -0
- package/lib/components/video/CreateVideoInput.js +8 -0
- package/lib/components/video/VideoAndThumb.d.ts +7 -0
- package/lib/components/video/VideoAndThumb.js +101 -0
- package/lib/components/video/VideoSchemas.d.ts +58 -0
- package/lib/components/video/VideoSchemas.js +197 -0
- package/lib/main.d.ts +1 -0
- package/lib/main.js +1 -0
- package/package.json +13 -6
- package/src/components/core/SEO.ts +36 -0
- package/src/components/core/SEOImage.ts +8 -0
- package/src/components/ui/DeployButton.tsx +424 -0
- package/src/components/video/CreateVideoInput.tsx +26 -0
- package/src/components/video/VideoAndThumb.tsx +187 -0
- package/src/components/video/VideoSchemas.ts +205 -0
- package/src/config/utils.ts +86 -0
- package/src/main.ts +6 -0
- package/src/validators/utils.ts +17 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FileInputProps, ObjectInputProps } from 'sanity';
|
|
2
|
+
export interface VideoInputOptions {
|
|
3
|
+
/** Enable thumbnail generation feature */
|
|
4
|
+
enableThumbnailGeneration?: boolean;
|
|
5
|
+
/** Support URL input instead of file upload */
|
|
6
|
+
inputType?: 'file' | 'url';
|
|
7
|
+
}
|
|
8
|
+
export declare function createVideoInput(options?: VideoInputOptions): (props: FileInputProps | ObjectInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { VideoAndThumb } from './VideoAndThumb';
|
|
3
|
+
export function createVideoInput(options = {}) {
|
|
4
|
+
const { enableThumbnailGeneration = true, inputType = 'file' } = options;
|
|
5
|
+
return function VideoInput(props) {
|
|
6
|
+
return (_jsx(VideoAndThumb, { props: props, isURL: inputType === 'url', enableThumbnailGeneration: enableThumbnailGeneration }));
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ObjectInputProps } from 'sanity';
|
|
2
|
+
export interface VideoAndThumbProperties {
|
|
3
|
+
props: ObjectInputProps;
|
|
4
|
+
isURL: boolean;
|
|
5
|
+
enableThumbnailGeneration?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function VideoAndThumb(params: VideoAndThumbProperties): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDownIcon, ChevronRightIcon, DropIcon, WarningOutlineIcon } from '@sanity/icons';
|
|
3
|
+
import { Button, Card, Flex, Stack, Text } from '@sanity/ui';
|
|
4
|
+
import { useRef, useState } from 'react';
|
|
5
|
+
import { set, useClient } from 'sanity';
|
|
6
|
+
import { buildFileUrl, getFile } from '@sanity/asset-utils';
|
|
7
|
+
export function VideoAndThumb(params) {
|
|
8
|
+
const { props, isURL, enableThumbnailGeneration = true } = params;
|
|
9
|
+
const client = useClient({ apiVersion: '2024-01-01' });
|
|
10
|
+
// Get config from the client
|
|
11
|
+
const sanityConfig = {
|
|
12
|
+
projectId: client.config().projectId,
|
|
13
|
+
dataset: client.config().dataset
|
|
14
|
+
};
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
const videoRef = useRef(null);
|
|
17
|
+
const [warning, setWarning] = useState(null);
|
|
18
|
+
let url;
|
|
19
|
+
const videoMsg = isURL ? 'video URL' : 'video file';
|
|
20
|
+
if (isURL) {
|
|
21
|
+
//@ts-ignore
|
|
22
|
+
url = props.value ? props.value.video : "";
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// is file
|
|
26
|
+
const file = props.value && props.value.video && props.value.video.asset
|
|
27
|
+
? getFile(props.value.video.asset, sanityConfig)
|
|
28
|
+
: null;
|
|
29
|
+
url = file ? buildFileUrl(file.asset, sanityConfig) : "";
|
|
30
|
+
}
|
|
31
|
+
// Get all field members
|
|
32
|
+
const fieldMembers = props.members.filter(member => member.kind === 'field');
|
|
33
|
+
// URL/File Field
|
|
34
|
+
const videoField = fieldMembers.filter(member => member.name === 'video');
|
|
35
|
+
// Thumbnail Image Field
|
|
36
|
+
const thumb = fieldMembers.filter(member => member.name === 'image');
|
|
37
|
+
const generateFrame = async (video) => {
|
|
38
|
+
// Clear any existing warnings
|
|
39
|
+
setWarning(null);
|
|
40
|
+
if (video.videoWidth && video.videoHeight) {
|
|
41
|
+
try {
|
|
42
|
+
const can = document.createElement('canvas');
|
|
43
|
+
can.width = video.videoWidth;
|
|
44
|
+
can.height = video.videoHeight;
|
|
45
|
+
const ctx = can.getContext('2d');
|
|
46
|
+
ctx?.drawImage(video, 0, 0);
|
|
47
|
+
can.toBlob(async (blob) => {
|
|
48
|
+
if (!blob) {
|
|
49
|
+
setWarning('Failed to generate image from video frame');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// Use the studio's authenticated client
|
|
54
|
+
const asset = await client.assets.upload('image', blob, {
|
|
55
|
+
filename: `video-frame-${Date.now()}.png`,
|
|
56
|
+
title: 'Generated from video frame'
|
|
57
|
+
});
|
|
58
|
+
// Create image reference
|
|
59
|
+
const imageReference = {
|
|
60
|
+
_type: 'image',
|
|
61
|
+
asset: {
|
|
62
|
+
_type: 'reference',
|
|
63
|
+
_ref: asset._id
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Update your object with the new image
|
|
67
|
+
props.onChange([
|
|
68
|
+
set(imageReference, ['image'])
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
catch (uploadError) {
|
|
72
|
+
console.error('Failed to upload image:', uploadError);
|
|
73
|
+
setWarning('Failed to upload generated frame to Sanity');
|
|
74
|
+
}
|
|
75
|
+
}, "image/png");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Error generating frame:', error);
|
|
79
|
+
setWarning('Failed to generate frame from video');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
setWarning(`Video has no image data. Please make sure a ${videoMsg} is properly set and preview image is visible.`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const handleGenerateFromFrame = () => {
|
|
87
|
+
if (videoRef.current) {
|
|
88
|
+
generateFrame(videoRef.current);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
setWarning(`Video element not found. Please make sure a ${videoMsg} is set and video preview unfolded.`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return (_jsxs(Stack, { space: 1, children: [_jsx("style", { children: ` video { width: 100%; } ` }), _jsx(_Fragment, { children: props.renderDefault({
|
|
95
|
+
...props,
|
|
96
|
+
members: videoField
|
|
97
|
+
}) }), url && (_jsx(Card, { children: _jsxs(Stack, { space: 2, children: [_jsx(Button, { mode: "bleed", justify: "flex-start", onClick: () => setIsOpen(!isOpen), padding: 2, children: _jsxs(Flex, { align: "center", gap: 2, children: [isOpen ? _jsx(ChevronDownIcon, {}) : _jsx(ChevronRightIcon, {}), _jsx(Text, { size: 1, weight: "medium", children: isOpen ? 'Hide Preview' : 'Show Preview' })] }) }), isOpen && (_jsx(Card, { padding: 2, border: true, children: _jsx("video", { ref: videoRef, src: url, muted: true, loop: true, autoPlay: true, controls: true, crossOrigin: "anonymous" }) }))] }) })), _jsx(_Fragment, { children: props.renderDefault({
|
|
98
|
+
...props,
|
|
99
|
+
members: thumb
|
|
100
|
+
}) }), enableThumbnailGeneration && (_jsx(Button, { fontSize: [2, 2, 3], icon: DropIcon, mode: "ghost", tone: "positive", text: "Generate from Video Frame", onClick: handleGenerateFromFrame })), warning && (_jsx(Card, { padding: 3, tone: "caution", border: true, children: _jsxs(Stack, { space: 2, children: [_jsxs(Text, { size: 1, weight: "medium", children: [_jsx(WarningOutlineIcon, { style: { marginRight: '8px' } }), "Warning"] }), _jsx(Text, { size: 1, children: warning })] }) }))] }));
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video File Schema
|
|
3
|
+
* Document type for uploaded video files with thumbnail generation
|
|
4
|
+
*/
|
|
5
|
+
export declare const VideoFile: {
|
|
6
|
+
type: "document";
|
|
7
|
+
name: "videoFile";
|
|
8
|
+
} & Omit<import("sanity").DocumentDefinition, "preview"> & {
|
|
9
|
+
preview?: import("sanity").PreviewConfig<{
|
|
10
|
+
video: string;
|
|
11
|
+
media: string;
|
|
12
|
+
}, Record<"media" | "video", any>> | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Video URL Schema
|
|
16
|
+
* Document type for external video URLs (e.g., Vimeo, YouTube)
|
|
17
|
+
*/
|
|
18
|
+
export declare const VideoURL: {
|
|
19
|
+
type: "document";
|
|
20
|
+
name: "videoUrl";
|
|
21
|
+
} & Omit<import("sanity").DocumentDefinition, "preview"> & {
|
|
22
|
+
preview?: import("sanity").PreviewConfig<{
|
|
23
|
+
video: string;
|
|
24
|
+
media: string;
|
|
25
|
+
}, Record<"media" | "video", any>> | undefined;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Video File Object Schema
|
|
29
|
+
* Object type for use as a field in other schemas
|
|
30
|
+
*/
|
|
31
|
+
export declare const VideoFileObject: {
|
|
32
|
+
type: "object";
|
|
33
|
+
name: "videoFileObject";
|
|
34
|
+
} & Omit<import("sanity").ObjectDefinition, "preview"> & {
|
|
35
|
+
preview?: import("sanity").PreviewConfig<Record<string, string>, Record<never, any>> | undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Video URL Object Schema
|
|
39
|
+
* Object type for use as a field in other schemas (URL version)
|
|
40
|
+
*/
|
|
41
|
+
export declare const VideoURLObject: {
|
|
42
|
+
type: "object";
|
|
43
|
+
name: "videoUrlObject";
|
|
44
|
+
} & Omit<import("sanity").ObjectDefinition, "preview"> & {
|
|
45
|
+
preview?: import("sanity").PreviewConfig<Record<string, string>, Record<never, any>> | undefined;
|
|
46
|
+
};
|
|
47
|
+
export declare const VideoFileNoThumb: {
|
|
48
|
+
type: "object";
|
|
49
|
+
name: "videoFileNoThumb";
|
|
50
|
+
} & Omit<import("sanity").ObjectDefinition, "preview"> & {
|
|
51
|
+
preview?: import("sanity").PreviewConfig<Record<string, string>, Record<never, any>> | undefined;
|
|
52
|
+
};
|
|
53
|
+
export declare const VideoURLNoThumb: {
|
|
54
|
+
type: "object";
|
|
55
|
+
name: "videoFileNoThumb";
|
|
56
|
+
} & Omit<import("sanity").ObjectDefinition, "preview"> & {
|
|
57
|
+
preview?: import("sanity").PreviewConfig<Record<string, string>, Record<never, any>> | undefined;
|
|
58
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { LinkIcon, PlayIcon, VideoIcon } from "@sanity/icons";
|
|
2
|
+
import { defineField, defineType } from "sanity";
|
|
3
|
+
import { createVideoInput } from "./CreateVideoInput";
|
|
4
|
+
/**
|
|
5
|
+
* Video File Schema
|
|
6
|
+
* Document type for uploaded video files with thumbnail generation
|
|
7
|
+
*/
|
|
8
|
+
export const VideoFile = defineType({
|
|
9
|
+
name: 'videoFile',
|
|
10
|
+
type: 'document',
|
|
11
|
+
title: 'Video File',
|
|
12
|
+
description: 'Video File + Thumbnail',
|
|
13
|
+
icon: VideoIcon,
|
|
14
|
+
fields: [
|
|
15
|
+
defineField({
|
|
16
|
+
name: 'video',
|
|
17
|
+
type: 'file',
|
|
18
|
+
options: {
|
|
19
|
+
accept: "video/mp4, video/webm"
|
|
20
|
+
},
|
|
21
|
+
icon: PlayIcon,
|
|
22
|
+
description: 'Video File. Supported formats: MP4 & WebM',
|
|
23
|
+
validation: Rule => Rule.required()
|
|
24
|
+
}),
|
|
25
|
+
defineField({
|
|
26
|
+
name: 'image',
|
|
27
|
+
title: 'Video Thumbnail',
|
|
28
|
+
type: 'image',
|
|
29
|
+
validation: Rule => Rule.required()
|
|
30
|
+
})
|
|
31
|
+
],
|
|
32
|
+
components: {
|
|
33
|
+
input: createVideoInput({
|
|
34
|
+
inputType: 'file',
|
|
35
|
+
enableThumbnailGeneration: true
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
preview: {
|
|
39
|
+
select: {
|
|
40
|
+
video: 'video',
|
|
41
|
+
media: 'image'
|
|
42
|
+
},
|
|
43
|
+
prepare(selected) {
|
|
44
|
+
const { video, media } = selected;
|
|
45
|
+
// Just use the original filename from the asset if available
|
|
46
|
+
const filename = video?.asset?._ref
|
|
47
|
+
? video.asset._ref.split('-').slice(1, -1).join('-')
|
|
48
|
+
: 'No video';
|
|
49
|
+
return {
|
|
50
|
+
title: filename,
|
|
51
|
+
media
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Video URL Schema
|
|
58
|
+
* Document type for external video URLs (e.g., Vimeo, YouTube)
|
|
59
|
+
*/
|
|
60
|
+
export const VideoURL = defineType({
|
|
61
|
+
name: 'videoUrl',
|
|
62
|
+
type: 'document',
|
|
63
|
+
title: 'Video URL',
|
|
64
|
+
description: 'External video URL + Thumbnail',
|
|
65
|
+
icon: VideoIcon,
|
|
66
|
+
fields: [
|
|
67
|
+
defineField({
|
|
68
|
+
name: 'video',
|
|
69
|
+
type: 'url',
|
|
70
|
+
icon: LinkIcon,
|
|
71
|
+
description: 'Video URL (e.g., Vimeo, YouTube)',
|
|
72
|
+
validation: Rule => Rule.required().uri({
|
|
73
|
+
scheme: ['http', 'https']
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
defineField({
|
|
77
|
+
name: 'image',
|
|
78
|
+
title: 'Video Thumbnail',
|
|
79
|
+
type: 'image',
|
|
80
|
+
validation: Rule => Rule.required()
|
|
81
|
+
})
|
|
82
|
+
],
|
|
83
|
+
components: {
|
|
84
|
+
input: createVideoInput({
|
|
85
|
+
inputType: 'url',
|
|
86
|
+
enableThumbnailGeneration: false // Can't generate from external URLs
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
preview: {
|
|
90
|
+
select: {
|
|
91
|
+
video: 'video',
|
|
92
|
+
media: 'image'
|
|
93
|
+
},
|
|
94
|
+
prepare(selected) {
|
|
95
|
+
const { video, media } = selected;
|
|
96
|
+
return {
|
|
97
|
+
title: video || 'No URL',
|
|
98
|
+
media
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Video File Object Schema
|
|
105
|
+
* Object type for use as a field in other schemas
|
|
106
|
+
*/
|
|
107
|
+
export const VideoFileObject = defineType({
|
|
108
|
+
name: 'videoFileObject',
|
|
109
|
+
type: 'object',
|
|
110
|
+
title: 'Video with Thumbnail',
|
|
111
|
+
fields: [
|
|
112
|
+
defineField({
|
|
113
|
+
name: 'video',
|
|
114
|
+
type: 'file',
|
|
115
|
+
options: {
|
|
116
|
+
accept: "video/mp4, video/webm"
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
defineField({
|
|
120
|
+
name: 'image',
|
|
121
|
+
title: 'Thumbnail',
|
|
122
|
+
type: 'image'
|
|
123
|
+
})
|
|
124
|
+
],
|
|
125
|
+
components: {
|
|
126
|
+
input: createVideoInput({
|
|
127
|
+
inputType: 'file',
|
|
128
|
+
enableThumbnailGeneration: true
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* Video URL Object Schema
|
|
134
|
+
* Object type for use as a field in other schemas (URL version)
|
|
135
|
+
*/
|
|
136
|
+
export const VideoURLObject = defineType({
|
|
137
|
+
name: 'videoUrlObject',
|
|
138
|
+
type: 'object',
|
|
139
|
+
title: 'Video URL with Thumbnail',
|
|
140
|
+
fields: [
|
|
141
|
+
defineField({
|
|
142
|
+
name: 'video',
|
|
143
|
+
type: 'url'
|
|
144
|
+
}),
|
|
145
|
+
defineField({
|
|
146
|
+
name: 'image',
|
|
147
|
+
title: 'Thumbnail',
|
|
148
|
+
type: 'image'
|
|
149
|
+
})
|
|
150
|
+
],
|
|
151
|
+
components: {
|
|
152
|
+
input: createVideoInput({
|
|
153
|
+
inputType: 'url',
|
|
154
|
+
enableThumbnailGeneration: false
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
export const VideoFileNoThumb = defineType({
|
|
159
|
+
name: 'videoFileNoThumb',
|
|
160
|
+
type: 'object',
|
|
161
|
+
title: 'Video File',
|
|
162
|
+
fields: [
|
|
163
|
+
defineField({
|
|
164
|
+
name: 'video',
|
|
165
|
+
type: 'file',
|
|
166
|
+
options: {
|
|
167
|
+
accept: 'video/mp4, video/webm'
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
],
|
|
171
|
+
components: {
|
|
172
|
+
input: createVideoInput({
|
|
173
|
+
inputType: 'file',
|
|
174
|
+
enableThumbnailGeneration: false
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
export const VideoURLNoThumb = defineType({
|
|
179
|
+
name: 'videoFileNoThumb',
|
|
180
|
+
type: 'object',
|
|
181
|
+
title: 'Video URL',
|
|
182
|
+
fields: [
|
|
183
|
+
defineField({
|
|
184
|
+
name: 'video',
|
|
185
|
+
type: 'file',
|
|
186
|
+
options: {
|
|
187
|
+
accept: 'video/mp4, video/webm'
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
],
|
|
191
|
+
components: {
|
|
192
|
+
input: createVideoInput({
|
|
193
|
+
inputType: 'url',
|
|
194
|
+
enableThumbnailGeneration: false
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
});
|
package/lib/main.d.ts
CHANGED
package/lib/main.js
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fils/sanity-components",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Fil's Components for Sanity Back-Ends",
|
|
5
|
-
"main": "lib/main.js",
|
|
6
5
|
"repository": "git@github.com:fil-studio/fils.git",
|
|
7
6
|
"author": "Fil Studio <hello@fil.studio>",
|
|
8
7
|
"license": "Apache-2.0",
|
|
9
8
|
"private": false,
|
|
10
9
|
"scripts": {
|
|
11
|
-
"
|
|
10
|
+
"prepublishOnly": "yarn build",
|
|
12
11
|
"build": "tsc"
|
|
13
12
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
"main": "./lib/main.js",
|
|
14
|
+
"types": "./lib/main.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./lib/main.js",
|
|
18
|
+
"types": "./lib/main.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
17
24
|
"peerDependencies": {
|
|
18
25
|
"@sanity/dashboard": "^4.1.3",
|
|
19
26
|
"sanity": "^3.85.1",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
import { SEOImage } from "./SEOImage";
|
|
3
|
+
|
|
4
|
+
export const SEO = defineType({
|
|
5
|
+
name: 'seo',
|
|
6
|
+
title: "SEO",
|
|
7
|
+
type: "object",
|
|
8
|
+
fields: [
|
|
9
|
+
defineField({
|
|
10
|
+
name: 'title',
|
|
11
|
+
type: 'string'
|
|
12
|
+
}),
|
|
13
|
+
defineField({
|
|
14
|
+
name: 'description',
|
|
15
|
+
type: 'text'
|
|
16
|
+
}),
|
|
17
|
+
SEOImage
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const LocalizedSEO = defineType({
|
|
22
|
+
name: 'localseo',
|
|
23
|
+
title: "SEO",
|
|
24
|
+
type: "object",
|
|
25
|
+
fields: [
|
|
26
|
+
defineField({
|
|
27
|
+
name: 'title',
|
|
28
|
+
type: 'internationalizedArrayString'
|
|
29
|
+
}),
|
|
30
|
+
defineField({
|
|
31
|
+
name: 'description',
|
|
32
|
+
type: 'internationalizedArrayText'
|
|
33
|
+
}),
|
|
34
|
+
SEOImage
|
|
35
|
+
]
|
|
36
|
+
});
|