@netlify/spark-ui 1.26.0-alpha.6 → 1.26.0-alpha.8
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/dist/assets/DropZone.css +1 -1
- package/dist/components/FilterableGrid/filter-types.d.ts +40 -0
- package/dist/components/FilterableGrid/index.d.ts +2 -0
- package/dist/components/preact/DropZone/DropZone.d.ts +29 -7
- package/dist/components/preact/DropZone/DropZone.js +101 -128
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +50 -0
- package/dist/components/preact/DropZone/useDropDeploy.js +64 -0
- package/package.json +4 -1
- package/packages/components/Button.astro +3 -0
- package/packages/components/FilterableGrid/FilterableGrid.astro +403 -0
- package/packages/components/FilterableGrid/filter-types.ts +48 -0
- package/packages/components/FilterableGrid/index.ts +2 -0
- package/packages/components/preact/DropZone/DropZone.css +52 -91
- package/packages/components/preact/DropZone/DropZone.tsx +153 -159
- package/packages/components/preact/DropZone/useDropDeploy.ts +149 -0
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
import { useRef, useState } from 'preact/hooks';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
deployFiles,
|
|
6
|
-
getSingleNonIndexHtmlFileName,
|
|
7
|
-
hasMultipleHtmlFilesWithoutIndex,
|
|
8
|
-
readDataTransfer,
|
|
9
|
-
readFileList,
|
|
10
|
-
type DeployProgress,
|
|
11
|
-
type DropClient,
|
|
12
|
-
type ProcessedFile,
|
|
13
|
-
} from './drop-core';
|
|
1
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import type { ComponentChildren } from 'preact';
|
|
3
|
+
import { readDataTransfer, readFileList, type DropClient } from './drop-core';
|
|
4
|
+
import { useDropDeploy, type DeployedSite, type DeployStatus, type TrackFn } from './useDropDeploy';
|
|
14
5
|
|
|
15
6
|
import './DropZone.css';
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| { kind: 'uploading'; uploaded: number; total: number }
|
|
22
|
-
| { kind: 'processing' }
|
|
23
|
-
| { kind: 'redirecting' }
|
|
24
|
-
| { kind: 'error'; message: string };
|
|
8
|
+
/** How long after the last `dragover` event we consider the drag gone. */
|
|
9
|
+
const DRAG_INACTIVE_MS = 120;
|
|
10
|
+
|
|
11
|
+
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
25
12
|
|
|
26
13
|
interface Props {
|
|
14
|
+
/** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
|
|
15
|
+
children?: ComponentChildren;
|
|
27
16
|
/** Override the API base (e.g. for a staging environment). */
|
|
28
17
|
apiBase?: string;
|
|
29
18
|
/** Inject a custom client (e.g. for testing). Defaults to AnonymousDropClient. */
|
|
@@ -31,160 +20,189 @@ interface Props {
|
|
|
31
20
|
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
32
21
|
claimUrl?: (siteName: string) => string;
|
|
33
22
|
/** Called once the deploy is live. */
|
|
34
|
-
onDeploy?: (result:
|
|
23
|
+
onDeploy?: (result: DeployedSite) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Render the built-in status overlay (drag tint, spinner, progress bar, error).
|
|
26
|
+
* Set to `false` to suppress it entirely and keep your children visible, then
|
|
27
|
+
* drive your own feedback from the `data-state` attribute on the root element.
|
|
28
|
+
* Note: in this mode you're responsible for surfacing errors yourself.
|
|
29
|
+
*/
|
|
30
|
+
showStatus?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Analytics hook. Called at each tracked moment with an event name and
|
|
33
|
+
* properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
|
|
34
|
+
* consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
|
|
35
|
+
* Emitted events (all include `dropzone_id` when `analyticsId` is set):
|
|
36
|
+
* - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
|
|
37
|
+
* - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
|
|
38
|
+
* - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
|
|
39
|
+
*/
|
|
40
|
+
onTrack?: TrackFn;
|
|
41
|
+
/**
|
|
42
|
+
* Stable identifier for this instance, attached to every tracked event as
|
|
43
|
+
* `dropzone_id`. Set it when a page renders more than one Drop Zone so the
|
|
44
|
+
* events can be told apart in Amplitude.
|
|
45
|
+
*/
|
|
46
|
+
analyticsId?: string;
|
|
35
47
|
className?: string;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
|
|
39
|
-
|
|
40
|
-
function progressPhaseToStatus(e: DeployProgress): Status {
|
|
41
|
-
switch (e.phase) {
|
|
42
|
-
case 'creating':
|
|
43
|
-
return { kind: 'creating' };
|
|
44
|
-
case 'uploading':
|
|
45
|
-
return { kind: 'uploading', uploaded: e.uploaded ?? 0, total: e.total ?? 0 };
|
|
46
|
-
case 'processing':
|
|
47
|
-
case 'ready':
|
|
48
|
-
return { kind: 'processing' };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
50
|
export default function DropZone({
|
|
51
|
+
children,
|
|
53
52
|
apiBase,
|
|
54
53
|
client,
|
|
55
54
|
claimUrl = defaultClaimUrl,
|
|
56
55
|
onDeploy,
|
|
56
|
+
showStatus = true,
|
|
57
|
+
onTrack,
|
|
58
|
+
analyticsId,
|
|
57
59
|
className,
|
|
58
60
|
}: Props) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
localStorage.setItem('drop-token', token);
|
|
94
|
-
localStorage.setItem('dropSiteName', deploy.subdomain);
|
|
95
|
-
} catch {
|
|
96
|
-
/* private mode / storage disabled — non-fatal */
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
setStatus({ kind: 'redirecting' });
|
|
100
|
-
onDeploy?.({ url, token, siteName: deploy.subdomain, siteId: deploy.id });
|
|
101
|
-
|
|
102
|
-
// Hand the drop token to the dashboard via the URL fragment — localStorage
|
|
103
|
-
// does not cross origins. The dashboard's /drop/:name page reads
|
|
104
|
-
// `#drop_token=` and persists it for the claim + post-signup steps.
|
|
105
|
-
window.location.assign(`${claimUrl(deploy.subdomain)}#drop_token=${token}`);
|
|
106
|
-
} catch (err) {
|
|
107
|
-
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function reset() {
|
|
112
|
-
setWarning(null);
|
|
113
|
-
setStatus({ kind: 'idle' });
|
|
114
|
-
}
|
|
61
|
+
// Tag every event with this instance's id so multiple zones on a page can be
|
|
62
|
+
// told apart, then hand it off to the consumer's analytics.
|
|
63
|
+
const track: TrackFn = (event, properties = {}) =>
|
|
64
|
+
onTrack?.(event, analyticsId ? { dropzone_id: analyticsId, ...properties } : properties);
|
|
65
|
+
|
|
66
|
+
const { status, warning, busy, deploy, reset } = useDropDeploy({
|
|
67
|
+
apiBase,
|
|
68
|
+
client,
|
|
69
|
+
claimUrl,
|
|
70
|
+
onDeploy,
|
|
71
|
+
track,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const { active, keepActive, endDrag } = useDragActive();
|
|
75
|
+
|
|
76
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
77
|
+
const openPicker = (method: 'keyboard' | 'click') => {
|
|
78
|
+
track('dropzone_browse_opened', { method });
|
|
79
|
+
inputRef.current?.click();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// The overlay only paints when there's something to show — dragging, deploying,
|
|
83
|
+
// or an error. With `showStatus` off, the consumer drives their own feedback.
|
|
84
|
+
const showOverlay = showStatus && (active || busy || status.kind === 'error');
|
|
85
|
+
const rootClass = [
|
|
86
|
+
'n-dropzone',
|
|
87
|
+
active && 'is-active',
|
|
88
|
+
busy && 'is-busy',
|
|
89
|
+
!showStatus && 'is-bare',
|
|
90
|
+
className,
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(' ');
|
|
115
94
|
|
|
116
95
|
return (
|
|
117
96
|
<div
|
|
118
|
-
class={
|
|
97
|
+
class={rootClass}
|
|
119
98
|
data-state={status.kind}
|
|
99
|
+
role="button"
|
|
100
|
+
tabIndex={busy ? -1 : 0}
|
|
101
|
+
aria-label="Drop a site folder or zip here to deploy it, or press Enter to browse for files"
|
|
102
|
+
aria-busy={busy}
|
|
120
103
|
onDragOver={e => {
|
|
121
104
|
if (busy) return;
|
|
122
105
|
e.preventDefault();
|
|
123
|
-
|
|
124
|
-
}}
|
|
125
|
-
onDragLeave={e => {
|
|
126
|
-
// Ignore drag-leave bubbling from children.
|
|
127
|
-
if (e.currentTarget === e.target) setActive(false);
|
|
106
|
+
keepActive();
|
|
128
107
|
}}
|
|
129
108
|
onDrop={e => {
|
|
130
109
|
e.preventDefault();
|
|
131
|
-
|
|
110
|
+
endDrag();
|
|
132
111
|
if (busy) return;
|
|
133
112
|
const dt = e.dataTransfer;
|
|
134
113
|
if (!dt) return;
|
|
135
114
|
// Capture entries synchronously — readDataTransfer reads them before
|
|
136
115
|
// its first await, so the DataTransfer isn't detached out from under us.
|
|
137
|
-
|
|
116
|
+
deploy(() => readDataTransfer(dt), 'drag');
|
|
117
|
+
}}
|
|
118
|
+
onClick={e => {
|
|
119
|
+
// Only the zone's own empty area opens the picker; clicks that land on
|
|
120
|
+
// children are left to those children so they stay interactive.
|
|
121
|
+
if (!busy && e.target === e.currentTarget) openPicker('click');
|
|
122
|
+
}}
|
|
123
|
+
onKeyDown={e => {
|
|
124
|
+
if (busy) return;
|
|
125
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
openPicker('keyboard');
|
|
128
|
+
}
|
|
138
129
|
}}
|
|
139
130
|
>
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
<div class="n-dropzone-content">{children}</div>
|
|
132
|
+
|
|
133
|
+
{/* Invisible-but-present fallback: keyboard/click opens the file picker. */}
|
|
134
|
+
<input
|
|
135
|
+
ref={inputRef}
|
|
136
|
+
type="file"
|
|
137
|
+
hidden
|
|
138
|
+
multiple
|
|
139
|
+
// @ts-expect-error non-standard directory-picker attributes
|
|
140
|
+
webkitdirectory=""
|
|
141
|
+
directory=""
|
|
142
|
+
onChange={e => {
|
|
143
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
144
|
+
if (input.files?.length) deploy(() => readFileList(input.files!), 'browse');
|
|
145
|
+
input.value = '';
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
{showOverlay && (
|
|
150
|
+
<div
|
|
151
|
+
class="n-dropzone-overlay n-fadeIn"
|
|
152
|
+
key={status.kind === 'idle' ? 'active' : status.kind}
|
|
153
|
+
>
|
|
154
|
+
<StatusView status={status} active={active} onReset={reset} />
|
|
155
|
+
{warning && status.kind !== 'error' && busy && (
|
|
156
|
+
<p class="n-dropzone-warning" role="status">
|
|
157
|
+
{warning}
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
150
161
|
)}
|
|
151
162
|
</div>
|
|
152
163
|
);
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Track whether a drag is currently over the zone. A drag has no reliable
|
|
168
|
+
* "cancelled" event — pressing Escape or dropping elsewhere just stops the
|
|
169
|
+
* stream of `dragover` events — so we stay active only while they keep arriving
|
|
170
|
+
* and clear shortly after they stop. This also handles drags over children,
|
|
171
|
+
* where `dragleave` targets a child rather than the zone.
|
|
172
|
+
*/
|
|
173
|
+
function useDragActive() {
|
|
174
|
+
const [active, setActive] = useState(false);
|
|
175
|
+
const timer = useRef<ReturnType<typeof setTimeout>>();
|
|
176
|
+
|
|
177
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
178
|
+
|
|
179
|
+
const keepActive = () => {
|
|
180
|
+
setActive(true);
|
|
181
|
+
clearTimeout(timer.current);
|
|
182
|
+
timer.current = setTimeout(() => setActive(false), DRAG_INACTIVE_MS);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const endDrag = () => {
|
|
186
|
+
clearTimeout(timer.current);
|
|
187
|
+
setActive(false);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return { active, keepActive, endDrag };
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
function StatusView({
|
|
156
194
|
status,
|
|
195
|
+
active,
|
|
157
196
|
onReset,
|
|
158
|
-
onFiles,
|
|
159
197
|
}: {
|
|
160
|
-
status:
|
|
198
|
+
status: DeployStatus;
|
|
199
|
+
active: boolean;
|
|
161
200
|
onReset: () => void;
|
|
162
|
-
onFiles: (read: () => Promise<ProcessedFile[]>) => void;
|
|
163
201
|
}) {
|
|
164
202
|
switch (status.kind) {
|
|
203
|
+
// Idle-but-active means a drag is hovering the zone.
|
|
165
204
|
case 'idle':
|
|
166
|
-
return
|
|
167
|
-
<>
|
|
168
|
-
<UploadGlyph />
|
|
169
|
-
<p class="n-dropzone-title">Drag & drop your site folder or zip</p>
|
|
170
|
-
<p class="n-dropzone-subtitle">Deploys instantly to a live URL — no account needed.</p>
|
|
171
|
-
<label class="n-dropzone-browse">
|
|
172
|
-
Browse files
|
|
173
|
-
<input
|
|
174
|
-
type="file"
|
|
175
|
-
hidden
|
|
176
|
-
multiple
|
|
177
|
-
// @ts-expect-error non-standard directory-picker attributes
|
|
178
|
-
webkitdirectory=""
|
|
179
|
-
directory=""
|
|
180
|
-
onChange={e => {
|
|
181
|
-
const input = e.currentTarget as HTMLInputElement;
|
|
182
|
-
if (input.files?.length) onFiles(() => readFileList(input.files!));
|
|
183
|
-
}}
|
|
184
|
-
/>
|
|
185
|
-
</label>
|
|
186
|
-
</>
|
|
187
|
-
);
|
|
205
|
+
return active ? <p class="n-dropzone-hint">Drop to deploy</p> : null;
|
|
188
206
|
|
|
189
207
|
case 'reading':
|
|
190
208
|
return <Spinner label="Reading files…" />;
|
|
@@ -232,27 +250,3 @@ function Spinner({ label }: { label: string }) {
|
|
|
232
250
|
</div>
|
|
233
251
|
);
|
|
234
252
|
}
|
|
235
|
-
|
|
236
|
-
function UploadGlyph() {
|
|
237
|
-
return (
|
|
238
|
-
<span class="n-dropzone-glyph" aria-hidden="true">
|
|
239
|
-
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
240
|
-
<path
|
|
241
|
-
class="n-dropzone-glyph-tray"
|
|
242
|
-
d="M8 30v6a4 4 0 0 0 4 4h24a4 4 0 0 0 4-4v-6"
|
|
243
|
-
stroke="currentColor"
|
|
244
|
-
stroke-width="3"
|
|
245
|
-
stroke-linecap="round"
|
|
246
|
-
/>
|
|
247
|
-
<path
|
|
248
|
-
class="n-dropzone-glyph-arrow"
|
|
249
|
-
d="M24 32V10m0 0-8 8m8-8 8 8"
|
|
250
|
-
stroke="currentColor"
|
|
251
|
-
stroke-width="3"
|
|
252
|
-
stroke-linecap="round"
|
|
253
|
-
stroke-linejoin="round"
|
|
254
|
-
/>
|
|
255
|
-
</svg>
|
|
256
|
-
</span>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useRef, useState } from 'preact/hooks';
|
|
2
|
+
import {
|
|
3
|
+
AnonymousDropClient,
|
|
4
|
+
deployFiles,
|
|
5
|
+
getSingleNonIndexHtmlFileName,
|
|
6
|
+
hasMultipleHtmlFilesWithoutIndex,
|
|
7
|
+
type DeployProgress,
|
|
8
|
+
type DropClient,
|
|
9
|
+
type ProcessedFile,
|
|
10
|
+
} from './drop-core';
|
|
11
|
+
|
|
12
|
+
/** How the files reached the zone — carried through to analytics. */
|
|
13
|
+
export type DeploySource = 'drag' | 'browse';
|
|
14
|
+
|
|
15
|
+
/** The live site handed back once a deploy succeeds. */
|
|
16
|
+
export interface DeployedSite {
|
|
17
|
+
url: string;
|
|
18
|
+
token: string;
|
|
19
|
+
siteName: string;
|
|
20
|
+
siteId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Finite states of a single deploy attempt. */
|
|
24
|
+
export type DeployStatus =
|
|
25
|
+
| { kind: 'idle' }
|
|
26
|
+
| { kind: 'reading' }
|
|
27
|
+
| { kind: 'creating' }
|
|
28
|
+
| { kind: 'uploading'; uploaded: number; total: number }
|
|
29
|
+
| { kind: 'processing' }
|
|
30
|
+
| { kind: 'redirecting' }
|
|
31
|
+
| { kind: 'error'; message: string };
|
|
32
|
+
|
|
33
|
+
/** Provider-agnostic analytics sink. */
|
|
34
|
+
export type TrackFn = (event: string, properties?: Record<string, unknown>) => void;
|
|
35
|
+
|
|
36
|
+
interface UseDropDeployOptions {
|
|
37
|
+
apiBase?: string;
|
|
38
|
+
client?: DropClient;
|
|
39
|
+
claimUrl: (siteName: string) => string;
|
|
40
|
+
onDeploy?: (site: DeployedSite) => void;
|
|
41
|
+
track: TrackFn;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Map a low-level deploy progress event onto our UI status. */
|
|
45
|
+
function progressToStatus(e: DeployProgress): DeployStatus {
|
|
46
|
+
switch (e.phase) {
|
|
47
|
+
case 'creating':
|
|
48
|
+
return { kind: 'creating' };
|
|
49
|
+
case 'uploading':
|
|
50
|
+
return { kind: 'uploading', uploaded: e.uploaded ?? 0, total: e.total ?? 0 };
|
|
51
|
+
case 'processing':
|
|
52
|
+
case 'ready':
|
|
53
|
+
return { kind: 'processing' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Inspect the file set and return a non-blocking index.html warning, if any. */
|
|
58
|
+
function indexHtmlWarning(files: ProcessedFile[]): string | null {
|
|
59
|
+
const singleNonIndex = getSingleNonIndexHtmlFileName(files);
|
|
60
|
+
if (singleNonIndex) {
|
|
61
|
+
return `No index.html found — "${singleNonIndex}" won’t be served at the site root.`;
|
|
62
|
+
}
|
|
63
|
+
if (hasMultipleHtmlFilesWithoutIndex(files)) {
|
|
64
|
+
return 'No index.html found — your site may not have a landing page.';
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Owns the deploy state machine and its side effects (upload, local persistence,
|
|
71
|
+
* analytics, redirect), keeping DropZone a thin view over the returned `status`.
|
|
72
|
+
*/
|
|
73
|
+
export function useDropDeploy({
|
|
74
|
+
apiBase,
|
|
75
|
+
client,
|
|
76
|
+
claimUrl,
|
|
77
|
+
onDeploy,
|
|
78
|
+
track,
|
|
79
|
+
}: UseDropDeployOptions) {
|
|
80
|
+
const [status, setStatus] = useState<DeployStatus>({ kind: 'idle' });
|
|
81
|
+
const [warning, setWarning] = useState<string | null>(null);
|
|
82
|
+
const dropClient = useRef(client ?? new AnonymousDropClient(apiBase)).current;
|
|
83
|
+
|
|
84
|
+
const busy = status.kind !== 'idle' && status.kind !== 'error';
|
|
85
|
+
|
|
86
|
+
async function deploy(read: () => Promise<ProcessedFile[]>, source: DeploySource) {
|
|
87
|
+
setWarning(null);
|
|
88
|
+
setStatus({ kind: 'reading' });
|
|
89
|
+
try {
|
|
90
|
+
const files = await read();
|
|
91
|
+
if (!files.length) {
|
|
92
|
+
setStatus({ kind: 'error', message: 'No files found to deploy.' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A drop is the "user provided files" signal; the browse path is already
|
|
97
|
+
// tracked when the picker opens.
|
|
98
|
+
if (source === 'drag') {
|
|
99
|
+
track('dropzone_files_dropped', { method: source, file_count: files.length });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setWarning(indexHtmlWarning(files));
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
url,
|
|
106
|
+
token,
|
|
107
|
+
deploy: result,
|
|
108
|
+
} = await deployFiles(dropClient, files, {
|
|
109
|
+
onProgress: e => setStatus(progressToStatus(e)),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Best-effort local persistence. Note this does NOT reach the dashboard:
|
|
113
|
+
// localStorage is per-origin, so a token written here (e.g. www.netlify.com)
|
|
114
|
+
// is invisible to app.netlify.com. The cross-origin handoff is the URL
|
|
115
|
+
// fragment below; this only helps if the drop and claim share an origin.
|
|
116
|
+
try {
|
|
117
|
+
localStorage.setItem('drop-token', token);
|
|
118
|
+
localStorage.setItem('dropSiteName', result.subdomain);
|
|
119
|
+
} catch {
|
|
120
|
+
/* private mode / storage disabled — non-fatal */
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setStatus({ kind: 'redirecting' });
|
|
124
|
+
// Fired before the redirect below. Note: the navigation can cut a
|
|
125
|
+
// fire-and-forget analytics request short — see the docs for delivery
|
|
126
|
+
// hardening (sendBeacon / deferring the redirect).
|
|
127
|
+
track('dropzone_deploy_succeeded', {
|
|
128
|
+
method: source,
|
|
129
|
+
site_name: result.subdomain,
|
|
130
|
+
file_count: files.length,
|
|
131
|
+
});
|
|
132
|
+
onDeploy?.({ url, token, siteName: result.subdomain, siteId: result.id });
|
|
133
|
+
|
|
134
|
+
// Hand the drop token to the dashboard via the URL fragment — localStorage
|
|
135
|
+
// does not cross origins. The dashboard's /drop/:name page reads
|
|
136
|
+
// `#drop_token=` and persists it for the claim + post-signup steps.
|
|
137
|
+
window.location.assign(`${claimUrl(result.subdomain)}#drop_token=${token}`);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reset() {
|
|
144
|
+
setWarning(null);
|
|
145
|
+
setStatus({ kind: 'idle' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { status, warning, busy, deploy, reset };
|
|
149
|
+
}
|