strapi-plugin-radiocult-uploader 1.0.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/admin/App-Cd-TZrqC.mjs +1179 -0
  4. package/dist/admin/App-DTOuR7aY.js +1199 -0
  5. package/dist/admin/Settings-B3cZR7VF.mjs +516 -0
  6. package/dist/admin/Settings-DHv9SmRU.js +534 -0
  7. package/dist/admin/en-CTCtUg23.mjs +8 -0
  8. package/dist/admin/en-D_61ojL7.js +8 -0
  9. package/dist/admin/index-BLAtUo_j.js +334 -0
  10. package/dist/admin/index-Dj3e3A79.mjs +318 -0
  11. package/dist/admin/index.js +4 -0
  12. package/dist/admin/index.mjs +4 -0
  13. package/dist/admin/src/api.d.ts +107 -0
  14. package/dist/admin/src/components/Initializer.d.ts +5 -0
  15. package/dist/admin/src/components/Panel.d.ts +18 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  17. package/dist/admin/src/index.d.ts +3 -0
  18. package/dist/admin/src/pages/App.d.ts +2 -0
  19. package/dist/admin/src/pages/HomePage.d.ts +6 -0
  20. package/dist/admin/src/pages/Settings.d.ts +8 -0
  21. package/dist/admin/src/pages/Uploader.d.ts +10 -0
  22. package/dist/admin/src/pluginId.d.ts +1 -0
  23. package/dist/admin/src/theme/tokens.d.ts +96 -0
  24. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  25. package/dist/admin/tokens-ACCekCH6.mjs +161 -0
  26. package/dist/admin/tokens-CDeNcvZq.js +178 -0
  27. package/dist/server/index.js +1193 -0
  28. package/dist/server/index.mjs +1165 -0
  29. package/dist/server/src/bootstrap.d.ts +5 -0
  30. package/dist/server/src/config/index.d.ts +17 -0
  31. package/dist/server/src/content-types/index.d.ts +51 -0
  32. package/dist/server/src/content-types/track-record/schema.json.d.ts +29 -0
  33. package/dist/server/src/controllers/index.d.ts +21 -0
  34. package/dist/server/src/controllers/settings.d.ts +16 -0
  35. package/dist/server/src/controllers/uploader.d.ts +10 -0
  36. package/dist/server/src/controllers/utils.d.ts +16 -0
  37. package/dist/server/src/destroy.d.ts +5 -0
  38. package/dist/server/src/index.d.ts +267 -0
  39. package/dist/server/src/middlewares/index.d.ts +2 -0
  40. package/dist/server/src/policies/index.d.ts +2 -0
  41. package/dist/server/src/register.d.ts +5 -0
  42. package/dist/server/src/routes/admin/index.d.ts +22 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/id3.d.ts +18 -0
  45. package/dist/server/src/services/index.d.ts +156 -0
  46. package/dist/server/src/services/mapping.d.ts +47 -0
  47. package/dist/server/src/services/radiocult.d.ts +115 -0
  48. package/dist/server/src/services/records.d.ts +25 -0
  49. package/dist/server/src/services/settings.d.ts +103 -0
  50. package/dist/server/src/utils/errors.d.ts +5 -0
  51. package/dist/server/src/utils/plugin.d.ts +4 -0
  52. package/logo.png +0 -0
  53. package/package.json +98 -0
  54. package/scripts/migrate-from-fields.mjs +78 -0
@@ -0,0 +1,1179 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useAuth, Page as Page$1 } from "@strapi/strapi/admin";
3
+ import { useNavigate, useParams, Routes, Route } from "react-router-dom";
4
+ import { Main, Typography, Box, Flex, Button } from "@strapi/design-system";
5
+ import * as React from "react";
6
+ import styled from "styled-components";
7
+ import { f as fetchStatus, P as PLATFORMS, a as PLATFORM_LABEL, b as apiError, c as fetchUiConfig, s as saveTrackDetails, u as uploadTrack, p as publishTrack } from "./index-Dj3e3A79.mjs";
8
+ import { u as useGoogleFont, g as googleFontHref, r as resolveFont, t as themeToCssVars } from "./tokens-ACCekCH6.mjs";
9
+ const HomePage = () => {
10
+ const navigate = useNavigate();
11
+ return /* @__PURE__ */ jsxs(Main, { padding: 8, children: [
12
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: "Radio Cult Uploader" }),
13
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, paddingBottom: 6, children: /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: "Upload show episodes to Radio Cult and publish them to SoundCloud and Mixcloud." }) }),
14
+ /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "filterShadow", padding: 6, style: { maxWidth: 720 }, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 4, children: [
15
+ /* @__PURE__ */ jsxs(Typography, { children: [
16
+ "The uploader opens from an episode: go to your episode content type in the Content Manager, open an entry, and use the",
17
+ " ",
18
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: "Upload to Soundcloud/Mixcloud/Radio Cult" }),
19
+ " ",
20
+ "panel on the right."
21
+ ] }),
22
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "Before first use, set your Radio Cult API key and map your episode content type in the plugin settings. You can also change the uploader's colours and logo there." }),
23
+ /* @__PURE__ */ jsx(Button, { onClick: () => navigate("/settings/radiocult-uploader"), children: "Open settings" })
24
+ ] }) })
25
+ ] });
26
+ };
27
+ const POLL_MS = 3e4;
28
+ const MAX_BYTES = 750 * 1024 * 1024;
29
+ const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)}MB`;
30
+ let nextId = 1;
31
+ const Page = styled.div`
32
+ font-family: var(--rcu-fontFamily, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
33
+ background: linear-gradient(160deg, var(--rcu-pageBg), var(--rcu-pageBgDeep) 100%);
34
+ background-attachment: fixed;
35
+ color: var(--rcu-text);
36
+ min-height: 100vh;
37
+ padding: 40px 20px 80px;
38
+
39
+ *,
40
+ *::before,
41
+ *::after {
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ .wrap {
46
+ max-width: 860px;
47
+ margin: 0 auto;
48
+ }
49
+
50
+ /* ---------- header ---------- */
51
+ header {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 18px;
55
+ margin-bottom: 34px;
56
+ animation: rcu-rise 0.6s ease both;
57
+ }
58
+ .eq {
59
+ flex: none;
60
+ width: 62px;
61
+ height: 62px;
62
+ display: block;
63
+ }
64
+ .eq img,
65
+ .eq svg {
66
+ width: 100%;
67
+ height: 100%;
68
+ display: block;
69
+ animation: rcu-breathe 2.4s ease-in-out infinite;
70
+ }
71
+ @keyframes rcu-breathe {
72
+ 50% {
73
+ transform: scale(1.07);
74
+ opacity: 0.78;
75
+ }
76
+ }
77
+ .eq.paused img,
78
+ .eq.paused svg {
79
+ animation: none;
80
+ transform: none;
81
+ }
82
+
83
+ h1 {
84
+ font-size: 26px;
85
+ letter-spacing: 0.04em;
86
+ font-weight: 800;
87
+ margin: 0;
88
+ background: linear-gradient(
89
+ 90deg,
90
+ var(--rcu-text),
91
+ var(--rcu-contrast) 35%,
92
+ var(--rcu-textDim) 60%,
93
+ var(--rcu-text)
94
+ );
95
+ background-size: 200% auto;
96
+ -webkit-background-clip: text;
97
+ background-clip: text;
98
+ color: transparent;
99
+ animation: rcu-sheen 6s linear infinite;
100
+ }
101
+ @keyframes rcu-sheen {
102
+ to {
103
+ background-position: 200% center;
104
+ }
105
+ }
106
+ .strap {
107
+ color: var(--rcu-textDim);
108
+ font-size: 13px;
109
+ margin-top: 4px;
110
+ }
111
+ .strap b {
112
+ color: var(--rcu-text);
113
+ font-weight: 600;
114
+ }
115
+
116
+ /* ---------- cards / steps ---------- */
117
+ .card {
118
+ background: var(--rcu-card);
119
+ border: 1px solid var(--rcu-cardBorder);
120
+ border-radius: 18px;
121
+ padding: 26px 28px;
122
+ margin-bottom: 22px;
123
+ backdrop-filter: blur(14px);
124
+ -webkit-backdrop-filter: blur(14px);
125
+ animation: rcu-rise 0.6s ease both;
126
+ transition: border-color 0.4s, box-shadow 0.4s;
127
+ }
128
+ .card:nth-of-type(1) {
129
+ animation-delay: 0.05s;
130
+ }
131
+ .card:nth-of-type(2) {
132
+ animation-delay: 0.12s;
133
+ }
134
+ .card:nth-of-type(3) {
135
+ animation-delay: 0.19s;
136
+ }
137
+ .card:nth-of-type(4) {
138
+ animation-delay: 0.26s;
139
+ }
140
+ @keyframes rcu-rise {
141
+ from {
142
+ opacity: 0;
143
+ transform: translateY(14px);
144
+ }
145
+ to {
146
+ opacity: 1;
147
+ transform: none;
148
+ }
149
+ }
150
+ .card.active {
151
+ border-color: var(--rcu-text);
152
+ box-shadow: 0 6px 26px var(--rcu-shadowSoft);
153
+ }
154
+ .card.done {
155
+ border-color: var(--rcu-green);
156
+ }
157
+
158
+ .step-head {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 14px;
162
+ margin-bottom: 16px;
163
+ }
164
+ .badge {
165
+ width: 34px;
166
+ height: 34px;
167
+ border-radius: 50%;
168
+ flex: none;
169
+ display: grid;
170
+ place-items: center;
171
+ font-weight: 800;
172
+ font-size: 15px;
173
+ color: var(--rcu-buttonText);
174
+ background: linear-gradient(135deg, var(--rcu-text), var(--rcu-contrast));
175
+ transition: transform 0.3s;
176
+ }
177
+ .card.done .badge {
178
+ background: linear-gradient(135deg, var(--rcu-green), var(--rcu-vinylMid));
179
+ animation: rcu-pop 0.45s ease;
180
+ }
181
+ @keyframes rcu-pop {
182
+ 50% {
183
+ transform: scale(1.25);
184
+ }
185
+ }
186
+ .step-head h2 {
187
+ font-size: 17px;
188
+ font-weight: 700;
189
+ letter-spacing: 0.02em;
190
+ margin: 0;
191
+ }
192
+ .step-head .hint {
193
+ margin-left: auto;
194
+ font-size: 12px;
195
+ color: var(--rcu-textDim);
196
+ text-align: right;
197
+ }
198
+
199
+ .locked {
200
+ opacity: 0.45;
201
+ pointer-events: none;
202
+ filter: saturate(0.5);
203
+ }
204
+
205
+ /* ---------- form ---------- */
206
+ .grid {
207
+ display: grid;
208
+ grid-template-columns: 1fr 1fr;
209
+ gap: 14px;
210
+ }
211
+ .grid .full {
212
+ grid-column: 1 / -1;
213
+ }
214
+ @media (max-width: 620px) {
215
+ .grid {
216
+ grid-template-columns: 1fr;
217
+ }
218
+ }
219
+ label {
220
+ display: block;
221
+ font-size: 11px;
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.12em;
224
+ color: var(--rcu-textDim);
225
+ margin-bottom: 6px;
226
+ }
227
+ input[type='text'],
228
+ textarea {
229
+ width: 100%;
230
+ color: var(--rcu-text);
231
+ font-size: 14px;
232
+ font-family: inherit;
233
+ background: var(--rcu-inputBg);
234
+ border: 1px solid var(--rcu-cardBorder);
235
+ border-radius: 10px;
236
+ padding: 11px 13px;
237
+ transition: border-color 0.25s, box-shadow 0.25s;
238
+ }
239
+ input[type='text']:focus,
240
+ textarea:focus {
241
+ outline: none;
242
+ border-color: var(--rcu-text);
243
+ box-shadow: 0 0 0 3px var(--rcu-shadowSoft);
244
+ }
245
+ textarea {
246
+ resize: vertical;
247
+ min-height: 92px;
248
+ }
249
+ .fine {
250
+ font-size: 12px;
251
+ color: var(--rcu-textDim);
252
+ margin-top: 5px;
253
+ }
254
+
255
+ .artwork {
256
+ display: flex;
257
+ gap: 16px;
258
+ align-items: center;
259
+ margin-top: 16px;
260
+ }
261
+ .artwork img {
262
+ width: 88px;
263
+ height: 88px;
264
+ object-fit: cover;
265
+ border-radius: 12px;
266
+ border: 1px solid var(--rcu-cardBorder);
267
+ box-shadow: 0 6px 20px var(--rcu-shadow);
268
+ }
269
+ .artwork .fine {
270
+ margin: 0;
271
+ }
272
+
273
+ /* ---------- buttons ---------- */
274
+ .btn {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ gap: 8px;
279
+ font-size: 14px;
280
+ font-weight: 700;
281
+ font-family: inherit;
282
+ letter-spacing: 0.02em;
283
+ color: var(--rcu-buttonText);
284
+ background: linear-gradient(135deg, var(--rcu-text), var(--rcu-contrast));
285
+ border: none;
286
+ border-radius: 12px;
287
+ padding: 12px 22px;
288
+ cursor: pointer;
289
+ box-shadow: 0 4px 16px var(--rcu-shadow);
290
+ transition: transform 0.15s, box-shadow 0.25s, filter 0.2s;
291
+ }
292
+ .btn:hover:not(:disabled) {
293
+ transform: translateY(-2px);
294
+ box-shadow: 0 8px 22px var(--rcu-shadow);
295
+ }
296
+ .btn:active:not(:disabled) {
297
+ transform: translateY(0);
298
+ }
299
+ .btn:disabled {
300
+ opacity: 0.38;
301
+ cursor: not-allowed;
302
+ box-shadow: none;
303
+ }
304
+ .btn.ghost {
305
+ background: transparent;
306
+ border: 1px solid var(--rcu-cardBorder);
307
+ box-shadow: none;
308
+ color: var(--rcu-text);
309
+ }
310
+ .btn.ghost:hover:not(:disabled) {
311
+ border-color: var(--rcu-text);
312
+ background: var(--rcu-inputBg);
313
+ }
314
+ .btn.danger {
315
+ background: linear-gradient(135deg, var(--rcu-red), var(--rcu-contrast));
316
+ box-shadow: 0 4px 16px var(--rcu-shadow);
317
+ }
318
+ .btn.small {
319
+ font-size: 12px;
320
+ padding: 8px 14px;
321
+ border-radius: 9px;
322
+ }
323
+ .btn .spin {
324
+ width: 14px;
325
+ height: 14px;
326
+ border-radius: 50%;
327
+ flex: none;
328
+ border: 2px solid var(--rcu-shadowSoft);
329
+ border-top-color: var(--rcu-buttonText);
330
+ animation: rcu-rot 0.8s linear infinite;
331
+ }
332
+ @keyframes rcu-rot {
333
+ to {
334
+ transform: rotate(360deg);
335
+ }
336
+ }
337
+ .row {
338
+ display: flex;
339
+ gap: 10px;
340
+ flex-wrap: wrap;
341
+ align-items: center;
342
+ margin-top: 16px;
343
+ }
344
+
345
+ /* ---------- dropzone / progress ---------- */
346
+ .dropzone {
347
+ border: 2px dashed var(--rcu-cardBorder);
348
+ border-radius: 14px;
349
+ padding: 30px 20px;
350
+ text-align: center;
351
+ cursor: pointer;
352
+ transition: border-color 0.25s, background 0.25s, transform 0.15s;
353
+ }
354
+ .dropzone:hover,
355
+ .dropzone.over {
356
+ border-color: var(--rcu-text);
357
+ background: var(--rcu-inputBg);
358
+ }
359
+ .dropzone.over {
360
+ transform: scale(1.01);
361
+ }
362
+ .dropzone .big {
363
+ font-size: 15px;
364
+ font-weight: 600;
365
+ }
366
+ .dropzone .fine {
367
+ margin-top: 6px;
368
+ }
369
+ .filechip {
370
+ display: inline-flex;
371
+ align-items: center;
372
+ gap: 10px;
373
+ margin-top: 14px;
374
+ font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
375
+ font-size: 13px;
376
+ background: var(--rcu-inputBg);
377
+ border: 1px solid var(--rcu-cardBorder);
378
+ border-radius: 999px;
379
+ padding: 8px 16px;
380
+ }
381
+ .filechip .x {
382
+ cursor: pointer;
383
+ color: var(--rcu-textDim);
384
+ font-weight: 700;
385
+ }
386
+ .filechip .x:hover {
387
+ color: var(--rcu-red);
388
+ }
389
+
390
+ .vinyl {
391
+ width: 46px;
392
+ height: 46px;
393
+ border-radius: 50%;
394
+ flex: none;
395
+ background: radial-gradient(
396
+ circle at center,
397
+ var(--rcu-pageBg) 0 17%,
398
+ var(--rcu-vinylDark) 18% 30%,
399
+ var(--rcu-vinylMid) 31% 33%,
400
+ var(--rcu-vinylDark) 34% 46%,
401
+ var(--rcu-vinylMid) 47% 49%,
402
+ var(--rcu-vinylDark) 50% 62%,
403
+ var(--rcu-vinylMid) 63% 65%,
404
+ var(--rcu-vinylDark) 66% 100%
405
+ );
406
+ border: 1px solid var(--rcu-cardBorder);
407
+ animation: rcu-rot 2.2s linear infinite;
408
+ box-shadow: 0 3px 12px var(--rcu-shadow);
409
+ }
410
+ .bar {
411
+ height: 12px;
412
+ border-radius: 999px;
413
+ overflow: hidden;
414
+ background: var(--rcu-sunk);
415
+ border: 1px solid var(--rcu-cardBorder);
416
+ }
417
+ .bar > div {
418
+ height: 100%;
419
+ width: 0%;
420
+ background: linear-gradient(
421
+ 90deg,
422
+ var(--rcu-text),
423
+ var(--rcu-contrast),
424
+ var(--rcu-textDim),
425
+ var(--rcu-text)
426
+ );
427
+ background-size: 300% 100%;
428
+ animation: rcu-sheen 2.2s linear infinite;
429
+ transition: width 0.3s ease;
430
+ border-radius: 999px;
431
+ }
432
+ .bar.indeterminate > div {
433
+ width: 100% !important;
434
+ opacity: 0.75;
435
+ }
436
+ .progress-label {
437
+ font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
438
+ font-size: 12px;
439
+ color: var(--rcu-textDim);
440
+ margin-top: 8px;
441
+ }
442
+
443
+ .okline {
444
+ color: var(--rcu-green);
445
+ font-weight: 600;
446
+ font-size: 14px;
447
+ }
448
+ .warnline {
449
+ color: var(--rcu-amber);
450
+ font-size: 13px;
451
+ margin-top: 8px;
452
+ }
453
+
454
+ /* ---------- platforms ---------- */
455
+ .platforms {
456
+ display: grid;
457
+ grid-template-columns: 1fr 1fr;
458
+ gap: 14px;
459
+ }
460
+ @media (max-width: 620px) {
461
+ .platforms {
462
+ grid-template-columns: 1fr;
463
+ }
464
+ }
465
+ .platform {
466
+ border: 1px solid var(--rcu-cardBorder);
467
+ border-radius: 14px;
468
+ padding: 18px;
469
+ background: var(--rcu-card);
470
+ transition: border-color 0.3s, box-shadow 0.3s;
471
+ }
472
+ .platform[data-platform='mixcloud'].glow {
473
+ border-color: var(--rcu-mixcloud);
474
+ box-shadow: 0 4px 18px var(--rcu-shadowSoft);
475
+ }
476
+ .platform[data-platform='soundcloud'].glow {
477
+ border-color: var(--rcu-soundcloud);
478
+ box-shadow: 0 4px 18px var(--rcu-shadowSoft);
479
+ }
480
+ .platform h3 {
481
+ font-size: 14px;
482
+ letter-spacing: 0.08em;
483
+ text-transform: uppercase;
484
+ margin: 0 0 10px;
485
+ }
486
+ .platform[data-platform='mixcloud'] h3 {
487
+ color: var(--rcu-mixcloud);
488
+ }
489
+ .platform[data-platform='soundcloud'] h3 {
490
+ color: var(--rcu-soundcloud);
491
+ }
492
+ .pstatus {
493
+ font-size: 13px;
494
+ color: var(--rcu-textDim);
495
+ min-height: 20px;
496
+ margin-bottom: 12px;
497
+ }
498
+ .pstatus a {
499
+ color: var(--rcu-text);
500
+ word-break: break-all;
501
+ }
502
+ .pulse {
503
+ display: inline-block;
504
+ width: 8px;
505
+ height: 8px;
506
+ border-radius: 50%;
507
+ background: var(--rcu-amber);
508
+ margin-right: 7px;
509
+ animation: rcu-pulse 1.4s ease infinite;
510
+ }
511
+ @keyframes rcu-pulse {
512
+ 50% {
513
+ opacity: 0.25;
514
+ transform: scale(0.8);
515
+ }
516
+ }
517
+
518
+ /* ---------- log ---------- */
519
+ .log {
520
+ font-family: 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
521
+ font-size: 12px;
522
+ line-height: 1.75;
523
+ background: var(--rcu-sunk);
524
+ border: 1px solid var(--rcu-cardBorder);
525
+ border-radius: 12px;
526
+ padding: 16px 18px;
527
+ max-height: 300px;
528
+ overflow-y: auto;
529
+ }
530
+ .log .entry {
531
+ animation: rcu-rise 0.3s ease both;
532
+ overflow-wrap: break-word;
533
+ }
534
+ .log .t {
535
+ color: var(--rcu-textDim);
536
+ margin-right: 8px;
537
+ }
538
+ .log .info {
539
+ color: var(--rcu-text);
540
+ }
541
+ .log .warn {
542
+ color: var(--rcu-amber);
543
+ }
544
+ .log .error {
545
+ color: var(--rcu-red);
546
+ }
547
+ .log .detail {
548
+ color: var(--rcu-textDim);
549
+ padding-left: 20px;
550
+ white-space: pre-wrap;
551
+ }
552
+ .log .cursor {
553
+ display: inline-block;
554
+ width: 8px;
555
+ height: 13px;
556
+ background: var(--rcu-text);
557
+ vertical-align: text-bottom;
558
+ animation: rcu-blink 1.1s steps(1) infinite;
559
+ }
560
+ @keyframes rcu-blink {
561
+ 50% {
562
+ opacity: 0;
563
+ }
564
+ }
565
+
566
+ /* ---------- toasts / notice ---------- */
567
+ .toasts {
568
+ position: fixed;
569
+ top: 18px;
570
+ right: 18px;
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 10px;
574
+ z-index: 50;
575
+ }
576
+ .toast {
577
+ padding: 13px 18px;
578
+ border-radius: 12px;
579
+ font-size: 13px;
580
+ font-weight: 600;
581
+ color: var(--rcu-buttonText);
582
+ background: var(--rcu-text);
583
+ border: 1px solid var(--rcu-contrast);
584
+ box-shadow: 0 10px 30px var(--rcu-shadow);
585
+ animation: rcu-slidein 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
586
+ max-width: 340px;
587
+ }
588
+ .toast.success {
589
+ border-color: var(--rcu-green);
590
+ }
591
+ .toast.danger {
592
+ border-color: var(--rcu-red);
593
+ }
594
+ @keyframes rcu-slidein {
595
+ from {
596
+ opacity: 0;
597
+ transform: translateX(40px);
598
+ }
599
+ }
600
+
601
+ .notice {
602
+ text-align: center;
603
+ padding: 60px 30px;
604
+ }
605
+ .notice .moon {
606
+ font-size: 44px;
607
+ display: block;
608
+ margin-bottom: 18px;
609
+ animation: rcu-bob 3s ease-in-out infinite;
610
+ }
611
+ @keyframes rcu-bob {
612
+ 50% {
613
+ transform: translateY(-8px);
614
+ }
615
+ }
616
+ .notice p {
617
+ color: var(--rcu-textDim);
618
+ margin-top: 10px;
619
+ line-height: 1.6;
620
+ }
621
+ `;
622
+ const DefaultLogo = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 62 62", "aria-hidden": "true", children: [
623
+ /* @__PURE__ */ jsx("circle", { cx: "31", cy: "31", r: "29", fill: "none", stroke: "currentColor", strokeWidth: "3" }),
624
+ /* @__PURE__ */ jsx("circle", { cx: "31", cy: "31", r: "6", fill: "currentColor" }),
625
+ /* @__PURE__ */ jsx(
626
+ "path",
627
+ {
628
+ d: "M31 44 V22 M22 30 L31 20 L40 30",
629
+ fill: "none",
630
+ stroke: "currentColor",
631
+ strokeWidth: "4",
632
+ strokeLinecap: "round",
633
+ strokeLinejoin: "round"
634
+ }
635
+ )
636
+ ] });
637
+ const Uploader = () => {
638
+ const { documentId } = useParams();
639
+ const token = useAuth("RadiocultUploader", (state) => state.token);
640
+ const [theme, setTheme] = React.useState(null);
641
+ const [status, setStatus] = React.useState(null);
642
+ const [notice, setNotice] = React.useState(null);
643
+ const [logEntries, setLogEntries] = React.useState([]);
644
+ const [toasts, setToasts] = React.useState([]);
645
+ const [form, setForm] = React.useState({
646
+ title: "",
647
+ artist: "",
648
+ album: "",
649
+ tags: "",
650
+ description: ""
651
+ });
652
+ const formReady = React.useRef(false);
653
+ const [file, setFile] = React.useState(null);
654
+ const [dragOver, setDragOver] = React.useState(false);
655
+ const [uploading, setUploading] = React.useState(false);
656
+ const [progress, setProgress] = React.useState(0);
657
+ const [warnings, setWarnings] = React.useState([]);
658
+ const [saving, setSaving] = React.useState(false);
659
+ const [publishing, setPublishing] = React.useState(null);
660
+ const [checking, setChecking] = React.useState(false);
661
+ const xhrRef = React.useRef(null);
662
+ const fileInput = React.useRef(null);
663
+ const statusRef = React.useRef(null);
664
+ const uploadingRef = React.useRef(false);
665
+ uploadingRef.current = uploading;
666
+ const log = React.useCallback(
667
+ (level, text, detail) => {
668
+ setLogEntries(
669
+ (entries) => [
670
+ { id: nextId++, time: (/* @__PURE__ */ new Date()).toLocaleTimeString("en-GB"), level, text, detail },
671
+ ...entries
672
+ ].slice(0, 200)
673
+ );
674
+ },
675
+ []
676
+ );
677
+ const toast = React.useCallback((kind, text) => {
678
+ const id = nextId++;
679
+ setToasts((all) => [...all, { id, kind, text }]);
680
+ setTimeout(() => setToasts((all) => all.filter((t) => t.id !== id)), 5200);
681
+ }, []);
682
+ const loadStatus = React.useCallback(
683
+ async (refresh) => {
684
+ if (!documentId) return null;
685
+ try {
686
+ const data = await fetchStatus(documentId, refresh);
687
+ const prev = statusRef.current;
688
+ statusRef.current = data;
689
+ PLATFORMS.forEach((p) => {
690
+ if (!prev?.platforms[p].link && data.platforms[p].link) {
691
+ log("info", `${PLATFORM_LABEL[p]} link saved to the entry`, data.platforms[p].link);
692
+ toast("success", `${PLATFORM_LABEL[p]} link saved to the entry ✨`);
693
+ }
694
+ });
695
+ if (data.rateLimitMessage) log("warn", data.rateLimitMessage);
696
+ if (!formReady.current) {
697
+ const d = data.details;
698
+ setForm({
699
+ title: d.title || "",
700
+ artist: d.artist || "",
701
+ album: d.album || "",
702
+ tags: (d.genres || []).join(", "),
703
+ description: d.description || ""
704
+ });
705
+ formReady.current = true;
706
+ log("info", "Entry loaded", `documentId ${documentId}`);
707
+ }
708
+ const tabName = data.details.showName || data.details.title || "";
709
+ document.title = tabName ? `↑${tabName}` : "↑ Episode Uploader";
710
+ setStatus(data);
711
+ setNotice(null);
712
+ return data;
713
+ } catch (error) {
714
+ const message = apiError(error);
715
+ log("error", "Could not load the entry status", message);
716
+ if (!statusRef.current) {
717
+ setNotice({
718
+ title: "Could not load the entry",
719
+ text: `${message} — check the plugin settings and the server, then reload this page.`
720
+ });
721
+ }
722
+ return null;
723
+ }
724
+ },
725
+ [documentId, log, toast]
726
+ );
727
+ React.useEffect(() => {
728
+ fetchUiConfig().then(({ theme: t }) => setTheme(t)).catch(() => setTheme(null));
729
+ loadStatus(false);
730
+ }, [loadStatus]);
731
+ useGoogleFont(googleFontHref(resolveFont(theme)));
732
+ const anyPublishing = PLATFORMS.some((p) => status?.platforms[p].state === "publishing");
733
+ React.useEffect(() => {
734
+ if (!anyPublishing) return void 0;
735
+ const timer = setInterval(() => {
736
+ if (document.visibilityState === "visible" && !uploadingRef.current) loadStatus(false);
737
+ }, POLL_MS);
738
+ return () => clearInterval(timer);
739
+ }, [anyPublishing, loadStatus]);
740
+ React.useEffect(() => {
741
+ const handler = (e) => {
742
+ if (uploadingRef.current) {
743
+ e.preventDefault();
744
+ e.returnValue = "";
745
+ }
746
+ };
747
+ window.addEventListener("beforeunload", handler);
748
+ return () => window.removeEventListener("beforeunload", handler);
749
+ }, []);
750
+ const pickFile = (f) => {
751
+ if (f) {
752
+ const ext = (f.name.match(/\.[^.]+$/) || [""])[0].toLowerCase();
753
+ if (ext !== ".mp3" && ext !== ".m4a") {
754
+ log("error", `Rejected "${f.name}"`, "Radio Cult only accepts .mp3 or .m4a");
755
+ toast("danger", "Only .mp3 or .m4a files can be uploaded");
756
+ return;
757
+ }
758
+ if (f.size > MAX_BYTES) {
759
+ log("error", `Rejected "${f.name}"`, `Over Radio Cult’s 750MB limit (${mb(f.size)})`);
760
+ toast("danger", "File is over the 750MB limit");
761
+ return;
762
+ }
763
+ log("info", `Selected "${f.name}" (${mb(f.size)})`);
764
+ }
765
+ setFile(f);
766
+ };
767
+ const startUpload = (force) => {
768
+ if (!file || uploading || !documentId || !token) return;
769
+ const meta = { title: form.title.trim(), artist: form.artist.trim(), album: form.album.trim() };
770
+ setUploading(true);
771
+ setProgress(0);
772
+ log(
773
+ "info",
774
+ `${force ? "Replacing track with" : "Uploading"} "${file.name}" (${mb(file.size)})`,
775
+ `title "${meta.title}" · artist "${meta.artist}" · album "${meta.album}"`
776
+ );
777
+ xhrRef.current = uploadTrack(documentId, file, meta, force, token, {
778
+ onProgress: setProgress,
779
+ onDone: (body) => {
780
+ log("info", `Track uploaded to Radio Cult (id ${body.trackId})`);
781
+ (body.warnings || []).forEach((w) => log("warn", w));
782
+ setWarnings(body.warnings || []);
783
+ toast("success", "Episode uploaded to Radio Cult 🎉");
784
+ setFile(null);
785
+ setUploading(false);
786
+ loadStatus(false);
787
+ },
788
+ onConflict: (message) => {
789
+ setUploading(false);
790
+ if (window.confirm(
791
+ `${message}
792
+
793
+ Replace it with the new file? The old track stays in the Radio Cult library until you delete it there.`
794
+ )) {
795
+ startUpload(true);
796
+ } else {
797
+ log("warn", "Upload cancelled — entry already has a Radio Cult track");
798
+ }
799
+ },
800
+ onError: (message, httpStatus) => {
801
+ log("error", "Upload to Radio Cult failed", `${message}${httpStatus ? ` (HTTP ${httpStatus})` : ""}`);
802
+ toast("danger", "Upload to Radio Cult failed");
803
+ setUploading(false);
804
+ },
805
+ onAbort: () => {
806
+ log("warn", "Upload cancelled");
807
+ setUploading(false);
808
+ }
809
+ });
810
+ };
811
+ const saveDetails = async () => {
812
+ if (saving || !documentId) return;
813
+ setSaving(true);
814
+ const meta = { title: form.title.trim(), artist: form.artist.trim(), album: form.album.trim() };
815
+ try {
816
+ await saveTrackDetails(documentId, meta);
817
+ log(
818
+ "info",
819
+ "Track details saved to Radio Cult",
820
+ `title "${meta.title}" · artist "${meta.artist}" · album "${meta.album}"`
821
+ );
822
+ toast("success", "Track details saved to Radio Cult");
823
+ } catch (error) {
824
+ log("error", "Saving track details failed", apiError(error));
825
+ toast("danger", "Saving track details failed");
826
+ } finally {
827
+ setSaving(false);
828
+ }
829
+ };
830
+ const doPublish = async (platform, force) => {
831
+ if (publishing || !documentId) return;
832
+ setPublishing(platform);
833
+ const tags = form.tags.split(",").map((t) => t.trim()).filter(Boolean);
834
+ log("info", `Requesting ${PLATFORM_LABEL[platform]} publish`, `title "${form.title.trim()}" · tags [${tags.join(", ")}]`);
835
+ try {
836
+ await publishTrack(
837
+ documentId,
838
+ platform,
839
+ { title: form.title.trim(), tags, description: form.description },
840
+ force
841
+ );
842
+ log(
843
+ "info",
844
+ `${PLATFORM_LABEL[platform]} publish requested — Radio Cult is transferring; the link lands on the entry automatically`
845
+ );
846
+ toast("success", `Publish to ${PLATFORM_LABEL[platform]} requested`);
847
+ setPublishing(null);
848
+ await loadStatus(false);
849
+ } catch (error) {
850
+ setPublishing(null);
851
+ const message = apiError(error);
852
+ const httpStatus = error?.response?.status;
853
+ if (httpStatus === 409 && !force) {
854
+ if (window.confirm(`${message}
855
+
856
+ Send the request anyway?`)) return doPublish(platform, true);
857
+ log("warn", `${PLATFORM_LABEL[platform]} publish not sent`, message);
858
+ } else {
859
+ log("error", `${PLATFORM_LABEL[platform]} publish failed`, message);
860
+ toast("danger", `${PLATFORM_LABEL[platform]} publish failed`);
861
+ }
862
+ }
863
+ };
864
+ const checkNow = async () => {
865
+ if (checking) return;
866
+ setChecking(true);
867
+ log("info", "Checking Radio Cult for new links…");
868
+ const data = await loadStatus(true);
869
+ if (data) {
870
+ log(
871
+ "info",
872
+ `Check finished — ${PLATFORMS.map((p) => `${PLATFORM_LABEL[p]}: ${data.platforms[p].state.replace("_", " ")}`).join(" · ")}`
873
+ );
874
+ }
875
+ setChecking(false);
876
+ };
877
+ const cssVars = themeToCssVars(theme);
878
+ const stationName = theme?.stationName || "";
879
+ const hasTrack = !!status?.trackId;
880
+ const allLinked = !!status && PLATFORMS.every((p) => status.platforms[p].state === "linked");
881
+ const busyAnim = uploading || anyPublishing;
882
+ const renderBody = () => {
883
+ if (!documentId) {
884
+ return /* @__PURE__ */ jsx(Notice, { title: "No entry selected", text: "Open this page with the uploader button on an entry's edit page in the Strapi admin." });
885
+ }
886
+ if (notice) return /* @__PURE__ */ jsx(Notice, { title: notice.title, text: notice.text });
887
+ if (!status) return /* @__PURE__ */ jsx(Notice, { title: "Loading…", text: "Fetching the entry's Radio Cult status." });
888
+ if (!status.enabled) {
889
+ return /* @__PURE__ */ jsx(
890
+ Notice,
891
+ {
892
+ title: "Radio Cult is switched off",
893
+ text: "Uploads are disabled — check the API key, station ID and enabled toggle in Settings → Radio Cult Uploader."
894
+ }
895
+ );
896
+ }
897
+ return /* @__PURE__ */ jsxs("div", { children: [
898
+ /* @__PURE__ */ jsxs("section", { className: `card ${hasTrack ? "done" : "active"}`, children: [
899
+ /* @__PURE__ */ jsxs("div", { className: "step-head", children: [
900
+ /* @__PURE__ */ jsx("div", { className: "badge", children: hasTrack ? "✓" : "1" }),
901
+ /* @__PURE__ */ jsx("h2", { children: "Track details" }),
902
+ /* @__PURE__ */ jsxs("div", { className: "hint", children: [
903
+ "Prefilled from the entry — edit freely.",
904
+ /* @__PURE__ */ jsx("br", {}),
905
+ "Changes never touch the Strapi entry."
906
+ ] })
907
+ ] }),
908
+ /* @__PURE__ */ jsxs("div", { className: "grid", children: [
909
+ /* @__PURE__ */ jsxs("div", { className: "full", children: [
910
+ /* @__PURE__ */ jsx("label", { htmlFor: "f-title", children: "Title" }),
911
+ /* @__PURE__ */ jsx(
912
+ "input",
913
+ {
914
+ type: "text",
915
+ id: "f-title",
916
+ autoComplete: "off",
917
+ value: form.title,
918
+ onChange: (e) => setForm({ ...form, title: e.target.value })
919
+ }
920
+ )
921
+ ] }),
922
+ /* @__PURE__ */ jsxs("div", { children: [
923
+ /* @__PURE__ */ jsx("label", { htmlFor: "f-artist", children: "Artist" }),
924
+ /* @__PURE__ */ jsx(
925
+ "input",
926
+ {
927
+ type: "text",
928
+ id: "f-artist",
929
+ autoComplete: "off",
930
+ value: form.artist,
931
+ onChange: (e) => setForm({ ...form, artist: e.target.value })
932
+ }
933
+ )
934
+ ] }),
935
+ /* @__PURE__ */ jsxs("div", { children: [
936
+ /* @__PURE__ */ jsx("label", { htmlFor: "f-album", children: "Album" }),
937
+ /* @__PURE__ */ jsx(
938
+ "input",
939
+ {
940
+ type: "text",
941
+ id: "f-album",
942
+ autoComplete: "off",
943
+ value: form.album,
944
+ onChange: (e) => setForm({ ...form, album: e.target.value })
945
+ }
946
+ )
947
+ ] }),
948
+ /* @__PURE__ */ jsxs("div", { className: "full", children: [
949
+ /* @__PURE__ */ jsx("label", { htmlFor: "f-tags", children: "Tags" }),
950
+ /* @__PURE__ */ jsx(
951
+ "input",
952
+ {
953
+ type: "text",
954
+ id: "f-tags",
955
+ autoComplete: "off",
956
+ value: form.tags,
957
+ onChange: (e) => setForm({ ...form, tags: e.target.value })
958
+ }
959
+ ),
960
+ /* @__PURE__ */ jsx("div", { className: "fine", children: "Comma-separated. Used on Mixcloud (first 5) and SoundCloud." })
961
+ ] }),
962
+ /* @__PURE__ */ jsxs("div", { className: "full", children: [
963
+ /* @__PURE__ */ jsx("label", { htmlFor: "f-desc", children: "Description" }),
964
+ /* @__PURE__ */ jsx(
965
+ "textarea",
966
+ {
967
+ id: "f-desc",
968
+ value: form.description,
969
+ onChange: (e) => setForm({ ...form, description: e.target.value })
970
+ }
971
+ ),
972
+ /* @__PURE__ */ jsx("div", { className: "fine", children: "Used on Mixcloud and SoundCloud." })
973
+ ] })
974
+ ] }),
975
+ /* @__PURE__ */ jsx("div", { className: "artwork", children: status.details.image?.previewUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
976
+ /* @__PURE__ */ jsx("img", { src: status.details.image.previewUrl, alt: status.details.image.name }),
977
+ /* @__PURE__ */ jsxs("div", { className: "fine", children: [
978
+ "Artwork comes from the entry image — change it on the entry page.",
979
+ status.details.image.usable ? "" : " ⚠ No jpeg/png version under 2MB, so Mixcloud/SoundCloud may get no artwork."
980
+ ] })
981
+ ] }) : /* @__PURE__ */ jsx("div", { className: "warnline", children: "No image set on the entry — the track and publishes will have no artwork." }) }),
982
+ hasTrack ? /* @__PURE__ */ jsxs("div", { className: "row", children: [
983
+ /* @__PURE__ */ jsxs("button", { className: "btn ghost small", onClick: saveDetails, disabled: saving, children: [
984
+ saving ? /* @__PURE__ */ jsx("span", { className: "spin" }) : null,
985
+ "Save details to Radio Cult"
986
+ ] }),
987
+ /* @__PURE__ */ jsx("span", { className: "fine", children: "Pushes title / artist / album to the already-uploaded track." })
988
+ ] }) : null
989
+ ] }),
990
+ /* @__PURE__ */ jsxs("section", { className: `card ${hasTrack ? "done" : "active"}`, children: [
991
+ /* @__PURE__ */ jsxs("div", { className: "step-head", children: [
992
+ /* @__PURE__ */ jsx("div", { className: "badge", children: hasTrack ? "✓" : "2" }),
993
+ /* @__PURE__ */ jsx("h2", { children: "Upload to Radio Cult" }),
994
+ /* @__PURE__ */ jsx("div", { className: "hint", children: hasTrack ? "Pick a new file only if you need to replace the audio" : ".mp3 or .m4a · up to 750MB" })
995
+ ] }),
996
+ hasTrack ? /* @__PURE__ */ jsxs("div", { className: "okline", children: [
997
+ "✓ Track uploaded",
998
+ status.uploadedAt ? ` on ${new Date(status.uploadedAt).toLocaleString("en-GB")}` : "",
999
+ " (id",
1000
+ " ",
1001
+ status.trackId,
1002
+ ")"
1003
+ ] }) : null,
1004
+ !uploading ? /* @__PURE__ */ jsxs("div", { children: [
1005
+ /* @__PURE__ */ jsxs(
1006
+ "div",
1007
+ {
1008
+ className: `dropzone ${dragOver ? "over" : ""}`,
1009
+ onClick: () => fileInput.current?.click(),
1010
+ onDragEnter: (e) => {
1011
+ e.preventDefault();
1012
+ setDragOver(true);
1013
+ },
1014
+ onDragOver: (e) => {
1015
+ e.preventDefault();
1016
+ setDragOver(true);
1017
+ },
1018
+ onDragLeave: (e) => {
1019
+ e.preventDefault();
1020
+ setDragOver(false);
1021
+ },
1022
+ onDrop: (e) => {
1023
+ e.preventDefault();
1024
+ setDragOver(false);
1025
+ if (e.dataTransfer.files[0]) pickFile(e.dataTransfer.files[0]);
1026
+ },
1027
+ children: [
1028
+ /* @__PURE__ */ jsx("div", { className: "big", children: "Drop the episode audio here" }),
1029
+ /* @__PURE__ */ jsx("div", { className: "fine", children: "or click to browse — mp3 gets the entry image embedded as artwork" })
1030
+ ]
1031
+ }
1032
+ ),
1033
+ /* @__PURE__ */ jsx(
1034
+ "input",
1035
+ {
1036
+ ref: fileInput,
1037
+ type: "file",
1038
+ accept: ".mp3,.m4a,audio/mpeg,audio/mp4",
1039
+ style: { display: "none" },
1040
+ onChange: (e) => pickFile(e.target.files?.[0] ?? null)
1041
+ }
1042
+ ),
1043
+ file ? /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("div", { className: "filechip", children: [
1044
+ /* @__PURE__ */ jsxs("span", { children: [
1045
+ "♫ ",
1046
+ file.name,
1047
+ " · ",
1048
+ mb(file.size)
1049
+ ] }),
1050
+ /* @__PURE__ */ jsx(
1051
+ "span",
1052
+ {
1053
+ className: "x",
1054
+ title: "Remove",
1055
+ onClick: () => pickFile(null),
1056
+ children: "✕"
1057
+ }
1058
+ )
1059
+ ] }) }) : null,
1060
+ /* @__PURE__ */ jsx("div", { className: "row", children: /* @__PURE__ */ jsx(
1061
+ "button",
1062
+ {
1063
+ className: `btn ${hasTrack ? "danger" : ""}`,
1064
+ disabled: !file,
1065
+ onClick: () => startUpload(false),
1066
+ children: hasTrack ? "Replace track on Radio Cult" : "Upload to Radio Cult"
1067
+ }
1068
+ ) })
1069
+ ] }) : /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("div", { className: "row", style: { marginTop: 4 }, children: [
1070
+ /* @__PURE__ */ jsx("div", { className: "vinyl" }),
1071
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
1072
+ /* @__PURE__ */ jsx("div", { className: `bar ${progress === null ? "indeterminate" : ""}`, children: /* @__PURE__ */ jsx("div", { style: { width: `${progress ?? 100}%` } }) }),
1073
+ /* @__PURE__ */ jsx("div", { className: "progress-label", children: progress === null ? "File received — Radio Cult is processing… keep this tab open." : `Sending file… ${progress}%` })
1074
+ ] }),
1075
+ /* @__PURE__ */ jsx("button", { className: "btn danger small", onClick: () => xhrRef.current?.abort(), children: "Cancel" })
1076
+ ] }) }),
1077
+ /* @__PURE__ */ jsx("div", { children: warnings.map((w) => /* @__PURE__ */ jsxs("div", { className: "warnline", children: [
1078
+ "⚠ ",
1079
+ w
1080
+ ] }, w)) })
1081
+ ] }),
1082
+ /* @__PURE__ */ jsxs(
1083
+ "section",
1084
+ {
1085
+ className: `card ${!hasTrack ? "locked" : allLinked ? "done" : "active"}`,
1086
+ children: [
1087
+ /* @__PURE__ */ jsxs("div", { className: "step-head", children: [
1088
+ /* @__PURE__ */ jsx("div", { className: "badge", children: allLinked ? "✓" : "3" }),
1089
+ /* @__PURE__ */ jsx("h2", { children: "Publish to Mixcloud & SoundCloud" }),
1090
+ /* @__PURE__ */ jsx("div", { className: "hint", children: hasTrack ? "" : "Unlocks after step 2 — publishing goes through Radio Cult." })
1091
+ ] }),
1092
+ /* @__PURE__ */ jsx("div", { className: "platforms", children: PLATFORMS.map((p) => {
1093
+ const s = status.platforms[p];
1094
+ return /* @__PURE__ */ jsxs(
1095
+ "div",
1096
+ {
1097
+ className: `platform ${s.state === "publishing" || s.state === "linked" ? "glow" : ""}`,
1098
+ "data-platform": p,
1099
+ children: [
1100
+ /* @__PURE__ */ jsx("h3", { children: PLATFORM_LABEL[p] }),
1101
+ /* @__PURE__ */ jsx("div", { className: "pstatus", children: s.state === "linked" && s.link ? /* @__PURE__ */ jsx("a", { href: s.link, target: "_blank", rel: "noreferrer", children: s.link }) : s.state === "publishing" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1102
+ /* @__PURE__ */ jsx("span", { className: "pulse" }),
1103
+ "Publishing… link is saved to the entry automatically."
1104
+ ] }) : s.state === "stalled" ? "No link after 7 days — the transfer probably failed." : "Not published yet." }),
1105
+ s.state !== "linked" && s.state !== "publishing" ? /* @__PURE__ */ jsxs(
1106
+ "button",
1107
+ {
1108
+ className: "btn small",
1109
+ disabled: !hasTrack || !!publishing,
1110
+ onClick: () => doPublish(p, false),
1111
+ children: [
1112
+ publishing === p ? /* @__PURE__ */ jsx("span", { className: "spin" }) : null,
1113
+ s.state === "stalled" ? "Publish again" : `Publish to ${PLATFORM_LABEL[p]}`
1114
+ ]
1115
+ }
1116
+ ) : null
1117
+ ]
1118
+ },
1119
+ p
1120
+ );
1121
+ }) }),
1122
+ /* @__PURE__ */ jsxs("div", { className: "row", children: [
1123
+ /* @__PURE__ */ jsxs("button", { className: "btn ghost small", onClick: checkNow, disabled: checking, children: [
1124
+ checking ? /* @__PURE__ */ jsx("span", { className: "spin" }) : null,
1125
+ "Check for links now"
1126
+ ] }),
1127
+ /* @__PURE__ */ jsx("span", { className: "fine", children: "Links are also checked automatically every 10 minutes and saved straight onto the entry." })
1128
+ ] })
1129
+ ]
1130
+ }
1131
+ ),
1132
+ /* @__PURE__ */ jsxs("section", { className: "card", children: [
1133
+ /* @__PURE__ */ jsxs("div", { className: "step-head", children: [
1134
+ /* @__PURE__ */ jsx("div", { className: "badge", style: { boxShadow: "none" }, children: "▤" }),
1135
+ /* @__PURE__ */ jsx("h2", { children: "Activity log" })
1136
+ ] }),
1137
+ /* @__PURE__ */ jsxs("div", { className: "log", children: [
1138
+ /* @__PURE__ */ jsx("span", { className: "cursor" }),
1139
+ logEntries.map((entry) => /* @__PURE__ */ jsxs("div", { className: "entry", children: [
1140
+ /* @__PURE__ */ jsx("span", { className: "t", children: entry.time }),
1141
+ /* @__PURE__ */ jsx("span", { className: entry.level, children: entry.text }),
1142
+ entry.detail ? /* @__PURE__ */ jsx("div", { className: "detail", children: entry.detail }) : null
1143
+ ] }, entry.id))
1144
+ ] })
1145
+ ] })
1146
+ ] });
1147
+ };
1148
+ return /* @__PURE__ */ jsxs(Page, { style: cssVars, children: [
1149
+ /* @__PURE__ */ jsxs("div", { className: "wrap", children: [
1150
+ /* @__PURE__ */ jsxs("header", { children: [
1151
+ /* @__PURE__ */ jsx("div", { className: `eq ${busyAnim ? "" : "paused"}`, children: theme?.logoUrl ? /* @__PURE__ */ jsx("img", { src: theme.logoUrl, alt: stationName || "logo" }) : /* @__PURE__ */ jsx(DefaultLogo, {}) }),
1152
+ /* @__PURE__ */ jsxs("div", { children: [
1153
+ /* @__PURE__ */ jsx("h1", { children: "Episode Uploader" }),
1154
+ /* @__PURE__ */ jsxs("div", { className: "strap", children: [
1155
+ stationName ? `${stationName} · ` : "",
1156
+ /* @__PURE__ */ jsx("b", { children: status?.details.title || "loading entry…" })
1157
+ ] })
1158
+ ] })
1159
+ ] }),
1160
+ renderBody()
1161
+ ] }),
1162
+ /* @__PURE__ */ jsx("div", { className: "toasts", children: toasts.map((t) => /* @__PURE__ */ jsx("div", { className: `toast ${t.kind}`, children: t.text }, t.id)) })
1163
+ ] });
1164
+ };
1165
+ const Notice = ({ title, text }) => /* @__PURE__ */ jsxs("section", { className: "card notice", children: [
1166
+ /* @__PURE__ */ jsx("span", { className: "moon", children: "🌙" }),
1167
+ /* @__PURE__ */ jsx("h2", { children: title }),
1168
+ /* @__PURE__ */ jsx("p", { children: text })
1169
+ ] });
1170
+ const App = () => {
1171
+ return /* @__PURE__ */ jsxs(Routes, { children: [
1172
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(HomePage, {}) }),
1173
+ /* @__PURE__ */ jsx(Route, { path: "upload/:documentId", element: /* @__PURE__ */ jsx(Uploader, {}) }),
1174
+ /* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(Page$1.Error, {}) })
1175
+ ] });
1176
+ };
1177
+ export {
1178
+ App as default
1179
+ };