@fils/sanity-components 0.0.8 → 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/ui/DeployButton.d.ts +23 -0
- package/lib/components/ui/DeployButton.js +225 -0
- package/lib/components/ui/DeployButton.jsx.d.ts +15 -0
- package/lib/components/ui/DeployButton.jsx.js +249 -0
- 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 +2 -0
- package/lib/main.js +2 -0
- package/package.json +41 -10
- 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,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fil's Hosted Deploy Button
|
|
3
|
+
* Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
|
|
4
|
+
* Recommended to use inside the dashboard
|
|
5
|
+
* Uses studio secrets to store Github token
|
|
6
|
+
*/
|
|
7
|
+
interface DeployButtonConfig {
|
|
8
|
+
owner: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
branch?: string;
|
|
12
|
+
eventType?: string;
|
|
13
|
+
}
|
|
14
|
+
interface DeployButtonProps {
|
|
15
|
+
config: DeployButtonConfig;
|
|
16
|
+
title?: string;
|
|
17
|
+
deploymentUrl?: string;
|
|
18
|
+
environment?: string;
|
|
19
|
+
secretsNamespace?: string;
|
|
20
|
+
secretKey?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const DeployButton: ({ config, title, deploymentUrl, environment, secretsNamespace, secretKey }: DeployButtonProps) => import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export default DeployButton;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { Button, Card, Stack, Text, Spinner, Flex } from '@sanity/ui';
|
|
4
|
+
import { PlayIcon, CheckmarkIcon, CloseIcon } from '@sanity/icons';
|
|
5
|
+
import { useSecrets, SettingsView } from '@sanity/studio-secrets';
|
|
6
|
+
const DeployButton = ({ config, title = "🚀 Site Deployment", deploymentUrl, environment = "production", secretsNamespace = "deployButton", secretKey = "github_token" }) => {
|
|
7
|
+
// ... rest of code
|
|
8
|
+
// ALL HOOKS MUST BE CALLED FIRST - NEVER CONDITIONALLY
|
|
9
|
+
const [deployState, setDeployState] = useState('idle');
|
|
10
|
+
const [message, setMessage] = useState('');
|
|
11
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
12
|
+
const intervalRef = useRef(null);
|
|
13
|
+
const monitoringRef = useRef(false);
|
|
14
|
+
// Use Sanity secrets - ALWAYS call this hook
|
|
15
|
+
const { secrets } = useSecrets(secretsNamespace);
|
|
16
|
+
const token = secrets?.[secretKey];
|
|
17
|
+
// Get the effective token (secrets take priority over config)
|
|
18
|
+
const effectiveToken = token || config?.token;
|
|
19
|
+
// Only show settings if we don't have a token AND we have valid config
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!effectiveToken && config?.owner && config?.repo && !showSettings) {
|
|
22
|
+
setShowSettings(true);
|
|
23
|
+
}
|
|
24
|
+
else if (effectiveToken && showSettings) {
|
|
25
|
+
// Close settings if we now have a token
|
|
26
|
+
setShowSettings(false);
|
|
27
|
+
}
|
|
28
|
+
}, [effectiveToken, config?.owner, config?.repo, showSettings]);
|
|
29
|
+
// Clean up on unmount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
if (intervalRef.current) {
|
|
33
|
+
clearInterval(intervalRef.current);
|
|
34
|
+
intervalRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
// Plugin config for secrets
|
|
39
|
+
const pluginConfigKeys = [
|
|
40
|
+
{
|
|
41
|
+
key: secretKey,
|
|
42
|
+
title: `GitHub Token for ${environment}`,
|
|
43
|
+
description: `Personal access token for ${config?.owner}/${config?.repo} deployment`,
|
|
44
|
+
type: 'string',
|
|
45
|
+
inputType: 'password'
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
// Poll workflow status
|
|
49
|
+
const pollWorkflowStatus = async (runId) => {
|
|
50
|
+
try {
|
|
51
|
+
console.log('Polling workflow:', runId);
|
|
52
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs/${runId}`, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Authorization': `token ${effectiveToken}`,
|
|
55
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`HTTP ${response.status}`);
|
|
60
|
+
}
|
|
61
|
+
const run = await response.json();
|
|
62
|
+
console.log('Run status:', run.status, 'conclusion:', run.conclusion);
|
|
63
|
+
if (run.status === 'completed') {
|
|
64
|
+
// Stop polling
|
|
65
|
+
if (intervalRef.current) {
|
|
66
|
+
clearInterval(intervalRef.current);
|
|
67
|
+
intervalRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
monitoringRef.current = false;
|
|
70
|
+
if (run.conclusion === 'success') {
|
|
71
|
+
setDeployState('success');
|
|
72
|
+
setMessage('🎉 Deployment completed successfully!');
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setDeployState('error');
|
|
76
|
+
setMessage(`❌ Deployment failed: ${run.conclusion}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (run.status === 'in_progress' || run.status === 'queued') {
|
|
80
|
+
const startTime = new Date(run.run_started_at || run.created_at);
|
|
81
|
+
const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000);
|
|
82
|
+
setDeployState('deploying');
|
|
83
|
+
setMessage(`🔄 Deployment running... (${elapsed}s)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Polling error:', error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
// Find the workflow run
|
|
91
|
+
const findWorkflowRun = async () => {
|
|
92
|
+
try {
|
|
93
|
+
console.log('Looking for workflow run...');
|
|
94
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs?per_page=10`, {
|
|
95
|
+
headers: {
|
|
96
|
+
'Authorization': `token ${effectiveToken}`,
|
|
97
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`HTTP ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
console.log('Found runs:', data.workflow_runs.length);
|
|
105
|
+
// Find the most recent run (within last 90 seconds)
|
|
106
|
+
const recentRun = data.workflow_runs.find(run => {
|
|
107
|
+
const ageInSeconds = (Date.now() - new Date(run.created_at).getTime()) / 1000;
|
|
108
|
+
console.log(`Run ${run.id}: age ${ageInSeconds}s, event: ${run.event}, status: ${run.status}`);
|
|
109
|
+
return ageInSeconds < 90;
|
|
110
|
+
});
|
|
111
|
+
if (recentRun) {
|
|
112
|
+
console.log('Found recent run:', recentRun.id, 'Event:', recentRun.event);
|
|
113
|
+
setDeployState('deploying');
|
|
114
|
+
setMessage('🚀 Deployment started...');
|
|
115
|
+
// Start polling every 5 seconds
|
|
116
|
+
intervalRef.current = setInterval(() => {
|
|
117
|
+
pollWorkflowStatus(recentRun.id);
|
|
118
|
+
}, 5000);
|
|
119
|
+
// Initial poll
|
|
120
|
+
pollWorkflowStatus(recentRun.id);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.log('No recent workflow found');
|
|
124
|
+
setDeployState('error');
|
|
125
|
+
setMessage('❌ Could not find workflow run - check if workflow exists');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error('Error finding workflow:', error);
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
131
|
+
setDeployState('error');
|
|
132
|
+
setMessage(`❌ Error finding workflow: ${errorMessage}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const triggerDeploy = async () => {
|
|
136
|
+
// Prevent multiple deployments
|
|
137
|
+
if (deployState !== 'idle' && deployState !== 'success' && deployState !== 'error')
|
|
138
|
+
return;
|
|
139
|
+
setDeployState('triggering');
|
|
140
|
+
setMessage('⚡ Triggering deployment...');
|
|
141
|
+
try {
|
|
142
|
+
console.log('Triggering deployment...');
|
|
143
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/dispatches`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Authorization': `token ${effectiveToken}`,
|
|
147
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
148
|
+
'Content-Type': 'application/json'
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
event_type: config.eventType || 'deploy-site',
|
|
152
|
+
client_payload: {
|
|
153
|
+
environment: environment,
|
|
154
|
+
triggered_by: 'sanity_studio',
|
|
155
|
+
timestamp: new Date().toISOString()
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
console.log('Deployment triggered successfully');
|
|
161
|
+
setMessage('✅ Deployment triggered! Looking for workflow...');
|
|
162
|
+
// Wait 5 seconds then look for the workflow
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
if (!monitoringRef.current) {
|
|
165
|
+
monitoringRef.current = true;
|
|
166
|
+
findWorkflowRun();
|
|
167
|
+
}
|
|
168
|
+
}, 5000);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const error = await response.json();
|
|
172
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error('Deploy trigger failed:', error);
|
|
177
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
178
|
+
setDeployState('error');
|
|
179
|
+
setMessage(`❌ Failed to trigger: ${errorMessage}`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const getButtonProps = () => {
|
|
183
|
+
switch (deployState) {
|
|
184
|
+
case 'triggering':
|
|
185
|
+
return { tone: 'primary', disabled: true, icon: Spinner, text: 'Triggering...' };
|
|
186
|
+
case 'deploying':
|
|
187
|
+
return { tone: 'primary', disabled: true, icon: Spinner, text: 'Deploying...' };
|
|
188
|
+
case 'success':
|
|
189
|
+
return { tone: 'positive', disabled: false, icon: CheckmarkIcon, text: 'Deploy Again' };
|
|
190
|
+
case 'error':
|
|
191
|
+
return { tone: 'critical', disabled: false, icon: CloseIcon, text: 'Try Again' };
|
|
192
|
+
default:
|
|
193
|
+
return { tone: 'primary', disabled: false, icon: PlayIcon, text: 'Deploy Site' };
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
// NOW WE CAN HANDLE CONDITIONAL RENDERING AFTER ALL HOOKS
|
|
197
|
+
// Check for missing config
|
|
198
|
+
if (!config || !config.owner || !config.repo) {
|
|
199
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Deploy Button Configuration Error" }), _jsx(Text, { size: 1, children: "Missing required config: owner and repo are required" })] }) }));
|
|
200
|
+
}
|
|
201
|
+
// If showing settings, render the settings view
|
|
202
|
+
if (showSettings && !effectiveToken) {
|
|
203
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsxs(Text, { size: 2, weight: "semibold", children: ["\uD83D\uDD10 ", title, " - Setup Required"] }), _jsx(Text, { size: 1, muted: true, children: "Please configure your GitHub token to enable deployments." }), _jsx(SettingsView, { title: `${title} Configuration`, namespace: secretsNamespace, keys: pluginConfigKeys, onClose: () => {
|
|
204
|
+
setShowSettings(false);
|
|
205
|
+
} })] }) }));
|
|
206
|
+
}
|
|
207
|
+
// Check for missing token after settings
|
|
208
|
+
if (!effectiveToken) {
|
|
209
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Authentication Required" }), _jsx(Text, { size: 1, children: "No GitHub token configured." }), _jsx(Button, { tone: "primary", mode: "ghost", size: 1, onClick: () => setShowSettings(true), children: "Configure Token" })] }) }));
|
|
210
|
+
}
|
|
211
|
+
const buttonProps = getButtonProps();
|
|
212
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: title }), _jsx(Flex, { justify: "flex-start", children: _jsx(Button, { tone: buttonProps.tone, disabled: buttonProps.disabled, onClick: triggerDeploy, size: 3, style: {
|
|
213
|
+
width: '100%',
|
|
214
|
+
maxWidth: '360px',
|
|
215
|
+
}, children: _jsxs(Flex, { align: "center", justify: "center", style: {
|
|
216
|
+
gap: '6px',
|
|
217
|
+
padding: '16px 0px 16px 0px'
|
|
218
|
+
}, children: [buttonProps.icon && (_jsx("span", { style: {
|
|
219
|
+
display: 'flex',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
fontSize: '16px',
|
|
222
|
+
lineHeight: '1'
|
|
223
|
+
}, children: _jsx(buttonProps.icon, {}) })), _jsx(Text, { size: 2, weight: "medium", style: { lineHeight: '1' }, children: buttonProps.text })] }) }) }), message && (_jsx(Card, { padding: 3, radius: 1, tone: deployState === 'error' ? 'critical' : deployState === 'success' ? 'positive' : 'primary', children: _jsx(Text, { size: 1, children: message }) })), deploymentUrl && deployState === 'success' && (_jsx(Button, { as: "a", href: deploymentUrl, target: "_blank", rel: "noopener noreferrer", tone: "primary", mode: "ghost", size: 3, children: "\uD83C\uDF10 View Live Site" })), _jsxs(Text, { size: 1, muted: true, children: ["Deploys from ", config.branch || 'main', " branch to ", environment] })] }) }));
|
|
224
|
+
};
|
|
225
|
+
export default DeployButton;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fil's Hosted Deploy Button
|
|
3
|
+
* Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
|
|
4
|
+
* Recommended to use inside the dashboard
|
|
5
|
+
* Uses studio secrets to store Github token
|
|
6
|
+
*/
|
|
7
|
+
declare const DeployButton: ({ config, title, deploymentUrl, environment, secretsNamespace, secretKey }: {
|
|
8
|
+
config: any;
|
|
9
|
+
title?: string | undefined;
|
|
10
|
+
deploymentUrl: any;
|
|
11
|
+
environment?: string | undefined;
|
|
12
|
+
secretsNamespace?: string | undefined;
|
|
13
|
+
secretKey?: string | undefined;
|
|
14
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export default DeployButton;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Fil's Hosted Deploy Button
|
|
4
|
+
* Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
|
|
5
|
+
* Recommended to use inside the dashboard
|
|
6
|
+
* Uses studio secrets to store Github token
|
|
7
|
+
*/
|
|
8
|
+
//@ts-ignore
|
|
9
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
10
|
+
import { Button, Card, Stack, Text, Spinner, Flex } from '@sanity/ui';
|
|
11
|
+
import { PlayIcon, CheckmarkIcon, CloseIcon } from '@sanity/icons';
|
|
12
|
+
import { useSecrets, SettingsView } from '@sanity/studio-secrets';
|
|
13
|
+
const DeployButton = ({
|
|
14
|
+
//@ts-ignore
|
|
15
|
+
config, title = "🚀 Site Deployment",
|
|
16
|
+
//@ts-ignore
|
|
17
|
+
deploymentUrl, environment = "production", secretsNamespace = "deployButton", secretKey = "github_token" }) => {
|
|
18
|
+
// ALL HOOKS MUST BE CALLED FIRST - NEVER CONDITIONALLY
|
|
19
|
+
const [deployState, setDeployState] = useState('idle');
|
|
20
|
+
const [message, setMessage] = useState('');
|
|
21
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
22
|
+
const intervalRef = useRef(null);
|
|
23
|
+
const monitoringRef = useRef(false);
|
|
24
|
+
// Use Sanity secrets - ALWAYS call this hook
|
|
25
|
+
const { secrets } = useSecrets(secretsNamespace);
|
|
26
|
+
//@ts-ignore
|
|
27
|
+
const token = secrets?.[secretKey];
|
|
28
|
+
// Get the effective token (secrets take priority over config)
|
|
29
|
+
const effectiveToken = token || config?.token;
|
|
30
|
+
// Only show settings if we don't have a token AND we have valid config
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!effectiveToken && config?.owner && config?.repo && !showSettings) {
|
|
33
|
+
setShowSettings(true);
|
|
34
|
+
}
|
|
35
|
+
else if (effectiveToken && showSettings) {
|
|
36
|
+
// Close settings if we now have a token
|
|
37
|
+
setShowSettings(false);
|
|
38
|
+
}
|
|
39
|
+
}, [effectiveToken, config?.owner, config?.repo, showSettings]);
|
|
40
|
+
// Clean up on unmount
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
if (intervalRef.current) {
|
|
44
|
+
clearInterval(intervalRef.current);
|
|
45
|
+
intervalRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
// Plugin config for secrets
|
|
50
|
+
const pluginConfigKeys = [
|
|
51
|
+
{
|
|
52
|
+
key: secretKey,
|
|
53
|
+
title: `GitHub Token for ${environment}`,
|
|
54
|
+
description: `Personal access token for ${config?.owner}/${config?.repo} deployment`,
|
|
55
|
+
type: 'string',
|
|
56
|
+
inputType: 'password' // This should hide the token
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
// Poll workflow status
|
|
60
|
+
//@ts-ignore
|
|
61
|
+
const pollWorkflowStatus = async (runId) => {
|
|
62
|
+
try {
|
|
63
|
+
console.log('Polling workflow:', runId);
|
|
64
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs/${runId}`, {
|
|
65
|
+
headers: {
|
|
66
|
+
'Authorization': `token ${effectiveToken}`,
|
|
67
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`HTTP ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
const run = await response.json();
|
|
74
|
+
console.log('Run status:', run.status, 'conclusion:', run.conclusion);
|
|
75
|
+
if (run.status === 'completed') {
|
|
76
|
+
// Stop polling
|
|
77
|
+
if (intervalRef.current) {
|
|
78
|
+
clearInterval(intervalRef.current);
|
|
79
|
+
intervalRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
monitoringRef.current = false;
|
|
82
|
+
if (run.conclusion === 'success') {
|
|
83
|
+
setDeployState('success');
|
|
84
|
+
setMessage('🎉 Deployment completed successfully!');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
setDeployState('error');
|
|
88
|
+
setMessage(`❌ Deployment failed: ${run.conclusion}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (run.status === 'in_progress' || run.status === 'queued') {
|
|
92
|
+
const startTime = new Date(run.run_started_at || run.created_at);
|
|
93
|
+
const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000);
|
|
94
|
+
setDeployState('deploying');
|
|
95
|
+
setMessage(`🔄 Deployment running... (${elapsed}s)`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error('Polling error:', error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Find the workflow run
|
|
103
|
+
const findWorkflowRun = async () => {
|
|
104
|
+
try {
|
|
105
|
+
console.log('Looking for workflow run...');
|
|
106
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs?per_page=10`, {
|
|
107
|
+
headers: {
|
|
108
|
+
'Authorization': `token ${effectiveToken}`,
|
|
109
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`HTTP ${response.status}`);
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
console.log('Found runs:', data.workflow_runs.length);
|
|
117
|
+
// Find the most recent run (within last 90 seconds)
|
|
118
|
+
//@ts-ignore
|
|
119
|
+
const recentRun = data.workflow_runs.find(run => {
|
|
120
|
+
const ageInSeconds = (Date.now() - new Date(run.created_at).getTime()) / 1000;
|
|
121
|
+
console.log(`Run ${run.id}: age ${ageInSeconds}s, event: ${run.event}, status: ${run.status}`);
|
|
122
|
+
return ageInSeconds < 90;
|
|
123
|
+
});
|
|
124
|
+
if (recentRun) {
|
|
125
|
+
console.log('Found recent run:', recentRun.id, 'Event:', recentRun.event);
|
|
126
|
+
setDeployState('deploying');
|
|
127
|
+
setMessage('🚀 Deployment started...');
|
|
128
|
+
// Start polling every 5 seconds
|
|
129
|
+
//@ts-ignore
|
|
130
|
+
intervalRef.current = setInterval(() => {
|
|
131
|
+
pollWorkflowStatus(recentRun.id);
|
|
132
|
+
}, 5000);
|
|
133
|
+
// Initial poll
|
|
134
|
+
pollWorkflowStatus(recentRun.id);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log('No recent workflow found');
|
|
138
|
+
setDeployState('error');
|
|
139
|
+
setMessage('❌ Could not find workflow run - check if workflow exists');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error('Error finding workflow:', error);
|
|
144
|
+
setDeployState('error');
|
|
145
|
+
//@ts-ignore
|
|
146
|
+
setMessage(`❌ Error finding workflow: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const triggerDeploy = async () => {
|
|
150
|
+
// Prevent multiple deployments
|
|
151
|
+
if (deployState !== 'idle' && deployState !== 'success' && deployState !== 'error')
|
|
152
|
+
return;
|
|
153
|
+
setDeployState('triggering');
|
|
154
|
+
setMessage('⚡ Triggering deployment...');
|
|
155
|
+
try {
|
|
156
|
+
console.log('Triggering deployment...');
|
|
157
|
+
const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/dispatches`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Authorization': `token ${effectiveToken}`,
|
|
161
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
162
|
+
'Content-Type': 'application/json'
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
event_type: config.eventType || 'deploy-site',
|
|
166
|
+
client_payload: {
|
|
167
|
+
environment: environment,
|
|
168
|
+
triggered_by: 'sanity_studio',
|
|
169
|
+
timestamp: new Date().toISOString()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
if (response.ok) {
|
|
174
|
+
console.log('Deployment triggered successfully');
|
|
175
|
+
setMessage('✅ Deployment triggered! Looking for workflow...');
|
|
176
|
+
// Wait 5 seconds then look for the workflow
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
if (!monitoringRef.current) {
|
|
179
|
+
monitoringRef.current = true;
|
|
180
|
+
findWorkflowRun();
|
|
181
|
+
}
|
|
182
|
+
}, 5000);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const error = await response.json();
|
|
186
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
console.error('Deploy trigger failed:', error);
|
|
191
|
+
setDeployState('error');
|
|
192
|
+
//@ts-ignore
|
|
193
|
+
setMessage(`❌ Failed to trigger: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const getButtonProps = () => {
|
|
197
|
+
switch (deployState) {
|
|
198
|
+
case 'triggering':
|
|
199
|
+
return { tone: 'primary', disabled: true, icon: Spinner, text: 'Triggering...' };
|
|
200
|
+
case 'deploying':
|
|
201
|
+
return { tone: 'primary', disabled: true, icon: Spinner, text: 'Deploying...' };
|
|
202
|
+
case 'success':
|
|
203
|
+
return { tone: 'positive', disabled: false, icon: CheckmarkIcon, text: 'Deploy Again' };
|
|
204
|
+
case 'error':
|
|
205
|
+
return { tone: 'critical', disabled: false, icon: CloseIcon, text: 'Try Again' };
|
|
206
|
+
default:
|
|
207
|
+
return { tone: 'primary', disabled: false, icon: PlayIcon, text: 'Deploy Site' };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// NOW WE CAN HANDLE CONDITIONAL RENDERING AFTER ALL HOOKS
|
|
211
|
+
// Check for missing config
|
|
212
|
+
if (!config || !config.owner || !config.repo) {
|
|
213
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Deploy Button Configuration Error" }), _jsx(Text, { size: 1, children: "Missing required config: owner and repo are required" })] }) }));
|
|
214
|
+
}
|
|
215
|
+
// If showing settings, render the settings view
|
|
216
|
+
if (showSettings && !effectiveToken) {
|
|
217
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsxs(Text, { size: 2, weight: "semibold", children: ["\uD83D\uDD10 ", title, " - Setup Required"] }), _jsx(Text, { size: 1, muted: true, children: "Please configure your GitHub token to enable deployments." }), _jsx(SettingsView, { title: `${title} Configuration`, namespace: secretsNamespace, keys: pluginConfigKeys, onClose: () => {
|
|
218
|
+
setShowSettings(false);
|
|
219
|
+
} })] }) }));
|
|
220
|
+
}
|
|
221
|
+
// Check for missing token after settings
|
|
222
|
+
if (!effectiveToken) {
|
|
223
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Authentication Required" }), _jsx(Text, { size: 1, children: "No GitHub token configured." }), _jsx(Button, { tone: "primary", mode: "ghost",
|
|
224
|
+
//@ts-ignore
|
|
225
|
+
size: "small", onClick: () => setShowSettings(true), children: "Configure Token" })] }) }));
|
|
226
|
+
}
|
|
227
|
+
const buttonProps = getButtonProps();
|
|
228
|
+
return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: title }), _jsx(Flex, { justify: "flex-start", children: _jsx(Button
|
|
229
|
+
//@ts-ignore
|
|
230
|
+
, {
|
|
231
|
+
//@ts-ignore
|
|
232
|
+
tone: buttonProps.tone, disabled: buttonProps.disabled, onClick: triggerDeploy,
|
|
233
|
+
//@ts-ignore
|
|
234
|
+
size: "large", style: {
|
|
235
|
+
width: '100%',
|
|
236
|
+
maxWidth: '360px',
|
|
237
|
+
}, children: _jsxs(Flex, { align: "center", justify: "center", style: {
|
|
238
|
+
gap: '6px',
|
|
239
|
+
padding: '16px 0px 16px 0px'
|
|
240
|
+
}, children: [buttonProps.icon && (_jsx("span", { style: {
|
|
241
|
+
display: 'flex',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
fontSize: '16px',
|
|
244
|
+
lineHeight: '1'
|
|
245
|
+
}, children: React.createElement(buttonProps.icon) })), _jsx(Text, { size: 2, weight: "medium", style: { lineHeight: '1' }, children: buttonProps.text })] }) }) }), message && (_jsx(Card, { padding: 3, radius: 1, tone: deployState === 'error' ? 'critical' : deployState === 'success' ? 'positive' : 'primary', children: _jsx(Text, { size: 1, children: message }) })), deploymentUrl && deployState === 'success' && (_jsx(Button, { as: "a", href: deploymentUrl, target: "_blank", rel: "noopener noreferrer", tone: "primary", mode: "ghost",
|
|
246
|
+
//@ts-ignore
|
|
247
|
+
size: "small", children: "\uD83C\uDF10 View Live Site" })), _jsxs(Text, { size: 1, muted: true, children: ["Deploys from ", config.branch || 'main', " branch to ", environment] })] }) }));
|
|
248
|
+
};
|
|
249
|
+
export default DeployButton;
|
|
@@ -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
|
+
}
|