@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.
@@ -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
@@ -2,3 +2,4 @@ export * from './components/core/SEOImage';
2
2
  export * from './components/core/SEO';
3
3
  export * from './validators/utils';
4
4
  export * from './components/ui/DeployButton';
5
+ export * from './components/video/VideoSchemas';
package/lib/main.js CHANGED
@@ -3,3 +3,4 @@ export * from './components/core/SEO';
3
3
  // export * from './config/utils';
4
4
  export * from './validators/utils';
5
5
  export * from './components/ui/DeployButton';
6
+ export * from './components/video/VideoSchemas';
package/package.json CHANGED
@@ -1,19 +1,26 @@
1
1
  {
2
2
  "name": "@fils/sanity-components",
3
- "version": "0.0.9",
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
- "prepare": "yarn build",
10
+ "prepublishOnly": "yarn build",
12
11
  "build": "tsc"
13
12
  },
14
- "files": [
15
- "lib"
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
+ });
@@ -0,0 +1,8 @@
1
+ import { defineField } from "sanity";
2
+
3
+ export const SEOImage = defineField({
4
+ name: 'card',
5
+ type: 'image',
6
+ title: 'Sharing Image',
7
+ description: "Used on site embedding preview. Optimal size is at 1200px x 600px"
8
+ });