// Main deploy tool — fetches targets + token, renders per-project cards import { useState, useEffect, useCallback } from 'react' import { useClient } from 'sanity' import { Card, Box, Stack, Flex, Text, Heading, Spinner, Button, Dialog, useToast, } from '@sanity/ui' import { TokenIcon, TrashIcon, WarningOutlineIcon, AddIcon } from '@sanity/icons' import { DeployItem } from './DeployItem' import { TokenSetup } from './TokenSetup' import { DeployTargetForm } from './DeployTargetForm' import { VERSION } from '../version' import type { DeployTarget } from '../types' const TOKEN_QUERY = `*[_id == "config.vercelDeploy"][0].accessToken` const TARGETS_QUERY = `*[_type == "vercel_deploy"] | order(_createdAt asc)` export function DeployTool() { const client = useClient({ apiVersion: '2025-01-01' }) const toast = useToast() const [token, setToken] = useState(null) const [targets, setTargets] = useState([]) const [loading, setLoading] = useState(true) const [showTokenSetup, setShowTokenSetup] = useState(false) const [showCreateForm, setShowCreateForm] = useState(false) const [pendingEdit, setPendingEdit] = useState(null) const [pendingDelete, setPendingDelete] = useState(null) const [deleting, setDeleting] = useState(false) // ── Fetch token + targets ───────────────────────────────────────────────── const load = useCallback(async () => { setLoading(true) try { const [fetchedToken, fetchedTargets] = await Promise.all([ client.fetch(TOKEN_QUERY), client.fetch(TARGETS_QUERY), ]) setToken(fetchedToken ?? null) setTargets(fetchedTargets) } catch (err) { console.error('deploy-vercel-from-sanity: load error', err) } finally { setLoading(false) } }, [client]) useEffect(() => { load() }, [load]) // Inject responsive styles once on mount useEffect(() => { if (document.getElementById('dvfs-styles')) return const style = document.createElement('style') style.id = 'dvfs-styles' style.textContent = ` @media (max-width: 768px) { .dvfs-header { flex-wrap: wrap !important; row-gap: 8px !important; } .dvfs-header-actions { width: 100% !important; flex-wrap: wrap !important; justify-content: flex-start !important; } .dvfs-grid { grid-template-columns: 1fr !important; } .dvfs-card-flex { flex-direction: column !important; } .dvfs-deploy-col { width: 100% !important; align-self: auto !important; } .dvfs-deploy-col button { border-radius: 3px !important; } } @keyframes dvfs-open { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } .dvfs-accordion-content { animation: dvfs-open 0.15s ease-out; } ` document.head.appendChild(style) return () => { document.getElementById('dvfs-styles')?.remove() } }, []) // Live subscription — update targets when documents change useEffect(() => { const sub = client .listen(TARGETS_QUERY) .subscribe(() => { client.fetch(TARGETS_QUERY).then(setTargets).catch(err => { console.error('deploy-vercel-from-sanity: subscription refresh error', err) }) }) return () => sub.unsubscribe() }, [client]) // ── Delete target ───────────────────────────────────────────────────────── const confirmDelete = useCallback(async () => { if (!pendingDelete) return setDeleting(true) try { await client.delete(pendingDelete._id) setTargets(prev => prev.filter(t => t._id !== pendingDelete._id)) toast.push({ status: 'success', title: `Deleted "${pendingDelete.name}"` }) } catch (err) { toast.push({ status: 'error', title: 'Delete failed', description: String(err) }) } finally { setDeleting(false) setPendingDelete(null) } }, [client, pendingDelete, toast]) // ── Render ──────────────────────────────────────────────────────────────── if (loading) { return ( ) } return ( {/* ── Header ──────────────────────────────────────────────── */} Deploy with Vercel