@nuasite/cms 0.18.1 → 0.19.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/dist/editor.js +52746 -36711
- package/package.json +16 -14
- package/src/build-processor.ts +4 -1
- package/src/collection-scanner.ts +425 -48
- package/src/dev-middleware.ts +26 -203
- package/src/editor/api.ts +1 -22
- package/src/editor/components/ai-chat.tsx +3 -3
- package/src/editor/components/ai-tooltip.tsx +2 -1
- package/src/editor/components/block-editor.tsx +13 -108
- package/src/editor/components/collections-browser.tsx +168 -205
- package/src/editor/components/component-card.tsx +49 -0
- package/src/editor/components/confirm-dialog.tsx +34 -47
- package/src/editor/components/create-page-modal.tsx +529 -101
- package/src/editor/components/delete-page-dialog.tsx +100 -0
- package/src/editor/components/fields.tsx +175 -0
- package/src/editor/components/frontmatter-fields.tsx +281 -70
- package/src/editor/components/frontmatter-sidebar.tsx +223 -0
- package/src/editor/components/highlight-overlay.ts +3 -2
- package/src/editor/components/markdown-editor-overlay.tsx +131 -85
- package/src/editor/components/markdown-inline-editor.tsx +74 -5
- package/src/editor/components/mdx-block-view.tsx +102 -0
- package/src/editor/components/mdx-component-picker.tsx +123 -0
- package/src/editor/components/mdx-props-editor.tsx +94 -0
- package/src/editor/components/media-library.tsx +373 -100
- package/src/editor/components/modal-shell.tsx +87 -0
- package/src/editor/components/prop-editor.tsx +52 -0
- package/src/editor/components/redirect-countdown.tsx +3 -1
- package/src/editor/components/redirects-manager.tsx +269 -0
- package/src/editor/components/reference-picker.tsx +203 -0
- package/src/editor/components/seo-editor.tsx +285 -303
- package/src/editor/components/toast/toast-container.tsx +2 -1
- package/src/editor/components/toolbar.tsx +177 -46
- package/src/editor/constants.ts +26 -0
- package/src/editor/editor.ts +112 -0
- package/src/editor/fetch.ts +62 -0
- package/src/editor/index.tsx +19 -1
- package/src/editor/markdown-api.ts +105 -156
- package/src/editor/milkdown-mdx-plugin.tsx +269 -0
- package/src/editor/signals.ts +206 -13
- package/src/editor/types.ts +52 -1
- package/src/handlers/api-routes.ts +251 -0
- package/src/handlers/component-ops.ts +2 -18
- package/src/handlers/markdown-ops.ts +202 -47
- package/src/handlers/page-ops.ts +229 -0
- package/src/handlers/redirect-ops.ts +163 -0
- package/src/handlers/source-writer.ts +157 -1
- package/src/html-processor.ts +14 -2
- package/src/index.ts +76 -2
- package/src/manifest-writer.ts +19 -1
- package/src/media/contember.ts +2 -1
- package/src/media/local.ts +66 -28
- package/src/media/project-images.ts +81 -0
- package/src/media/s3.ts +32 -11
- package/src/media/types.ts +24 -2
- package/src/shared.ts +27 -0
- package/src/source-finder/collection-finder.ts +219 -41
- package/src/source-finder/index.ts +7 -1
- package/src/source-finder/search-index.ts +178 -36
- package/src/source-finder/snippet-utils.ts +423 -3
- package/src/types.ts +111 -2
- package/src/utils.ts +40 -4
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../signals'
|
|
17
17
|
import type { ChangePayload, PageSeoData, PendingSeoChange } from '../types'
|
|
18
18
|
import { ColorField, ComboBoxField, ImageField } from './fields'
|
|
19
|
+
import { CloseButton, ModalBackdrop } from './modal-shell'
|
|
19
20
|
|
|
20
21
|
const OG_TYPE_OPTIONS = [
|
|
21
22
|
{ value: 'website', label: 'Website', description: 'Default type for most pages' },
|
|
@@ -256,341 +257,322 @@ export function SeoEditor() {
|
|
|
256
257
|
)
|
|
257
258
|
|
|
258
259
|
return (
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{/* Header */}
|
|
270
|
-
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
271
|
-
<div class="flex items-center gap-3">
|
|
272
|
-
<h2 class="text-lg font-semibold text-white">SEO Settings</h2>
|
|
273
|
-
{dirtyCount > 0 && (
|
|
274
|
-
<span class="px-2 py-0.5 text-xs font-medium bg-cms-primary/20 text-cms-primary rounded-full">
|
|
275
|
-
{dirtyCount} change{dirtyCount !== 1 ? 's' : ''}
|
|
276
|
-
</span>
|
|
277
|
-
)}
|
|
278
|
-
</div>
|
|
279
|
-
<button
|
|
280
|
-
type="button"
|
|
281
|
-
onClick={handleClose}
|
|
282
|
-
class="text-white/50 hover:text-white p-1.5 hover:bg-white/10 rounded-full transition-colors"
|
|
283
|
-
data-cms-ui
|
|
284
|
-
>
|
|
285
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
286
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
287
|
-
</svg>
|
|
288
|
-
</button>
|
|
260
|
+
<ModalBackdrop onClose={handleClose} maxWidth="max-w-2xl" extraClass="max-h-[85vh] flex flex-col">
|
|
261
|
+
{/* Header */}
|
|
262
|
+
<div class="flex items-center justify-between p-5 border-b border-white/10">
|
|
263
|
+
<div class="flex items-center gap-3">
|
|
264
|
+
<h2 class="text-lg font-semibold text-white">SEO Settings</h2>
|
|
265
|
+
{dirtyCount > 0 && (
|
|
266
|
+
<span class="px-2 py-0.5 text-xs font-medium bg-cms-primary/20 text-cms-primary rounded-full">
|
|
267
|
+
{dirtyCount} change{dirtyCount !== 1 ? 's' : ''}
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
289
270
|
</div>
|
|
271
|
+
<CloseButton onClick={handleClose} />
|
|
272
|
+
</div>
|
|
290
273
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
274
|
+
{/* Content */}
|
|
275
|
+
<div class="flex-1 overflow-auto p-5">
|
|
276
|
+
{!hasSeoData
|
|
277
|
+
? (
|
|
278
|
+
<div class="flex flex-col items-center justify-center h-48 text-white/50">
|
|
279
|
+
<svg class="w-12 h-12 mb-3 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
280
|
+
<path
|
|
281
|
+
stroke-linecap="round"
|
|
282
|
+
stroke-linejoin="round"
|
|
283
|
+
stroke-width="1.5"
|
|
284
|
+
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
285
|
+
/>
|
|
286
|
+
</svg>
|
|
287
|
+
<p class="text-sm">No SEO data found for this page.</p>
|
|
288
|
+
<p class="text-xs text-white/40 mt-1">Add title, meta tags, or Open Graph tags to your page.</p>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
: (
|
|
292
|
+
<div class="space-y-8">
|
|
293
|
+
{/* Basic SEO */}
|
|
294
|
+
<SeoSection title="Basic SEO">
|
|
295
|
+
{seoData.title && (
|
|
296
|
+
<SeoField
|
|
297
|
+
label="Page Title"
|
|
298
|
+
id={seoData.title.id}
|
|
299
|
+
value={seoData.title.content}
|
|
300
|
+
placeholder="Enter page title..."
|
|
301
|
+
onChange={handleFieldChange}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
304
|
+
{seoData.description && (
|
|
305
|
+
<SeoField
|
|
306
|
+
label="Meta Description"
|
|
307
|
+
id={seoData.description.id}
|
|
308
|
+
value={seoData.description.content}
|
|
309
|
+
placeholder="Enter meta description..."
|
|
310
|
+
multiline
|
|
311
|
+
onChange={handleFieldChange}
|
|
302
312
|
/>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
)}
|
|
314
|
+
{seoData.keywords && (
|
|
315
|
+
<SeoField
|
|
316
|
+
label="Keywords"
|
|
317
|
+
id={seoData.keywords.id}
|
|
318
|
+
value={seoData.keywords.content}
|
|
319
|
+
placeholder="keyword1, keyword2, keyword3..."
|
|
320
|
+
onChange={handleFieldChange}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
{seoData.canonical && (
|
|
324
|
+
<SeoField
|
|
325
|
+
label="Canonical URL"
|
|
326
|
+
id={seoData.canonical.id}
|
|
327
|
+
value={seoData.canonical.href}
|
|
328
|
+
placeholder="https://example.com/page"
|
|
329
|
+
onChange={handleFieldChange}
|
|
330
|
+
/>
|
|
331
|
+
)}
|
|
332
|
+
{seoData.robots && (
|
|
333
|
+
<ComboBoxField
|
|
334
|
+
label="Robots"
|
|
335
|
+
value={robots.current}
|
|
336
|
+
placeholder="index, follow"
|
|
337
|
+
options={ROBOTS_OPTIONS}
|
|
338
|
+
onChange={(v) => {
|
|
339
|
+
if (robots.id) handleFieldChange(robots.id, v, robots.original)
|
|
340
|
+
}}
|
|
341
|
+
isDirty={robots.dirty}
|
|
342
|
+
/>
|
|
343
|
+
)}
|
|
344
|
+
{seoData.themeColor && (
|
|
345
|
+
<ColorField
|
|
346
|
+
label="Theme Color"
|
|
347
|
+
value={themeColor.current}
|
|
348
|
+
placeholder="#000000"
|
|
349
|
+
onChange={(v) => {
|
|
350
|
+
if (themeColor.id) handleFieldChange(themeColor.id, v, themeColor.original)
|
|
351
|
+
}}
|
|
352
|
+
isDirty={themeColor.dirty}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</SeoSection>
|
|
356
|
+
|
|
357
|
+
{/* Favicons */}
|
|
358
|
+
{seoData.favicons && seoData.favicons.length > 0 && (
|
|
359
|
+
<SeoSection title="Favicons">
|
|
360
|
+
{seoData.favicons.map((favicon, index) => {
|
|
361
|
+
const faviconId = favicon.id
|
|
362
|
+
const originalValue = favicon.href
|
|
363
|
+
const pendingChange = faviconId ? getPendingSeoChange(faviconId) : undefined
|
|
364
|
+
const currentValue = pendingChange?.newValue ?? originalValue ?? ''
|
|
365
|
+
const isDirty = pendingChange?.isDirty ?? false
|
|
366
|
+
|
|
367
|
+
const label = favicon.sizes
|
|
368
|
+
? `Favicon (${favicon.sizes})`
|
|
369
|
+
: favicon.type
|
|
370
|
+
? `Favicon (${favicon.type.replace('image/', '')})`
|
|
371
|
+
: `Favicon${seoData.favicons!.length > 1 ? ` ${index + 1}` : ''}`
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
<div key={faviconId || index} class="space-y-1.5">
|
|
375
|
+
<ImageField
|
|
376
|
+
label={label}
|
|
377
|
+
value={currentValue}
|
|
378
|
+
placeholder="/favicon.svg"
|
|
379
|
+
onChange={(v) => {
|
|
380
|
+
if (faviconId) handleFieldChange(faviconId, v, originalValue)
|
|
381
|
+
}}
|
|
382
|
+
onBrowse={() => {
|
|
383
|
+
openMediaLibraryWithCallback((url: string) => {
|
|
384
|
+
if (faviconId) handleFieldChange(faviconId, url, originalValue)
|
|
385
|
+
})
|
|
386
|
+
}}
|
|
387
|
+
isDirty={isDirty}
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
})}
|
|
392
|
+
</SeoSection>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{/* Open Graph */}
|
|
396
|
+
{seoData.openGraph && Object.keys(seoData.openGraph).length > 0 && (
|
|
397
|
+
<SeoSection title="Open Graph">
|
|
398
|
+
{seoData.openGraph.title && (
|
|
313
399
|
<SeoField
|
|
314
|
-
label="
|
|
315
|
-
id={seoData.title.id}
|
|
316
|
-
value={seoData.title.content}
|
|
317
|
-
placeholder="Enter
|
|
400
|
+
label="OG Title"
|
|
401
|
+
id={seoData.openGraph.title.id}
|
|
402
|
+
value={seoData.openGraph.title.content}
|
|
403
|
+
placeholder="Enter Open Graph title..."
|
|
318
404
|
onChange={handleFieldChange}
|
|
319
405
|
/>
|
|
320
406
|
)}
|
|
321
|
-
{seoData.description && (
|
|
407
|
+
{seoData.openGraph.description && (
|
|
322
408
|
<SeoField
|
|
323
|
-
label="
|
|
324
|
-
id={seoData.description.id}
|
|
325
|
-
value={seoData.description.content}
|
|
326
|
-
placeholder="Enter
|
|
409
|
+
label="OG Description"
|
|
410
|
+
id={seoData.openGraph.description.id}
|
|
411
|
+
value={seoData.openGraph.description.content}
|
|
412
|
+
placeholder="Enter Open Graph description..."
|
|
327
413
|
multiline
|
|
328
414
|
onChange={handleFieldChange}
|
|
329
415
|
/>
|
|
330
416
|
)}
|
|
331
|
-
{seoData.
|
|
417
|
+
{seoData.openGraph.image && (
|
|
418
|
+
<ImageField
|
|
419
|
+
label="OG Image"
|
|
420
|
+
value={ogImage.current}
|
|
421
|
+
placeholder="/images/og-image.jpg"
|
|
422
|
+
onChange={(v) => {
|
|
423
|
+
if (ogImage.id) handleFieldChange(ogImage.id, v, ogImage.original)
|
|
424
|
+
}}
|
|
425
|
+
onBrowse={() => {
|
|
426
|
+
openMediaLibraryWithCallback((url: string) => {
|
|
427
|
+
if (ogImage.id) handleFieldChange(ogImage.id, url, ogImage.original)
|
|
428
|
+
})
|
|
429
|
+
}}
|
|
430
|
+
isDirty={ogImage.dirty}
|
|
431
|
+
/>
|
|
432
|
+
)}
|
|
433
|
+
{seoData.openGraph.url && (
|
|
332
434
|
<SeoField
|
|
333
|
-
label="
|
|
334
|
-
id={seoData.
|
|
335
|
-
value={seoData.
|
|
336
|
-
placeholder="
|
|
435
|
+
label="OG URL"
|
|
436
|
+
id={seoData.openGraph.url.id}
|
|
437
|
+
value={seoData.openGraph.url.content}
|
|
438
|
+
placeholder="https://example.com/page"
|
|
337
439
|
onChange={handleFieldChange}
|
|
338
440
|
/>
|
|
339
441
|
)}
|
|
340
|
-
{seoData.
|
|
442
|
+
{seoData.openGraph.type && (
|
|
443
|
+
<ComboBoxField
|
|
444
|
+
label="OG Type"
|
|
445
|
+
value={ogType.current}
|
|
446
|
+
placeholder="website"
|
|
447
|
+
options={OG_TYPE_OPTIONS}
|
|
448
|
+
onChange={(v) => {
|
|
449
|
+
if (ogType.id) handleFieldChange(ogType.id, v, ogType.original)
|
|
450
|
+
}}
|
|
451
|
+
isDirty={ogType.dirty}
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
454
|
+
{seoData.openGraph.siteName && (
|
|
341
455
|
<SeoField
|
|
342
|
-
label="
|
|
343
|
-
id={seoData.
|
|
344
|
-
value={seoData.
|
|
345
|
-
placeholder="
|
|
456
|
+
label="OG Site Name"
|
|
457
|
+
id={seoData.openGraph.siteName.id}
|
|
458
|
+
value={seoData.openGraph.siteName.content}
|
|
459
|
+
placeholder="My Website"
|
|
346
460
|
onChange={handleFieldChange}
|
|
347
461
|
/>
|
|
348
462
|
)}
|
|
349
|
-
|
|
463
|
+
</SeoSection>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Twitter Card */}
|
|
467
|
+
{seoData.twitterCard && Object.keys(seoData.twitterCard).length > 0 && (
|
|
468
|
+
<SeoSection title="Twitter Card">
|
|
469
|
+
{seoData.twitterCard.card && (
|
|
350
470
|
<ComboBoxField
|
|
351
|
-
label="
|
|
352
|
-
value={
|
|
353
|
-
placeholder="
|
|
354
|
-
options={
|
|
471
|
+
label="Card Type"
|
|
472
|
+
value={twitterCard.current}
|
|
473
|
+
placeholder="summary_large_image"
|
|
474
|
+
options={TWITTER_CARD_OPTIONS}
|
|
355
475
|
onChange={(v) => {
|
|
356
|
-
if (
|
|
476
|
+
if (twitterCard.id) handleFieldChange(twitterCard.id, v, twitterCard.original)
|
|
357
477
|
}}
|
|
358
|
-
isDirty={
|
|
478
|
+
isDirty={twitterCard.dirty}
|
|
359
479
|
/>
|
|
360
480
|
)}
|
|
361
|
-
{seoData.
|
|
362
|
-
<
|
|
363
|
-
label="
|
|
364
|
-
|
|
365
|
-
|
|
481
|
+
{seoData.twitterCard.title && (
|
|
482
|
+
<SeoField
|
|
483
|
+
label="Twitter Title"
|
|
484
|
+
id={seoData.twitterCard.title.id}
|
|
485
|
+
value={seoData.twitterCard.title.content}
|
|
486
|
+
placeholder="Enter Twitter title..."
|
|
487
|
+
onChange={handleFieldChange}
|
|
488
|
+
/>
|
|
489
|
+
)}
|
|
490
|
+
{seoData.twitterCard.description && (
|
|
491
|
+
<SeoField
|
|
492
|
+
label="Twitter Description"
|
|
493
|
+
id={seoData.twitterCard.description.id}
|
|
494
|
+
value={seoData.twitterCard.description.content}
|
|
495
|
+
placeholder="Enter Twitter description..."
|
|
496
|
+
multiline
|
|
497
|
+
onChange={handleFieldChange}
|
|
498
|
+
/>
|
|
499
|
+
)}
|
|
500
|
+
{seoData.twitterCard.image && (
|
|
501
|
+
<ImageField
|
|
502
|
+
label="Twitter Image"
|
|
503
|
+
value={twitterImage.current}
|
|
504
|
+
placeholder="/images/twitter-image.jpg"
|
|
366
505
|
onChange={(v) => {
|
|
367
|
-
if (
|
|
506
|
+
if (twitterImage.id) handleFieldChange(twitterImage.id, v, twitterImage.original)
|
|
507
|
+
}}
|
|
508
|
+
onBrowse={() => {
|
|
509
|
+
openMediaLibraryWithCallback((url: string) => {
|
|
510
|
+
if (twitterImage.id) handleFieldChange(twitterImage.id, url, twitterImage.original)
|
|
511
|
+
})
|
|
368
512
|
}}
|
|
369
|
-
isDirty={
|
|
513
|
+
isDirty={twitterImage.dirty}
|
|
514
|
+
/>
|
|
515
|
+
)}
|
|
516
|
+
{seoData.twitterCard.site && (
|
|
517
|
+
<SeoField
|
|
518
|
+
label="Twitter Site"
|
|
519
|
+
id={seoData.twitterCard.site.id}
|
|
520
|
+
value={seoData.twitterCard.site.content}
|
|
521
|
+
placeholder="@username"
|
|
522
|
+
onChange={handleFieldChange}
|
|
370
523
|
/>
|
|
371
524
|
)}
|
|
372
525
|
</SeoSection>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const label = favicon.sizes
|
|
385
|
-
? `Favicon (${favicon.sizes})`
|
|
386
|
-
: favicon.type
|
|
387
|
-
? `Favicon (${favicon.type.replace('image/', '')})`
|
|
388
|
-
: `Favicon${seoData.favicons!.length > 1 ? ` ${index + 1}` : ''}`
|
|
389
|
-
|
|
390
|
-
return (
|
|
391
|
-
<div key={faviconId || index} class="space-y-1.5">
|
|
392
|
-
<ImageField
|
|
393
|
-
label={label}
|
|
394
|
-
value={currentValue}
|
|
395
|
-
placeholder="/favicon.svg"
|
|
396
|
-
onChange={(v) => {
|
|
397
|
-
if (faviconId) handleFieldChange(faviconId, v, originalValue)
|
|
398
|
-
}}
|
|
399
|
-
onBrowse={() => {
|
|
400
|
-
openMediaLibraryWithCallback((url: string) => {
|
|
401
|
-
if (faviconId) handleFieldChange(faviconId, url, originalValue)
|
|
402
|
-
})
|
|
403
|
-
}}
|
|
404
|
-
isDirty={isDirty}
|
|
405
|
-
/>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{/* JSON-LD Preview */}
|
|
529
|
+
{seoData.jsonLd && seoData.jsonLd.length > 0 && (
|
|
530
|
+
<SeoSection title="Structured Data (JSON-LD)">
|
|
531
|
+
<div class="space-y-3">
|
|
532
|
+
{seoData.jsonLd.map((entry, index) => (
|
|
533
|
+
<div key={index} class="p-3 bg-white/5 rounded-cms-md border border-white/10">
|
|
534
|
+
<div class="flex items-center justify-between mb-2">
|
|
535
|
+
<span class="text-sm font-medium text-white/80">@type: {entry.type}</span>
|
|
406
536
|
</div>
|
|
407
|
-
|
|
408
|
-
})}
|
|
409
|
-
</SeoSection>
|
|
410
|
-
)}
|
|
411
|
-
|
|
412
|
-
{/* Open Graph */}
|
|
413
|
-
{seoData.openGraph && Object.keys(seoData.openGraph).length > 0 && (
|
|
414
|
-
<SeoSection title="Open Graph">
|
|
415
|
-
{seoData.openGraph.title && (
|
|
416
|
-
<SeoField
|
|
417
|
-
label="OG Title"
|
|
418
|
-
id={seoData.openGraph.title.id}
|
|
419
|
-
value={seoData.openGraph.title.content}
|
|
420
|
-
placeholder="Enter Open Graph title..."
|
|
421
|
-
onChange={handleFieldChange}
|
|
422
|
-
/>
|
|
423
|
-
)}
|
|
424
|
-
{seoData.openGraph.description && (
|
|
425
|
-
<SeoField
|
|
426
|
-
label="OG Description"
|
|
427
|
-
id={seoData.openGraph.description.id}
|
|
428
|
-
value={seoData.openGraph.description.content}
|
|
429
|
-
placeholder="Enter Open Graph description..."
|
|
430
|
-
multiline
|
|
431
|
-
onChange={handleFieldChange}
|
|
432
|
-
/>
|
|
433
|
-
)}
|
|
434
|
-
{seoData.openGraph.image && (
|
|
435
|
-
<ImageField
|
|
436
|
-
label="OG Image"
|
|
437
|
-
value={ogImage.current}
|
|
438
|
-
placeholder="/images/og-image.jpg"
|
|
439
|
-
onChange={(v) => {
|
|
440
|
-
if (ogImage.id) handleFieldChange(ogImage.id, v, ogImage.original)
|
|
441
|
-
}}
|
|
442
|
-
onBrowse={() => {
|
|
443
|
-
openMediaLibraryWithCallback((url: string) => {
|
|
444
|
-
if (ogImage.id) handleFieldChange(ogImage.id, url, ogImage.original)
|
|
445
|
-
})
|
|
446
|
-
}}
|
|
447
|
-
isDirty={ogImage.dirty}
|
|
448
|
-
/>
|
|
449
|
-
)}
|
|
450
|
-
{seoData.openGraph.url && (
|
|
451
|
-
<SeoField
|
|
452
|
-
label="OG URL"
|
|
453
|
-
id={seoData.openGraph.url.id}
|
|
454
|
-
value={seoData.openGraph.url.content}
|
|
455
|
-
placeholder="https://example.com/page"
|
|
456
|
-
onChange={handleFieldChange}
|
|
457
|
-
/>
|
|
458
|
-
)}
|
|
459
|
-
{seoData.openGraph.type && (
|
|
460
|
-
<ComboBoxField
|
|
461
|
-
label="OG Type"
|
|
462
|
-
value={ogType.current}
|
|
463
|
-
placeholder="website"
|
|
464
|
-
options={OG_TYPE_OPTIONS}
|
|
465
|
-
onChange={(v) => {
|
|
466
|
-
if (ogType.id) handleFieldChange(ogType.id, v, ogType.original)
|
|
467
|
-
}}
|
|
468
|
-
isDirty={ogType.dirty}
|
|
469
|
-
/>
|
|
470
|
-
)}
|
|
471
|
-
{seoData.openGraph.siteName && (
|
|
472
|
-
<SeoField
|
|
473
|
-
label="OG Site Name"
|
|
474
|
-
id={seoData.openGraph.siteName.id}
|
|
475
|
-
value={seoData.openGraph.siteName.content}
|
|
476
|
-
placeholder="My Website"
|
|
477
|
-
onChange={handleFieldChange}
|
|
478
|
-
/>
|
|
479
|
-
)}
|
|
480
|
-
</SeoSection>
|
|
481
|
-
)}
|
|
482
|
-
|
|
483
|
-
{/* Twitter Card */}
|
|
484
|
-
{seoData.twitterCard && Object.keys(seoData.twitterCard).length > 0 && (
|
|
485
|
-
<SeoSection title="Twitter Card">
|
|
486
|
-
{seoData.twitterCard.card && (
|
|
487
|
-
<ComboBoxField
|
|
488
|
-
label="Card Type"
|
|
489
|
-
value={twitterCard.current}
|
|
490
|
-
placeholder="summary_large_image"
|
|
491
|
-
options={TWITTER_CARD_OPTIONS}
|
|
492
|
-
onChange={(v) => {
|
|
493
|
-
if (twitterCard.id) handleFieldChange(twitterCard.id, v, twitterCard.original)
|
|
494
|
-
}}
|
|
495
|
-
isDirty={twitterCard.dirty}
|
|
496
|
-
/>
|
|
497
|
-
)}
|
|
498
|
-
{seoData.twitterCard.title && (
|
|
499
|
-
<SeoField
|
|
500
|
-
label="Twitter Title"
|
|
501
|
-
id={seoData.twitterCard.title.id}
|
|
502
|
-
value={seoData.twitterCard.title.content}
|
|
503
|
-
placeholder="Enter Twitter title..."
|
|
504
|
-
onChange={handleFieldChange}
|
|
505
|
-
/>
|
|
506
|
-
)}
|
|
507
|
-
{seoData.twitterCard.description && (
|
|
508
|
-
<SeoField
|
|
509
|
-
label="Twitter Description"
|
|
510
|
-
id={seoData.twitterCard.description.id}
|
|
511
|
-
value={seoData.twitterCard.description.content}
|
|
512
|
-
placeholder="Enter Twitter description..."
|
|
513
|
-
multiline
|
|
514
|
-
onChange={handleFieldChange}
|
|
515
|
-
/>
|
|
516
|
-
)}
|
|
517
|
-
{seoData.twitterCard.image && (
|
|
518
|
-
<ImageField
|
|
519
|
-
label="Twitter Image"
|
|
520
|
-
value={twitterImage.current}
|
|
521
|
-
placeholder="/images/twitter-image.jpg"
|
|
522
|
-
onChange={(v) => {
|
|
523
|
-
if (twitterImage.id) handleFieldChange(twitterImage.id, v, twitterImage.original)
|
|
524
|
-
}}
|
|
525
|
-
onBrowse={() => {
|
|
526
|
-
openMediaLibraryWithCallback((url: string) => {
|
|
527
|
-
if (twitterImage.id) handleFieldChange(twitterImage.id, url, twitterImage.original)
|
|
528
|
-
})
|
|
529
|
-
}}
|
|
530
|
-
isDirty={twitterImage.dirty}
|
|
531
|
-
/>
|
|
532
|
-
)}
|
|
533
|
-
{seoData.twitterCard.site && (
|
|
534
|
-
<SeoField
|
|
535
|
-
label="Twitter Site"
|
|
536
|
-
id={seoData.twitterCard.site.id}
|
|
537
|
-
value={seoData.twitterCard.site.content}
|
|
538
|
-
placeholder="@username"
|
|
539
|
-
onChange={handleFieldChange}
|
|
540
|
-
/>
|
|
541
|
-
)}
|
|
542
|
-
</SeoSection>
|
|
543
|
-
)}
|
|
544
|
-
|
|
545
|
-
{/* JSON-LD Preview */}
|
|
546
|
-
{seoData.jsonLd && seoData.jsonLd.length > 0 && (
|
|
547
|
-
<SeoSection title="Structured Data (JSON-LD)">
|
|
548
|
-
<div class="space-y-3">
|
|
549
|
-
{seoData.jsonLd.map((entry, index) => (
|
|
550
|
-
<div key={index} class="p-3 bg-white/5 rounded-cms-md border border-white/10">
|
|
551
|
-
<div class="flex items-center justify-between mb-2">
|
|
552
|
-
<span class="text-sm font-medium text-white/80">@type: {entry.type}</span>
|
|
553
|
-
</div>
|
|
554
|
-
<pre class="text-xs text-white/60 overflow-auto max-h-32 p-2 bg-black/30 rounded">
|
|
537
|
+
<pre class="text-xs text-white/60 overflow-auto max-h-32 p-2 bg-black/30 rounded">
|
|
555
538
|
{JSON.stringify(entry.data, null, 2)}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
</div>
|
|
565
|
-
|
|
566
|
-
{/* Footer */}
|
|
567
|
-
{hasSeoData && (
|
|
568
|
-
<div class="flex items-center justify-end gap-3 px-5 py-4 border-t border-white/10">
|
|
569
|
-
<button
|
|
570
|
-
type="button"
|
|
571
|
-
onClick={handleClose}
|
|
572
|
-
class="px-4 py-2 text-sm font-medium text-white/70 hover:text-white hover:bg-white/10 rounded-cms-pill transition-colors"
|
|
573
|
-
data-cms-ui
|
|
574
|
-
>
|
|
575
|
-
Cancel
|
|
576
|
-
</button>
|
|
577
|
-
<button
|
|
578
|
-
type="button"
|
|
579
|
-
onClick={handleSaveAll}
|
|
580
|
-
disabled={dirtyCount === 0 || isSaving}
|
|
581
|
-
class={`px-5 py-2 text-sm font-medium rounded-cms-pill transition-colors flex items-center gap-2 ${
|
|
582
|
-
dirtyCount > 0 && !isSaving
|
|
583
|
-
? 'bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover'
|
|
584
|
-
: 'bg-white/10 text-white/40 cursor-not-allowed'
|
|
585
|
-
}`}
|
|
586
|
-
data-cms-ui
|
|
587
|
-
>
|
|
588
|
-
{isSaving && <span class="inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />}
|
|
589
|
-
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
590
|
-
</button>
|
|
591
|
-
</div>
|
|
592
|
-
)}
|
|
539
|
+
</pre>
|
|
540
|
+
</div>
|
|
541
|
+
))}
|
|
542
|
+
</div>
|
|
543
|
+
</SeoSection>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
593
547
|
</div>
|
|
594
|
-
|
|
548
|
+
|
|
549
|
+
{/* Footer */}
|
|
550
|
+
{hasSeoData && (
|
|
551
|
+
<div class="flex items-center justify-end gap-3 px-5 py-4 border-t border-white/10">
|
|
552
|
+
<button
|
|
553
|
+
type="button"
|
|
554
|
+
onClick={handleClose}
|
|
555
|
+
class="px-4 py-2 text-sm font-medium text-white/70 hover:text-white hover:bg-white/10 rounded-cms-pill transition-colors"
|
|
556
|
+
data-cms-ui
|
|
557
|
+
>
|
|
558
|
+
Cancel
|
|
559
|
+
</button>
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
onClick={handleSaveAll}
|
|
563
|
+
disabled={dirtyCount === 0 || isSaving}
|
|
564
|
+
class={`px-5 py-2 text-sm font-medium rounded-cms-pill transition-colors flex items-center gap-2 ${
|
|
565
|
+
dirtyCount > 0 && !isSaving
|
|
566
|
+
? 'bg-cms-primary text-cms-primary-text hover:bg-cms-primary-hover'
|
|
567
|
+
: 'bg-white/10 text-white/40 cursor-not-allowed'
|
|
568
|
+
}`}
|
|
569
|
+
data-cms-ui
|
|
570
|
+
>
|
|
571
|
+
{isSaving && <span class="inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />}
|
|
572
|
+
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
)}
|
|
576
|
+
</ModalBackdrop>
|
|
595
577
|
)
|
|
596
578
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Z_INDEX } from '../../constants'
|
|
1
2
|
import { Toast } from './toast'
|
|
2
3
|
import type { ToastMessage } from './types'
|
|
3
4
|
|
|
@@ -8,7 +9,7 @@ export interface ToastContainerProps {
|
|
|
8
9
|
|
|
9
10
|
export const ToastContainer = ({ toasts, onRemove }: ToastContainerProps) => {
|
|
10
11
|
return (
|
|
11
|
-
<div class="fixed left-1/2 -translate-x-1/2 bottom-28
|
|
12
|
+
<div style={{ zIndex: Z_INDEX.TOAST }} class="fixed left-1/2 -translate-x-1/2 bottom-28 flex flex-col gap-2 items-center">
|
|
12
13
|
{toasts.map(toast => <Toast key={toast.id} {...toast} onRemove={onRemove} />)}
|
|
13
14
|
</div>
|
|
14
15
|
)
|