qarpeo 0.0.0-alpha.10
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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/chunks/auth-schema.mjs +219 -0
- package/dist/chunks/wrangler.mjs +783 -0
- package/dist/cli/index.mjs +1085 -0
- package/dist/dev/env.d.ts +12 -0
- package/dist/dev/wrangler.d.ts +18 -0
- package/dist/module.d.ts +16 -0
- package/dist/module.mjs +4436 -0
- package/dist/runtime/ai.d.ts +25 -0
- package/dist/runtime/ai.mjs +106 -0
- package/dist/runtime/analytics.d.ts +24 -0
- package/dist/runtime/analytics.mjs +38 -0
- package/dist/runtime/app/components/BlobImage.vue +100 -0
- package/dist/runtime/app/components/CMSBlocks.vue +93 -0
- package/dist/runtime/app/components/CMSContent.vue +23 -0
- package/dist/runtime/app/components/CMSImage.vue +49 -0
- package/dist/runtime/app/components/CMSLocaleSwitcher.vue +110 -0
- package/dist/runtime/app/components/CMSPreviewBanner.vue +79 -0
- package/dist/runtime/app/components/CMSVersionBanner.vue +49 -0
- package/dist/runtime/app/components/CMSVersionSwitcher.vue +126 -0
- package/dist/runtime/app/components/ContentRenderer.vue +80 -0
- package/dist/runtime/app/components/FileUpload.vue +196 -0
- package/dist/runtime/app/components/ImpersonationBanner.vue +57 -0
- package/dist/runtime/app/components/SignedIn.vue +13 -0
- package/dist/runtime/app/components/SignedOut.vue +13 -0
- package/dist/runtime/app/components/shiki.css +34 -0
- package/dist/runtime/app/composables/analytics.d.ts +125 -0
- package/dist/runtime/app/composables/analytics.mjs +81 -0
- package/dist/runtime/app/composables/api-tokens.d.ts +49 -0
- package/dist/runtime/app/composables/api-tokens.mjs +61 -0
- package/dist/runtime/app/composables/auth.d.ts +50 -0
- package/dist/runtime/app/composables/auth.mjs +101 -0
- package/dist/runtime/app/composables/chat.d.ts +69 -0
- package/dist/runtime/app/composables/chat.mjs +159 -0
- package/dist/runtime/app/composables/cms-preview.d.ts +33 -0
- package/dist/runtime/app/composables/cms-preview.mjs +83 -0
- package/dist/runtime/app/composables/cms.d.ts +307 -0
- package/dist/runtime/app/composables/cms.mjs +355 -0
- package/dist/runtime/app/composables/docs-version.d.ts +35 -0
- package/dist/runtime/app/composables/docs-version.mjs +70 -0
- package/dist/runtime/app/composables/impersonation.d.ts +14 -0
- package/dist/runtime/app/composables/impersonation.mjs +44 -0
- package/dist/runtime/app/composables/notifications.d.ts +35 -0
- package/dist/runtime/app/composables/notifications.mjs +139 -0
- package/dist/runtime/app/composables/organization.d.ts +127 -0
- package/dist/runtime/app/composables/organization.mjs +138 -0
- package/dist/runtime/app/composables/passkey.d.ts +6 -0
- package/dist/runtime/app/composables/passkey.mjs +105 -0
- package/dist/runtime/app/composables/passwordless.d.ts +7 -0
- package/dist/runtime/app/composables/passwordless.mjs +67 -0
- package/dist/runtime/app/composables/payments.d.ts +43 -0
- package/dist/runtime/app/composables/payments.mjs +74 -0
- package/dist/runtime/app/composables/realtime.d.ts +96 -0
- package/dist/runtime/app/composables/realtime.mjs +432 -0
- package/dist/runtime/app/composables/site.d.ts +35 -0
- package/dist/runtime/app/composables/site.mjs +36 -0
- package/dist/runtime/app/composables/storage.d.ts +60 -0
- package/dist/runtime/app/composables/storage.mjs +294 -0
- package/dist/runtime/app/composables/tenant.d.ts +82 -0
- package/dist/runtime/app/composables/tenant.mjs +120 -0
- package/dist/runtime/app/composables/turnstile.d.ts +3 -0
- package/dist/runtime/app/composables/turnstile.mjs +10 -0
- package/dist/runtime/app/composables/two-factor.d.ts +12 -0
- package/dist/runtime/app/composables/two-factor.mjs +65 -0
- package/dist/runtime/app/middleware/auth.global.d.ts +2 -0
- package/dist/runtime/app/middleware/auth.global.mjs +44 -0
- package/dist/runtime/app/pages/cms.vue +2249 -0
- package/dist/runtime/app/pages/devtools/agents.vue +717 -0
- package/dist/runtime/app/pages/devtools/analytics.vue +618 -0
- package/dist/runtime/app/pages/devtools/auth.vue +351 -0
- package/dist/runtime/app/pages/devtools/blob.vue +745 -0
- package/dist/runtime/app/pages/devtools/db.vue +546 -0
- package/dist/runtime/app/pages/devtools/domains.vue +772 -0
- package/dist/runtime/app/pages/devtools/kv.vue +829 -0
- package/dist/runtime/app/pages/devtools/logs.vue +655 -0
- package/dist/runtime/app/pages/devtools/mail.vue +389 -0
- package/dist/runtime/app/pages/devtools/overview.vue +390 -0
- package/dist/runtime/app/pages/devtools/settings.vue +560 -0
- package/dist/runtime/app/pages/devtools.vue +165 -0
- package/dist/runtime/app/plugins/seo.d.ts +2 -0
- package/dist/runtime/app/plugins/seo.mjs +105 -0
- package/dist/runtime/app/plugins/shortcuts.client.d.ts +6 -0
- package/dist/runtime/app/plugins/shortcuts.client.mjs +19 -0
- package/dist/runtime/app/public/sw-notifications.mjs +52 -0
- package/dist/runtime/auth.d.ts +25 -0
- package/dist/runtime/auth.mjs +11 -0
- package/dist/runtime/bindings.d.ts +27 -0
- package/dist/runtime/bindings.mjs +38 -0
- package/dist/runtime/blob.d.ts +26 -0
- package/dist/runtime/blob.mjs +334 -0
- package/dist/runtime/bots.d.ts +1 -0
- package/dist/runtime/bots.mjs +1 -0
- package/dist/runtime/browser.d.ts +21 -0
- package/dist/runtime/browser.mjs +64 -0
- package/dist/runtime/cache.d.ts +19 -0
- package/dist/runtime/cache.mjs +12 -0
- package/dist/runtime/cms.d.ts +2 -0
- package/dist/runtime/cms.mjs +0 -0
- package/dist/runtime/db.d.ts +21 -0
- package/dist/runtime/db.mjs +36 -0
- package/dist/runtime/drizzle.d.ts +11 -0
- package/dist/runtime/drizzle.mjs +18 -0
- package/dist/runtime/email.d.ts +51 -0
- package/dist/runtime/email.mjs +245 -0
- package/dist/runtime/hyperdrive.d.ts +21 -0
- package/dist/runtime/hyperdrive.mjs +49 -0
- package/dist/runtime/index.d.ts +23 -0
- package/dist/runtime/index.mjs +23 -0
- package/dist/runtime/kv.d.ts +16 -0
- package/dist/runtime/kv.mjs +126 -0
- package/dist/runtime/payments.d.ts +2 -0
- package/dist/runtime/payments.mjs +0 -0
- package/dist/runtime/queue.d.ts +21 -0
- package/dist/runtime/queue.mjs +41 -0
- package/dist/runtime/rag.d.ts +1 -0
- package/dist/runtime/rag.mjs +1 -0
- package/dist/runtime/ratelimit.d.ts +21 -0
- package/dist/runtime/ratelimit.mjs +38 -0
- package/dist/runtime/realtime.d.ts +6 -0
- package/dist/runtime/realtime.mjs +13 -0
- package/dist/runtime/server/db/cms-schema.d.ts +651 -0
- package/dist/runtime/server/db/cms-schema.mjs +74 -0
- package/dist/runtime/server/durable-objects/agent.d.ts +14 -0
- package/dist/runtime/server/durable-objects/agent.mjs +571 -0
- package/dist/runtime/server/durable-objects/room.d.ts +34 -0
- package/dist/runtime/server/durable-objects/room.mjs +514 -0
- package/dist/runtime/server/middleware/auth.d.ts +2 -0
- package/dist/runtime/server/middleware/auth.mjs +108 -0
- package/dist/runtime/server/middleware/devtools.d.ts +2 -0
- package/dist/runtime/server/middleware/devtools.mjs +61 -0
- package/dist/runtime/server/middleware/ratelimit.d.ts +2 -0
- package/dist/runtime/server/middleware/ratelimit.mjs +49 -0
- package/dist/runtime/server/middleware/tenancy.d.ts +6 -0
- package/dist/runtime/server/middleware/tenancy.mjs +15 -0
- package/dist/runtime/server/middleware/tracker.d.ts +7 -0
- package/dist/runtime/server/middleware/tracker.mjs +9 -0
- package/dist/runtime/server/plugins/agents.d.ts +7 -0
- package/dist/runtime/server/plugins/agents.mjs +3 -0
- package/dist/runtime/server/plugins/cms.d.ts +7 -0
- package/dist/runtime/server/plugins/cms.mjs +16 -0
- package/dist/runtime/server/plugins/dev-logs.d.ts +1 -0
- package/dist/runtime/server/plugins/dev-logs.mjs +58 -0
- package/dist/runtime/server/plugins/dev-proxy.d.ts +3 -0
- package/dist/runtime/server/plugins/dev-proxy.mjs +21 -0
- package/dist/runtime/server/plugins/env.d.ts +6 -0
- package/dist/runtime/server/plugins/env.mjs +17 -0
- package/dist/runtime/server/plugins/queue.d.ts +6 -0
- package/dist/runtime/server/plugins/queue.mjs +8 -0
- package/dist/runtime/server/routes/__qarpeo/agents/chat.post.d.ts +20 -0
- package/dist/runtime/server/routes/__qarpeo/agents/chat.post.mjs +110 -0
- package/dist/runtime/server/routes/__qarpeo/agents/clear.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/agents/clear.post.mjs +15 -0
- package/dist/runtime/server/routes/__qarpeo/agents/create.post.d.ts +17 -0
- package/dist/runtime/server/routes/__qarpeo/agents/create.post.mjs +47 -0
- package/dist/runtime/server/routes/__qarpeo/agents/history.get.d.ts +15 -0
- package/dist/runtime/server/routes/__qarpeo/agents/history.get.mjs +24 -0
- package/dist/runtime/server/routes/__qarpeo/agents/list.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/agents/list.get.mjs +63 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/events.get.d.ts +64 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/events.get.mjs +55 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.d.ts +30 -0
- package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.mjs +73 -0
- package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.mjs +46 -0
- package/dist/runtime/server/routes/__qarpeo/auth/users.get.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/auth/users.get.mjs +65 -0
- package/dist/runtime/server/routes/__qarpeo/blob/files.get.d.ts +22 -0
- package/dist/runtime/server/routes/__qarpeo/blob/files.get.mjs +36 -0
- package/dist/runtime/server/routes/__qarpeo/db/query.post.d.ts +23 -0
- package/dist/runtime/server/routes/__qarpeo/db/query.post.mjs +34 -0
- package/dist/runtime/server/routes/__qarpeo/db/tables.get.d.ts +31 -0
- package/dist/runtime/server/routes/__qarpeo/db/tables.get.mjs +81 -0
- package/dist/runtime/server/routes/__qarpeo/kv/delete.post.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/kv/delete.post.mjs +18 -0
- package/dist/runtime/server/routes/__qarpeo/kv/get.get.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/kv/get.get.mjs +20 -0
- package/dist/runtime/server/routes/__qarpeo/kv/keys.get.d.ts +8 -0
- package/dist/runtime/server/routes/__qarpeo/kv/keys.get.mjs +27 -0
- package/dist/runtime/server/routes/__qarpeo/kv/set.post.d.ts +14 -0
- package/dist/runtime/server/routes/__qarpeo/kv/set.post.mjs +24 -0
- package/dist/runtime/server/routes/__qarpeo/logs.delete.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/logs.delete.mjs +6 -0
- package/dist/runtime/server/routes/__qarpeo/logs.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/logs.get.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/logs.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/logs.post.mjs +17 -0
- package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.mjs +37 -0
- package/dist/runtime/server/routes/__qarpeo/mail/clear.post.d.ts +4 -0
- package/dist/runtime/server/routes/__qarpeo/mail/clear.post.mjs +6 -0
- package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.d.ts +5 -0
- package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.mjs +7 -0
- package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.d.ts +4 -0
- package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.mjs +29 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.mjs +23 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.mjs +31 -0
- package/dist/runtime/server/routes/__qarpeo/overview/health.get.d.ts +8 -0
- package/dist/runtime/server/routes/__qarpeo/overview/health.get.mjs +46 -0
- package/dist/runtime/server/routes/__qarpeo/overview.get.d.ts +32 -0
- package/dist/runtime/server/routes/__qarpeo/overview.get.mjs +38 -0
- package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.d.ts +11 -0
- package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.mjs +84 -0
- package/dist/runtime/server/routes/__qarpeo/settings.get.d.ts +124 -0
- package/dist/runtime/server/routes/__qarpeo/settings.get.mjs +145 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/create.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/create.post.mjs +26 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.d.ts +6 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.mjs +29 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.d.ts +7 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.mjs +21 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.d.ts +9 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.mjs +21 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/list.get.d.ts +15 -0
- package/dist/runtime/server/routes/__qarpeo/tenants/list.get.mjs +22 -0
- package/dist/runtime/server/routes/agents/[agent]/chat.post.d.ts +2 -0
- package/dist/runtime/server/routes/agents/[agent]/chat.post.mjs +35 -0
- package/dist/runtime/server/routes/agents/[agent]/clear.post.d.ts +4 -0
- package/dist/runtime/server/routes/agents/[agent]/clear.post.mjs +27 -0
- package/dist/runtime/server/routes/agents/[agent]/history.get.d.ts +4 -0
- package/dist/runtime/server/routes/agents/[agent]/history.get.mjs +27 -0
- package/dist/runtime/server/routes/analytics/events.get.d.ts +3 -0
- package/dist/runtime/server/routes/analytics/events.get.mjs +17 -0
- package/dist/runtime/server/routes/analytics/pageviews.get.d.ts +3 -0
- package/dist/runtime/server/routes/analytics/pageviews.get.mjs +15 -0
- package/dist/runtime/server/routes/analytics/summary.get.d.ts +6 -0
- package/dist/runtime/server/routes/analytics/summary.get.mjs +15 -0
- package/dist/runtime/server/routes/atom.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/atom.xml.get.mjs +76 -0
- package/dist/runtime/server/routes/auth/2fa/disable.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/2fa/disable.post.mjs +44 -0
- package/dist/runtime/server/routes/auth/2fa/setup.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/2fa/setup.post.mjs +32 -0
- package/dist/runtime/server/routes/auth/2fa/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/2fa/verify.post.mjs +92 -0
- package/dist/runtime/server/routes/auth/[provider]/callback.d.ts +1 -0
- package/dist/runtime/server/routes/auth/[provider]/callback.mjs +1 -0
- package/dist/runtime/server/routes/auth/[provider].d.ts +3 -0
- package/dist/runtime/server/routes/auth/[provider].mjs +95 -0
- package/dist/runtime/server/routes/auth/account.delete.d.ts +5 -0
- package/dist/runtime/server/routes/auth/account.delete.mjs +52 -0
- package/dist/runtime/server/routes/auth/impersonate/start.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/impersonate/start.post.mjs +14 -0
- package/dist/runtime/server/routes/auth/impersonate/stop.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/impersonate/stop.post.mjs +5 -0
- package/dist/runtime/server/routes/auth/logout-all.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout-all.post.mjs +16 -0
- package/dist/runtime/server/routes/auth/logout-others.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout-others.post.mjs +11 -0
- package/dist/runtime/server/routes/auth/logout.post.d.ts +4 -0
- package/dist/runtime/server/routes/auth/logout.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/magic-link/send.post.d.ts +10 -0
- package/dist/runtime/server/routes/auth/magic-link/send.post.mjs +29 -0
- package/dist/runtime/server/routes/auth/magic-link/verify.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/magic-link/verify.get.mjs +81 -0
- package/dist/runtime/server/routes/auth/organization/create.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/create.post.mjs +10 -0
- package/dist/runtime/server/routes/auth/organization/invite.post.d.ts +9 -0
- package/dist/runtime/server/routes/auth/organization/invite.post.mjs +27 -0
- package/dist/runtime/server/routes/auth/organization/join.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/join.post.mjs +15 -0
- package/dist/runtime/server/routes/auth/organization/list.get.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/list.get.mjs +7 -0
- package/dist/runtime/server/routes/auth/organization/members/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/organization/members/[id].delete.mjs +33 -0
- package/dist/runtime/server/routes/auth/organization/members/index.get.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/members/index.get.mjs +18 -0
- package/dist/runtime/server/routes/auth/organization/mfa.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/mfa.post.mjs +33 -0
- package/dist/runtime/server/routes/auth/organization/sso/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/organization/sso/[id].delete.mjs +28 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.get.mjs +23 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/organization/sso/index.post.mjs +84 -0
- package/dist/runtime/server/routes/auth/organization/switch.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/organization/switch.post.mjs +14 -0
- package/dist/runtime/server/routes/auth/otp/send.post.d.ts +10 -0
- package/dist/runtime/server/routes/auth/otp/send.post.mjs +29 -0
- package/dist/runtime/server/routes/auth/otp/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/otp/verify.post.mjs +95 -0
- package/dist/runtime/server/routes/auth/sessions/[id].delete.d.ts +5 -0
- package/dist/runtime/server/routes/auth/sessions/[id].delete.mjs +24 -0
- package/dist/runtime/server/routes/auth/sessions.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sessions.get.mjs +11 -0
- package/dist/runtime/server/routes/auth/sso/login.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/login.get.mjs +94 -0
- package/dist/runtime/server/routes/auth/sso/oidc/callback.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/oidc/callback.get.mjs +113 -0
- package/dist/runtime/server/routes/auth/sso/saml/acs.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/sso/saml/acs.post.mjs +95 -0
- package/dist/runtime/server/routes/auth/tokens/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/auth/tokens/[id].delete.mjs +13 -0
- package/dist/runtime/server/routes/auth/tokens/create.post.d.ts +9 -0
- package/dist/runtime/server/routes/auth/tokens/create.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/tokens/index.get.d.ts +9 -0
- package/dist/runtime/server/routes/auth/tokens/index.get.mjs +5 -0
- package/dist/runtime/server/routes/auth/turnstile/verify.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/turnstile/verify.post.mjs +15 -0
- package/dist/runtime/server/routes/auth/user.get.d.ts +2 -0
- package/dist/runtime/server/routes/auth/user.get.mjs +6 -0
- package/dist/runtime/server/routes/auth/webauthn/login/options.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/webauthn/login/options.post.mjs +17 -0
- package/dist/runtime/server/routes/auth/webauthn/login/verify.post.d.ts +6 -0
- package/dist/runtime/server/routes/auth/webauthn/login/verify.post.mjs +127 -0
- package/dist/runtime/server/routes/auth/webauthn/register/options.post.d.ts +2 -0
- package/dist/runtime/server/routes/auth/webauthn/register/options.post.mjs +47 -0
- package/dist/runtime/server/routes/auth/webauthn/register/verify.post.d.ts +5 -0
- package/dist/runtime/server/routes/auth/webauthn/register/verify.post.mjs +88 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.d.ts +8 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.mjs +27 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.d.ts +6 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.mjs +19 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.d.ts +6 -0
- package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.mjs +33 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].delete.mjs +17 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].put.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[id].put.mjs +18 -0
- package/dist/runtime/server/routes/cms/[collection]/[slug].get.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/[slug].get.mjs +19 -0
- package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.mjs +69 -0
- package/dist/runtime/server/routes/cms/[collection]/by-path.get.d.ts +8 -0
- package/dist/runtime/server/routes/cms/[collection]/by-path.get.mjs +23 -0
- package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.mjs +65 -0
- package/dist/runtime/server/routes/cms/[collection]/index.get.d.ts +7 -0
- package/dist/runtime/server/routes/cms/[collection]/index.get.mjs +15 -0
- package/dist/runtime/server/routes/cms/[collection]/index.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/[collection]/index.post.mjs +23 -0
- package/dist/runtime/server/routes/cms/[collection]/references.get.d.ts +19 -0
- package/dist/runtime/server/routes/cms/[collection]/references.get.mjs +47 -0
- package/dist/runtime/server/routes/cms/[collection]/surroundings.get.d.ts +10 -0
- package/dist/runtime/server/routes/cms/[collection]/surroundings.get.mjs +12 -0
- package/dist/runtime/server/routes/cms/collections/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/[id].delete.mjs +10 -0
- package/dist/runtime/server/routes/cms/collections/[id].put.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/[id].put.mjs +11 -0
- package/dist/runtime/server/routes/cms/collections/index.get.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/index.get.mjs +6 -0
- package/dist/runtime/server/routes/cms/collections/index.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/collections/index.post.mjs +19 -0
- package/dist/runtime/server/routes/cms/components/index.get.d.ts +17 -0
- package/dist/runtime/server/routes/cms/components/index.get.mjs +16 -0
- package/dist/runtime/server/routes/cms/deploy.post.d.ts +4 -0
- package/dist/runtime/server/routes/cms/deploy.post.mjs +46 -0
- package/dist/runtime/server/routes/cms/export.post.d.ts +10 -0
- package/dist/runtime/server/routes/cms/export.post.mjs +55 -0
- package/dist/runtime/server/routes/cms/navigation.get.d.ts +8 -0
- package/dist/runtime/server/routes/cms/navigation.get.mjs +36 -0
- package/dist/runtime/server/routes/cms/preview/disable.get.d.ts +9 -0
- package/dist/runtime/server/routes/cms/preview/disable.get.mjs +9 -0
- package/dist/runtime/server/routes/cms/preview/enable.get.d.ts +14 -0
- package/dist/runtime/server/routes/cms/preview/enable.get.mjs +42 -0
- package/dist/runtime/server/routes/cms/publish-scheduled.post.d.ts +11 -0
- package/dist/runtime/server/routes/cms/publish-scheduled.post.mjs +35 -0
- package/dist/runtime/server/routes/cms/search.get.d.ts +13 -0
- package/dist/runtime/server/routes/cms/search.get.mjs +137 -0
- package/dist/runtime/server/routes/cms/sync.post.d.ts +7 -0
- package/dist/runtime/server/routes/cms/sync.post.mjs +18 -0
- package/dist/runtime/server/routes/cms/webhooks/[provider].post.d.ts +150 -0
- package/dist/runtime/server/routes/cms/webhooks/[provider].post.mjs +213 -0
- package/dist/runtime/server/routes/docs/search.get.d.ts +15 -0
- package/dist/runtime/server/routes/docs/search.get.mjs +20 -0
- package/dist/runtime/server/routes/feed.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/feed.xml.get.mjs +73 -0
- package/dist/runtime/server/routes/mail/bounce.post.d.ts +14 -0
- package/dist/runtime/server/routes/mail/bounce.post.mjs +56 -0
- package/dist/runtime/server/routes/mail/inbox/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/mail/inbox/[id].get.mjs +9 -0
- package/dist/runtime/server/routes/mail/inbox.get.d.ts +4 -0
- package/dist/runtime/server/routes/mail/inbox.get.mjs +7 -0
- package/dist/runtime/server/routes/mail/suppressions/[email].delete.d.ts +11 -0
- package/dist/runtime/server/routes/mail/suppressions/[email].delete.mjs +18 -0
- package/dist/runtime/server/routes/mail/suppressions/index.get.d.ts +12 -0
- package/dist/runtime/server/routes/mail/suppressions/index.get.mjs +23 -0
- package/dist/runtime/server/routes/mail/suppressions/index.post.d.ts +10 -0
- package/dist/runtime/server/routes/mail/suppressions/index.post.mjs +23 -0
- package/dist/runtime/server/routes/mail/unsubscribe.d.ts +13 -0
- package/dist/runtime/server/routes/mail/unsubscribe.mjs +91 -0
- package/dist/runtime/server/routes/mail/webhooks.post.d.ts +12 -0
- package/dist/runtime/server/routes/mail/webhooks.post.mjs +71 -0
- package/dist/runtime/server/routes/notifications/[id]/read.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/[id]/read.post.mjs +13 -0
- package/dist/runtime/server/routes/notifications/[id].delete.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/[id].delete.mjs +13 -0
- package/dist/runtime/server/routes/notifications/[id].get.d.ts +2 -0
- package/dist/runtime/server/routes/notifications/[id].get.mjs +15 -0
- package/dist/runtime/server/routes/notifications/index.get.d.ts +6 -0
- package/dist/runtime/server/routes/notifications/index.get.mjs +18 -0
- package/dist/runtime/server/routes/notifications/push/subscribe.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/push/subscribe.post.mjs +20 -0
- package/dist/runtime/server/routes/notifications/push/unsubscribe.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/push/unsubscribe.post.mjs +15 -0
- package/dist/runtime/server/routes/notifications/read-all.post.d.ts +4 -0
- package/dist/runtime/server/routes/notifications/read-all.post.mjs +11 -0
- package/dist/runtime/server/routes/payments/cancel.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/cancel.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/change-plan.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/change-plan.post.mjs +12 -0
- package/dist/runtime/server/routes/payments/checkout.post.d.ts +9 -0
- package/dist/runtime/server/routes/payments/checkout.post.mjs +16 -0
- package/dist/runtime/server/routes/payments/pause.post.d.ts +7 -0
- package/dist/runtime/server/routes/payments/pause.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/portal.post.d.ts +5 -0
- package/dist/runtime/server/routes/payments/portal.post.mjs +9 -0
- package/dist/runtime/server/routes/payments/subscription.get.d.ts +6 -0
- package/dist/runtime/server/routes/payments/subscription.get.mjs +9 -0
- package/dist/runtime/server/routes/payments/usage.get.d.ts +12 -0
- package/dist/runtime/server/routes/payments/usage.get.mjs +18 -0
- package/dist/runtime/server/routes/payments/usage.post.d.ts +6 -0
- package/dist/runtime/server/routes/payments/usage.post.mjs +24 -0
- package/dist/runtime/server/routes/payments/webhook.post.d.ts +14 -0
- package/dist/runtime/server/routes/payments/webhook.post.mjs +21 -0
- package/dist/runtime/server/routes/realtime/broadcast.post.d.ts +4 -0
- package/dist/runtime/server/routes/realtime/broadcast.post.mjs +26 -0
- package/dist/runtime/server/routes/realtime/history.delete.d.ts +13 -0
- package/dist/runtime/server/routes/realtime/history.delete.mjs +12 -0
- package/dist/runtime/server/routes/realtime/history.get.d.ts +16 -0
- package/dist/runtime/server/routes/realtime/history.get.mjs +15 -0
- package/dist/runtime/server/routes/realtime/presence.get.d.ts +6 -0
- package/dist/runtime/server/routes/realtime/presence.get.mjs +27 -0
- package/dist/runtime/server/routes/realtime/state.get.d.ts +5 -0
- package/dist/runtime/server/routes/realtime/state.get.mjs +26 -0
- package/dist/runtime/server/routes/realtime/state.post.d.ts +6 -0
- package/dist/runtime/server/routes/realtime/state.post.mjs +37 -0
- package/dist/runtime/server/routes/realtime/ws.get.d.ts +2 -0
- package/dist/runtime/server/routes/realtime/ws.get.mjs +73 -0
- package/dist/runtime/server/routes/robots.txt.get.d.ts +2 -0
- package/dist/runtime/server/routes/robots.txt.get.mjs +41 -0
- package/dist/runtime/server/routes/rss.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/rss.xml.get.mjs +2 -0
- package/dist/runtime/server/routes/sitemap.xml.get.d.ts +2 -0
- package/dist/runtime/server/routes/sitemap.xml.get.mjs +84 -0
- package/dist/runtime/server/routes/storage/file/[...key].delete.d.ts +4 -0
- package/dist/runtime/server/routes/storage/file/[...key].delete.mjs +23 -0
- package/dist/runtime/server/routes/storage/file/[...key].get.d.ts +2 -0
- package/dist/runtime/server/routes/storage/file/[...key].get.mjs +23 -0
- package/dist/runtime/server/routes/storage/files.get.d.ts +14 -0
- package/dist/runtime/server/routes/storage/files.get.mjs +11 -0
- package/dist/runtime/server/routes/storage/presigned.post.d.ts +8 -0
- package/dist/runtime/server/routes/storage/presigned.post.mjs +39 -0
- package/dist/runtime/server/routes/storage/sign.post.d.ts +6 -0
- package/dist/runtime/server/routes/storage/sign.post.mjs +24 -0
- package/dist/runtime/server/routes/storage/upload.post.d.ts +22 -0
- package/dist/runtime/server/routes/storage/upload.post.mjs +96 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.mjs +39 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.mjs +39 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.mjs +20 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.mjs +44 -0
- package/dist/runtime/server/routes/tenants/current.get.d.ts +8 -0
- package/dist/runtime/server/routes/tenants/current.get.mjs +6 -0
- package/dist/runtime/server/tasks/cms/publish.d.ts +4 -0
- package/dist/runtime/server/tasks/cms/publish.mjs +18 -0
- package/dist/runtime/server/utils/agent-store.d.ts +24 -0
- package/dist/runtime/server/utils/agent-store.mjs +53 -0
- package/dist/runtime/server/utils/agents-registry-fallback.d.ts +1 -0
- package/dist/runtime/server/utils/agents-registry-fallback.mjs +1 -0
- package/dist/runtime/server/utils/ai.d.ts +119 -0
- package/dist/runtime/server/utils/ai.mjs +607 -0
- package/dist/runtime/server/utils/analytics-query.d.ts +109 -0
- package/dist/runtime/server/utils/analytics-query.mjs +234 -0
- package/dist/runtime/server/utils/auth.d.ts +60 -0
- package/dist/runtime/server/utils/auth.mjs +361 -0
- package/dist/runtime/server/utils/blob-store.d.ts +13 -0
- package/dist/runtime/server/utils/blob-store.mjs +60 -0
- package/dist/runtime/server/utils/bots.d.ts +21 -0
- package/dist/runtime/server/utils/bots.mjs +491 -0
- package/dist/runtime/server/utils/cache.d.ts +33 -0
- package/dist/runtime/server/utils/cache.mjs +128 -0
- package/dist/runtime/server/utils/cms-events.d.ts +17 -0
- package/dist/runtime/server/utils/cms-events.mjs +80 -0
- package/dist/runtime/server/utils/cms-filter.d.ts +3 -0
- package/dist/runtime/server/utils/cms-filter.mjs +34 -0
- package/dist/runtime/server/utils/cms.d.ts +278 -0
- package/dist/runtime/server/utils/cms.mjs +3268 -0
- package/dist/runtime/server/utils/config.d.ts +27 -0
- package/dist/runtime/server/utils/config.mjs +38 -0
- package/dist/runtime/server/utils/context.d.ts +13 -0
- package/dist/runtime/server/utils/context.mjs +98 -0
- package/dist/runtime/server/utils/crypto.d.ts +16 -0
- package/dist/runtime/server/utils/crypto.mjs +56 -0
- package/dist/runtime/server/utils/dev-logs.d.ts +21 -0
- package/dist/runtime/server/utils/dev-logs.mjs +41 -0
- package/dist/runtime/server/utils/dev-proxy.d.ts +1 -0
- package/dist/runtime/server/utils/dev-proxy.mjs +69 -0
- package/dist/runtime/server/utils/domains.d.ts +46 -0
- package/dist/runtime/server/utils/domains.mjs +318 -0
- package/dist/runtime/server/utils/drizzle.d.ts +10 -0
- package/dist/runtime/server/utils/drizzle.mjs +36 -0
- package/dist/runtime/server/utils/email-templates.d.ts +44 -0
- package/dist/runtime/server/utils/email-templates.mjs +373 -0
- package/dist/runtime/server/utils/feed.d.ts +42 -0
- package/dist/runtime/server/utils/feed.mjs +161 -0
- package/dist/runtime/server/utils/impersonation.d.ts +67 -0
- package/dist/runtime/server/utils/impersonation.mjs +298 -0
- package/dist/runtime/server/utils/jobs.d.ts +70 -0
- package/dist/runtime/server/utils/jobs.mjs +125 -0
- package/dist/runtime/server/utils/kv-store.d.ts +9 -0
- package/dist/runtime/server/utils/kv-store.mjs +32 -0
- package/dist/runtime/server/utils/mail-suppression.d.ts +62 -0
- package/dist/runtime/server/utils/mail-suppression.mjs +328 -0
- package/dist/runtime/server/utils/mail.d.ts +90 -0
- package/dist/runtime/server/utils/mail.mjs +366 -0
- package/dist/runtime/server/utils/notifications.d.ts +51 -0
- package/dist/runtime/server/utils/notifications.mjs +215 -0
- package/dist/runtime/server/utils/organization.d.ts +54 -0
- package/dist/runtime/server/utils/organization.mjs +354 -0
- package/dist/runtime/server/utils/passwordless.d.ts +48 -0
- package/dist/runtime/server/utils/passwordless.mjs +179 -0
- package/dist/runtime/server/utils/payments.d.ts +121 -0
- package/dist/runtime/server/utils/payments.mjs +1067 -0
- package/dist/runtime/server/utils/queue-fallback.d.ts +2 -0
- package/dist/runtime/server/utils/queue-fallback.mjs +2 -0
- package/dist/runtime/server/utils/queue.d.ts +4 -0
- package/dist/runtime/server/utils/queue.mjs +3 -0
- package/dist/runtime/server/utils/rag.d.ts +25 -0
- package/dist/runtime/server/utils/rag.mjs +333 -0
- package/dist/runtime/server/utils/ratelimit.d.ts +45 -0
- package/dist/runtime/server/utils/ratelimit.mjs +182 -0
- package/dist/runtime/server/utils/realtime.d.ts +110 -0
- package/dist/runtime/server/utils/realtime.mjs +197 -0
- package/dist/runtime/server/utils/schema-fallback.d.ts +3 -0
- package/dist/runtime/server/utils/schema-fallback.mjs +2 -0
- package/dist/runtime/server/utils/session.d.ts +72 -0
- package/dist/runtime/server/utils/session.mjs +287 -0
- package/dist/runtime/server/utils/sso.d.ts +49 -0
- package/dist/runtime/server/utils/sso.mjs +131 -0
- package/dist/runtime/server/utils/storage.d.ts +111 -0
- package/dist/runtime/server/utils/storage.mjs +654 -0
- package/dist/runtime/server/utils/tenant.d.ts +44 -0
- package/dist/runtime/server/utils/tenant.mjs +285 -0
- package/dist/runtime/server/utils/totp.d.ts +33 -0
- package/dist/runtime/server/utils/totp.mjs +112 -0
- package/dist/runtime/server/utils/tracker.d.ts +72 -0
- package/dist/runtime/server/utils/tracker.mjs +139 -0
- package/dist/runtime/server/utils/turnstile.d.ts +17 -0
- package/dist/runtime/server/utils/turnstile.mjs +68 -0
- package/dist/runtime/server/utils/vapid.d.ts +35 -0
- package/dist/runtime/server/utils/vapid.mjs +211 -0
- package/dist/runtime/storage.d.ts +1 -0
- package/dist/runtime/storage.mjs +0 -0
- package/dist/runtime/types/ai.d.ts +68 -0
- package/dist/runtime/types/ai.mjs +0 -0
- package/dist/runtime/types/analytics.d.ts +30 -0
- package/dist/runtime/types/analytics.mjs +0 -0
- package/dist/runtime/types/auth-globals.d.ts +14 -0
- package/dist/runtime/types/auth.d.ts +153 -0
- package/dist/runtime/types/auth.mjs +0 -0
- package/dist/runtime/types/browser.d.ts +29 -0
- package/dist/runtime/types/browser.mjs +0 -0
- package/dist/runtime/types/d1.d.ts +44 -0
- package/dist/runtime/types/d1.mjs +0 -0
- package/dist/runtime/types/drizzle.d.ts +13 -0
- package/dist/runtime/types/drizzle.mjs +0 -0
- package/dist/runtime/types/durable-object.d.ts +51 -0
- package/dist/runtime/types/durable-object.mjs +0 -0
- package/dist/runtime/types/email.d.ts +66 -0
- package/dist/runtime/types/email.mjs +0 -0
- package/dist/runtime/types/hyperdrive.d.ts +18 -0
- package/dist/runtime/types/hyperdrive.mjs +0 -0
- package/dist/runtime/types/imports.d.ts +58 -0
- package/dist/runtime/types/index.d.ts +156 -0
- package/dist/runtime/types/index.mjs +13 -0
- package/dist/runtime/types/queue.d.ts +67 -0
- package/dist/runtime/types/queue.mjs +0 -0
- package/dist/runtime/types/r2.d.ts +108 -0
- package/dist/runtime/types/r2.mjs +0 -0
- package/dist/runtime/types/ratelimit.d.ts +21 -0
- package/dist/runtime/types/ratelimit.mjs +0 -0
- package/dist/runtime/types/vectorize.d.ts +74 -0
- package/dist/runtime/types/vectorize.mjs +0 -0
- package/dist/runtime/vectorize.d.ts +21 -0
- package/dist/runtime/vectorize.mjs +53 -0
- package/dist/types.d.ts +2750 -0
- package/dist/types.mjs +1 -0
- package/dist/utils/agents.d.ts +8 -0
- package/dist/utils/auth-schema.d.ts +11 -0
- package/dist/utils/bindings.d.ts +16 -0
- package/dist/utils/cms-schema.d.ts +1 -0
- package/dist/utils/cms-sources.d.ts +62 -0
- package/dist/utils/drizzle.d.ts +42 -0
- package/dist/utils/jobs.d.ts +8 -0
- package/dist/utils/local-db.d.ts +28 -0
- package/dist/utils/notifications-schema.d.ts +15 -0
- package/dist/utils/payments-schema.d.ts +11 -0
- package/dist/utils/pkg.d.ts +15 -0
- package/dist/utils/queue.d.ts +8 -0
- package/dist/utils/tenants-schema.d.ts +11 -0
- package/package.json +104 -0
|
@@ -0,0 +1,2249 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, onMounted } from "vue"
|
|
3
|
+
|
|
4
|
+
definePageMeta({
|
|
5
|
+
layout: false,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
const router = useRouter()
|
|
10
|
+
|
|
11
|
+
const runtimeConfig = useRuntimeConfig()
|
|
12
|
+
const cmsConfig = (runtimeConfig.public as any)?.qarpeo?.cms || {}
|
|
13
|
+
const apiPath = cmsConfig.apiPath || "/api/cms"
|
|
14
|
+
const hasDeployTrigger = Boolean(cmsConfig.hasDeployTrigger)
|
|
15
|
+
|
|
16
|
+
interface CMSCollectionItem {
|
|
17
|
+
id: string
|
|
18
|
+
name: string
|
|
19
|
+
description?: string | null
|
|
20
|
+
schemaJson?: string
|
|
21
|
+
fields?: Record<string, any>
|
|
22
|
+
source?: string
|
|
23
|
+
mode?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CMSEntryItem {
|
|
27
|
+
id: string
|
|
28
|
+
collectionId: string
|
|
29
|
+
slug: string
|
|
30
|
+
title: string
|
|
31
|
+
description?: string | null
|
|
32
|
+
coverImage?: string | null
|
|
33
|
+
tags?: string | null
|
|
34
|
+
order?: number
|
|
35
|
+
contentMd: string
|
|
36
|
+
status: string
|
|
37
|
+
authorId?: string | null
|
|
38
|
+
publishedAt?: number | null
|
|
39
|
+
createdAt?: number
|
|
40
|
+
updatedAt?: number
|
|
41
|
+
metadata?: Record<string, any>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// State
|
|
45
|
+
const user = ref<any>(null)
|
|
46
|
+
const collections = ref<CMSCollectionItem[]>([])
|
|
47
|
+
const selectedCollectionId = computed(() => (route.query.collection as string) || "")
|
|
48
|
+
const entries = ref<CMSEntryItem[]>([])
|
|
49
|
+
const currentView = ref<"list" | "editor">("list")
|
|
50
|
+
|
|
51
|
+
const loading = ref(false)
|
|
52
|
+
const saving = ref(false)
|
|
53
|
+
const syncing = ref(false)
|
|
54
|
+
const exporting = ref(false)
|
|
55
|
+
const deploying = ref(false)
|
|
56
|
+
|
|
57
|
+
const searchQuery = ref("")
|
|
58
|
+
const statusFilter = ref("all")
|
|
59
|
+
const statusOptions = [
|
|
60
|
+
{ label: "All Statuses", value: "all" },
|
|
61
|
+
{ label: "Published", value: "published" },
|
|
62
|
+
{ label: "Drafts", value: "draft" },
|
|
63
|
+
{ label: "Pending Review", value: "pending_review" },
|
|
64
|
+
{ label: "Scheduled", value: "scheduled" },
|
|
65
|
+
{ label: "Archived", value: "archived" },
|
|
66
|
+
]
|
|
67
|
+
const tableColumns = [
|
|
68
|
+
{ accessorKey: "title", header: "Title" },
|
|
69
|
+
{ accessorKey: "slug", header: "Slug" },
|
|
70
|
+
{ accessorKey: "status", header: "Status" },
|
|
71
|
+
{ accessorKey: "updatedAt", header: "Updated" },
|
|
72
|
+
{ id: "actions", header: "" },
|
|
73
|
+
]
|
|
74
|
+
const openMediaLibrary = ref(false)
|
|
75
|
+
const showNewCollectionModal = ref(false)
|
|
76
|
+
const showPreview = ref(false)
|
|
77
|
+
const uploadProgress = ref(0)
|
|
78
|
+
const mediaItems = ref<any[]>([])
|
|
79
|
+
const mediaLoading = ref(false)
|
|
80
|
+
const mediaSearchQuery = ref("")
|
|
81
|
+
const mediaCopiedUrl = ref<string | null>(null)
|
|
82
|
+
const imageTargetField = ref<string | null>(null)
|
|
83
|
+
|
|
84
|
+
// Live preview state
|
|
85
|
+
const previewMode = ref<"doc" | "page">("doc")
|
|
86
|
+
const previewDevice = ref<"desktop" | "tablet" | "mobile">("desktop")
|
|
87
|
+
const previewIframeUrl = ref("")
|
|
88
|
+
const previewLoading = ref(false)
|
|
89
|
+
const previewCopied = ref(false)
|
|
90
|
+
|
|
91
|
+
const editorToolbarItems = [
|
|
92
|
+
[
|
|
93
|
+
{ kind: "undo", icon: "i-lucide-undo", tooltip: { text: "Undo" } },
|
|
94
|
+
{ kind: "redo", icon: "i-lucide-redo", tooltip: { text: "Redo" } },
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
{ kind: "heading", level: 1, icon: "i-lucide-heading-1", tooltip: { text: "Heading 1" } },
|
|
98
|
+
{ kind: "heading", level: 2, icon: "i-lucide-heading-2", tooltip: { text: "Heading 2" } },
|
|
99
|
+
{ kind: "heading", level: 3, icon: "i-lucide-heading-3", tooltip: { text: "Heading 3" } },
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
{ kind: "mark", mark: "bold", icon: "i-lucide-bold", tooltip: { text: "Bold" } },
|
|
103
|
+
{ kind: "mark", mark: "italic", icon: "i-lucide-italic", tooltip: { text: "Italic" } },
|
|
104
|
+
{
|
|
105
|
+
kind: "mark",
|
|
106
|
+
mark: "strike",
|
|
107
|
+
icon: "i-lucide-strikethrough",
|
|
108
|
+
tooltip: { text: "Strikethrough" },
|
|
109
|
+
},
|
|
110
|
+
{ kind: "mark", mark: "code", icon: "i-lucide-code", tooltip: { text: "Inline Code" } },
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
{ kind: "bulletList", icon: "i-lucide-list", tooltip: { text: "Bullet List" } },
|
|
114
|
+
{ kind: "orderedList", icon: "i-lucide-list-ordered", tooltip: { text: "Numbered List" } },
|
|
115
|
+
{ kind: "blockquote", icon: "i-lucide-quote", tooltip: { text: "Blockquote" } },
|
|
116
|
+
{ kind: "codeBlock", icon: "i-lucide-square-code", tooltip: { text: "Code Block" } },
|
|
117
|
+
{ kind: "horizontalRule", icon: "i-lucide-minus", tooltip: { text: "Divider" } },
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
{ kind: "link", icon: "i-lucide-link", tooltip: { text: "Link" } },
|
|
121
|
+
{
|
|
122
|
+
icon: "i-lucide-image",
|
|
123
|
+
tooltip: { text: "Insert Image" },
|
|
124
|
+
onClick: () => {
|
|
125
|
+
imageTargetField.value = null
|
|
126
|
+
openMediaLibrary.value = true
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
const newCol = ref({ id: "", name: "", description: "" })
|
|
133
|
+
const scheduledPublishDate = ref("")
|
|
134
|
+
|
|
135
|
+
const activeEntry = ref<CMSEntryItem>({
|
|
136
|
+
id: "",
|
|
137
|
+
collectionId: "",
|
|
138
|
+
slug: "",
|
|
139
|
+
title: "",
|
|
140
|
+
description: "",
|
|
141
|
+
coverImage: "",
|
|
142
|
+
tags: "",
|
|
143
|
+
order: 0,
|
|
144
|
+
contentMd: "",
|
|
145
|
+
status: "draft",
|
|
146
|
+
metadata: {},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Computeds
|
|
150
|
+
const currentCollection = computed(() =>
|
|
151
|
+
collections.value.find((c) => c.id === selectedCollectionId.value),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const collectionNavItems = computed(() =>
|
|
155
|
+
collections.value.map((col) => ({
|
|
156
|
+
label: col.name,
|
|
157
|
+
value: col.id,
|
|
158
|
+
badge: col.source
|
|
159
|
+
? {
|
|
160
|
+
label: col.source,
|
|
161
|
+
size: "xs" as const,
|
|
162
|
+
variant: "subtle" as const,
|
|
163
|
+
color: "neutral" as const,
|
|
164
|
+
}
|
|
165
|
+
: undefined,
|
|
166
|
+
onSelect: () => selectCollection(col.id),
|
|
167
|
+
})),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const collectionConfig = computed(() => {
|
|
171
|
+
return cmsConfig.collections?.[selectedCollectionId.value] || {}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const collectionMode = computed<string>(() => {
|
|
175
|
+
return collectionConfig.value.mode || currentCollection.value?.mode || "db"
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const isFileLocked = computed(() => collectionMode.value === "file-lock")
|
|
179
|
+
const isGitSync = computed(() => collectionMode.value === "git-sync")
|
|
180
|
+
|
|
181
|
+
const activeApproval = computed(() => {
|
|
182
|
+
return collectionConfig.value.approval
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const hasApproval = computed(() => Boolean(activeApproval.value))
|
|
186
|
+
|
|
187
|
+
const isAuthor = computed(() => {
|
|
188
|
+
return (
|
|
189
|
+
activeEntry.value.authorId && user.value?.id && activeEntry.value.authorId === user.value.id
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const canApprove = computed(() => {
|
|
194
|
+
if (!hasApproval.value) return true
|
|
195
|
+
const app = activeApproval.value
|
|
196
|
+
const u = user.value
|
|
197
|
+
if (!u) return false
|
|
198
|
+
const role = u.role || "user"
|
|
199
|
+
const isApprover = u.isAdmin || (app.roles && app.roles.includes(role))
|
|
200
|
+
if (!isApprover) return false
|
|
201
|
+
if (!app.allowSelfApproval && isAuthor.value) return false
|
|
202
|
+
return true
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const currentCollectionFields = computed<Record<string, any>>(() => {
|
|
206
|
+
const staticFields = collectionConfig.value.fields || {}
|
|
207
|
+
const dbFields = currentCollection.value?.fields || {}
|
|
208
|
+
return { ...staticFields, ...dbFields }
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const hasCustomFields = computed(() => Object.keys(currentCollectionFields.value).length > 0)
|
|
212
|
+
|
|
213
|
+
// Methods
|
|
214
|
+
async function fetchUser() {
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch("/api/auth/user")
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
const data = await res.json()
|
|
219
|
+
user.value = data.user
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function fetchCollections() {
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`${apiPath}/collections`)
|
|
227
|
+
const data = await res.json()
|
|
228
|
+
collections.value = data.collections || []
|
|
229
|
+
|
|
230
|
+
const currentId = selectedCollectionId.value
|
|
231
|
+
const exists = collections.value.some((c) => c.id === currentId)
|
|
232
|
+
if (exists) {
|
|
233
|
+
fetchEntries()
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error("Failed to load collections:", err)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function fetchEntries() {
|
|
241
|
+
if (!selectedCollectionId.value) return
|
|
242
|
+
loading.value = true
|
|
243
|
+
try {
|
|
244
|
+
const params = new URLSearchParams({ limit: "50" })
|
|
245
|
+
params.set("status", statusFilter.value)
|
|
246
|
+
const res = await fetch(`${apiPath}/${selectedCollectionId.value}?${params.toString()}`)
|
|
247
|
+
const data = await res.json()
|
|
248
|
+
entries.value = data.entries || []
|
|
249
|
+
} finally {
|
|
250
|
+
loading.value = false
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function selectCollection(id: string) {
|
|
255
|
+
const query = { ...route.query }
|
|
256
|
+
if (id) {
|
|
257
|
+
query.collection = id
|
|
258
|
+
} else {
|
|
259
|
+
delete query.collection
|
|
260
|
+
}
|
|
261
|
+
router.replace({ query })
|
|
262
|
+
currentView.value = "list"
|
|
263
|
+
if (id) {
|
|
264
|
+
fetchEntries()
|
|
265
|
+
} else {
|
|
266
|
+
entries.value = []
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function backToList() {
|
|
271
|
+
currentView.value = "list"
|
|
272
|
+
fetchEntries()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
watch(selectedCollectionId, (newId) => {
|
|
276
|
+
currentView.value = "list"
|
|
277
|
+
if (newId) {
|
|
278
|
+
fetchEntries()
|
|
279
|
+
} else {
|
|
280
|
+
entries.value = []
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
function createNewEntry() {
|
|
285
|
+
const meta: Record<string, any> = {}
|
|
286
|
+
for (const [k, fld] of Object.entries(currentCollectionFields.value)) {
|
|
287
|
+
if (fld.default !== undefined) meta[k] = fld.default
|
|
288
|
+
}
|
|
289
|
+
activeEntry.value = {
|
|
290
|
+
id: "",
|
|
291
|
+
collectionId: selectedCollectionId.value,
|
|
292
|
+
title: "",
|
|
293
|
+
slug: "",
|
|
294
|
+
description: "",
|
|
295
|
+
coverImage: "",
|
|
296
|
+
tags: "",
|
|
297
|
+
order: 0,
|
|
298
|
+
contentMd: "# New Entry\n\nWrite content here...",
|
|
299
|
+
status: "draft",
|
|
300
|
+
metadata: meta,
|
|
301
|
+
}
|
|
302
|
+
scheduledPublishDate.value = ""
|
|
303
|
+
currentView.value = "editor"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function editEntry(entry: CMSEntryItem) {
|
|
307
|
+
const meta = entry.metadata ? { ...entry.metadata } : {}
|
|
308
|
+
for (const [k, fld] of Object.entries(currentCollectionFields.value)) {
|
|
309
|
+
if (meta[k] === undefined && fld.default !== undefined) {
|
|
310
|
+
meta[k] = fld.default
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
activeEntry.value = {
|
|
314
|
+
...entry,
|
|
315
|
+
contentMd: entry.contentMd || "",
|
|
316
|
+
metadata: meta,
|
|
317
|
+
}
|
|
318
|
+
if (entry.publishedAt && entry.status === "scheduled") {
|
|
319
|
+
const d = new Date(entry.publishedAt)
|
|
320
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
321
|
+
scheduledPublishDate.value = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
322
|
+
} else {
|
|
323
|
+
scheduledPublishDate.value = ""
|
|
324
|
+
}
|
|
325
|
+
currentView.value = "editor"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function onTitleChange() {
|
|
329
|
+
if (!activeEntry.value.id) {
|
|
330
|
+
activeEntry.value.slug = activeEntry.value.title
|
|
331
|
+
.toLowerCase()
|
|
332
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
333
|
+
.replace(/(^-|-$)/g, "")
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function saveEntry(targetStatus?: string) {
|
|
338
|
+
saving.value = true
|
|
339
|
+
try {
|
|
340
|
+
const scheduledTimestamp = scheduledPublishDate.value
|
|
341
|
+
? new Date(scheduledPublishDate.value).getTime()
|
|
342
|
+
: undefined
|
|
343
|
+
|
|
344
|
+
const payload = {
|
|
345
|
+
title: activeEntry.value.title,
|
|
346
|
+
slug: activeEntry.value.slug,
|
|
347
|
+
description: activeEntry.value.description,
|
|
348
|
+
coverImage: activeEntry.value.coverImage,
|
|
349
|
+
tags: activeEntry.value.tags,
|
|
350
|
+
order: Number(activeEntry.value.order) || 0,
|
|
351
|
+
contentMd: activeEntry.value.contentMd,
|
|
352
|
+
status: targetStatus || activeEntry.value.status,
|
|
353
|
+
publishedAt:
|
|
354
|
+
targetStatus === "scheduled"
|
|
355
|
+
? scheduledTimestamp
|
|
356
|
+
: targetStatus === "published"
|
|
357
|
+
? activeEntry.value.publishedAt || Date.now()
|
|
358
|
+
: null,
|
|
359
|
+
metadata: activeEntry.value.metadata || {},
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let res: Response
|
|
363
|
+
if (activeEntry.value.id) {
|
|
364
|
+
res = await fetch(`${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}`, {
|
|
365
|
+
method: "PUT",
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
body: JSON.stringify(payload),
|
|
368
|
+
})
|
|
369
|
+
} else {
|
|
370
|
+
res = await fetch(`${apiPath}/${selectedCollectionId.value}`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: { "Content-Type": "application/json" },
|
|
373
|
+
body: JSON.stringify(payload),
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (res.ok) {
|
|
378
|
+
const data = await res.json()
|
|
379
|
+
activeEntry.value = data.entry
|
|
380
|
+
alert("Saved successfully!")
|
|
381
|
+
fetchEntries()
|
|
382
|
+
} else {
|
|
383
|
+
const err = await res.json().catch(() => ({}))
|
|
384
|
+
alert(err.message || "Failed to save")
|
|
385
|
+
}
|
|
386
|
+
} finally {
|
|
387
|
+
saving.value = false
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function togglePublish(entry: CMSEntryItem) {
|
|
392
|
+
const nextStatus = entry.status === "published" ? "draft" : "published"
|
|
393
|
+
const res = await fetch(`${apiPath}/${selectedCollectionId.value}/${entry.id}/publish`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: { "Content-Type": "application/json" },
|
|
396
|
+
body: JSON.stringify({ status: nextStatus }),
|
|
397
|
+
})
|
|
398
|
+
if (res.ok) fetchEntries()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function deleteEntry(id: string) {
|
|
402
|
+
if (!confirm("Are you sure you want to delete this entry?")) return
|
|
403
|
+
const res = await fetch(`${apiPath}/${selectedCollectionId.value}/${id}`, {
|
|
404
|
+
method: "DELETE",
|
|
405
|
+
})
|
|
406
|
+
if (res.ok) fetchEntries()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function submitForReview() {
|
|
410
|
+
if (!activeEntry.value.id) {
|
|
411
|
+
await saveEntry("draft")
|
|
412
|
+
}
|
|
413
|
+
if (!activeEntry.value.id) return
|
|
414
|
+
const note = prompt("Optional note for reviewer:")
|
|
415
|
+
saving.value = true
|
|
416
|
+
try {
|
|
417
|
+
const res = await fetch(
|
|
418
|
+
`${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/review`,
|
|
419
|
+
{
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { "Content-Type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ action: "submit", note: note || undefined }),
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
if (res.ok) {
|
|
426
|
+
const data = await res.json()
|
|
427
|
+
activeEntry.value = data.entry
|
|
428
|
+
alert("Submitted for review!")
|
|
429
|
+
fetchEntries()
|
|
430
|
+
} else {
|
|
431
|
+
const err = await res.json().catch(() => ({}))
|
|
432
|
+
alert(err.message || "Failed to submit for review")
|
|
433
|
+
}
|
|
434
|
+
} finally {
|
|
435
|
+
saving.value = false
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function rejectReview() {
|
|
440
|
+
if (!activeEntry.value.id) return
|
|
441
|
+
const reason = prompt("Reason for requesting changes:")
|
|
442
|
+
if (reason === null) return
|
|
443
|
+
saving.value = true
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch(
|
|
446
|
+
`${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/review`,
|
|
447
|
+
{
|
|
448
|
+
method: "POST",
|
|
449
|
+
headers: { "Content-Type": "application/json" },
|
|
450
|
+
body: JSON.stringify({ action: "reject", reason: reason || undefined }),
|
|
451
|
+
},
|
|
452
|
+
)
|
|
453
|
+
if (res.ok) {
|
|
454
|
+
const data = await res.json()
|
|
455
|
+
activeEntry.value = data.entry
|
|
456
|
+
alert("Changes requested. Entry reverted to draft.")
|
|
457
|
+
fetchEntries()
|
|
458
|
+
} else {
|
|
459
|
+
const err = await res.json().catch(() => ({}))
|
|
460
|
+
alert(err.message || "Failed to reject review")
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
saving.value = false
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function getPreviewUrl(): Promise<string | null> {
|
|
468
|
+
if (!activeEntry.value.id) return null
|
|
469
|
+
try {
|
|
470
|
+
const res = await fetch(
|
|
471
|
+
`${apiPath}/${selectedCollectionId.value}/${activeEntry.value.id}/preview-token`,
|
|
472
|
+
{ method: "POST" },
|
|
473
|
+
)
|
|
474
|
+
if (res.ok) {
|
|
475
|
+
const data = await res.json()
|
|
476
|
+
const slug = activeEntry.value.slug
|
|
477
|
+
const col = selectedCollectionId.value
|
|
478
|
+
return `${window.location.origin}/${col}/${slug}?preview=true&preview_token=${encodeURIComponent(data.token)}&expires=${data.expires}`
|
|
479
|
+
}
|
|
480
|
+
} catch (e) {
|
|
481
|
+
console.error("Failed to generate preview token:", e)
|
|
482
|
+
}
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function loadPagePreview() {
|
|
487
|
+
if (!activeEntry.value.id) return
|
|
488
|
+
previewLoading.value = true
|
|
489
|
+
const url = await getPreviewUrl()
|
|
490
|
+
if (url) {
|
|
491
|
+
previewIframeUrl.value = url
|
|
492
|
+
}
|
|
493
|
+
previewLoading.value = false
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function copyPreviewLink() {
|
|
497
|
+
if (!activeEntry.value.id) return
|
|
498
|
+
const url = await getPreviewUrl()
|
|
499
|
+
if (url) {
|
|
500
|
+
await navigator.clipboard.writeText(url)
|
|
501
|
+
previewCopied.value = true
|
|
502
|
+
setTimeout(() => {
|
|
503
|
+
previewCopied.value = false
|
|
504
|
+
}, 2500)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function openPreviewInNewTab() {
|
|
509
|
+
if (previewIframeUrl.value) {
|
|
510
|
+
window.open(previewIframeUrl.value, "_blank")
|
|
511
|
+
} else {
|
|
512
|
+
getPreviewUrl().then((url) => {
|
|
513
|
+
if (url) window.open(url, "_blank")
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
watch([showPreview, previewMode], async ([open, mode]) => {
|
|
519
|
+
if (open && mode === "page" && !previewIframeUrl.value) {
|
|
520
|
+
await loadPagePreview()
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
async function triggerSync() {
|
|
525
|
+
syncing.value = true
|
|
526
|
+
try {
|
|
527
|
+
const res = await fetch(`${apiPath}/sync`, { method: "POST" })
|
|
528
|
+
if (res.ok) {
|
|
529
|
+
const data = await res.json()
|
|
530
|
+
alert(`Sync completed! Added/updated: ${data.synced || 0} entries.`)
|
|
531
|
+
fetchEntries()
|
|
532
|
+
}
|
|
533
|
+
} finally {
|
|
534
|
+
syncing.value = false
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function triggerExport() {
|
|
539
|
+
exporting.value = true
|
|
540
|
+
try {
|
|
541
|
+
const res = await fetch(`${apiPath}/export`, { method: "POST" })
|
|
542
|
+
if (res.ok) {
|
|
543
|
+
const data = await res.json()
|
|
544
|
+
alert(`Export completed! Written: ${data.exported || 0} files.`)
|
|
545
|
+
}
|
|
546
|
+
} finally {
|
|
547
|
+
exporting.value = false
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function triggerDeploy() {
|
|
552
|
+
if (!confirm("Trigger a site redeploy now?")) return
|
|
553
|
+
deploying.value = true
|
|
554
|
+
try {
|
|
555
|
+
const res = await fetch(`${apiPath}/deploy`, { method: "POST" })
|
|
556
|
+
if (res.ok) {
|
|
557
|
+
const data = await res.json()
|
|
558
|
+
alert("Deploy triggered! (" + (data.triggered || []).join(", ") + ")")
|
|
559
|
+
} else {
|
|
560
|
+
const err = await res.json().catch(() => ({}))
|
|
561
|
+
alert("Deploy failed: " + (err.message || res.statusText))
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {
|
|
564
|
+
alert("Deploy error: " + e)
|
|
565
|
+
} finally {
|
|
566
|
+
deploying.value = false
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function createCollection() {
|
|
571
|
+
if (!newCol.value.id || !newCol.value.name) return
|
|
572
|
+
const createdId = newCol.value.id
|
|
573
|
+
const res = await fetch(`${apiPath}/collections`, {
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: { "Content-Type": "application/json" },
|
|
576
|
+
body: JSON.stringify(newCol.value),
|
|
577
|
+
})
|
|
578
|
+
if (res.ok) {
|
|
579
|
+
showNewCollectionModal.value = false
|
|
580
|
+
newCol.value = { id: "", name: "", description: "" }
|
|
581
|
+
await fetchCollections()
|
|
582
|
+
selectCollection(createdId)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const filteredMediaItems = computed(() => {
|
|
587
|
+
if (!mediaSearchQuery.value.trim()) return mediaItems.value
|
|
588
|
+
const q = mediaSearchQuery.value.toLowerCase()
|
|
589
|
+
return mediaItems.value.filter((item) =>
|
|
590
|
+
(item.pathname || item.key || item.url || "").toLowerCase().includes(q),
|
|
591
|
+
)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
async function fetchMediaFiles() {
|
|
595
|
+
mediaLoading.value = true
|
|
596
|
+
try {
|
|
597
|
+
const res = await fetch("/api/storage/files")
|
|
598
|
+
if (res.ok) {
|
|
599
|
+
const data = await res.json()
|
|
600
|
+
mediaItems.value = data.files || []
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.error("Failed to load media files:", e)
|
|
604
|
+
} finally {
|
|
605
|
+
mediaLoading.value = false
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
watch(openMediaLibrary, (isOpen) => {
|
|
610
|
+
if (isOpen) {
|
|
611
|
+
fetchMediaFiles()
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
function formatBytes(bytes: number): string {
|
|
616
|
+
if (!bytes || bytes <= 0) return "0 B"
|
|
617
|
+
const k = 1024
|
|
618
|
+
const sizes = ["B", "KB", "MB", "GB"]
|
|
619
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
620
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function copyMediaUrl(url: string) {
|
|
624
|
+
navigator.clipboard.writeText(url)
|
|
625
|
+
mediaCopiedUrl.value = url
|
|
626
|
+
setTimeout(() => {
|
|
627
|
+
if (mediaCopiedUrl.value === url) mediaCopiedUrl.value = null
|
|
628
|
+
}, 2000)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function deleteMediaItem(item: any) {
|
|
632
|
+
if (
|
|
633
|
+
!confirm(`Are you sure you want to delete "${item.pathname || item.key}" from Cloudflare R2?`)
|
|
634
|
+
)
|
|
635
|
+
return
|
|
636
|
+
try {
|
|
637
|
+
const res = await fetch(`/api/storage/file/${encodeURIComponent(item.key)}`, {
|
|
638
|
+
method: "DELETE",
|
|
639
|
+
})
|
|
640
|
+
if (res.ok) {
|
|
641
|
+
mediaItems.value = mediaItems.value.filter((m) => m.key !== item.key)
|
|
642
|
+
}
|
|
643
|
+
} catch (e) {
|
|
644
|
+
console.error("Failed to delete media file:", e)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function handleFileUpload(e: any) {
|
|
649
|
+
const file = e.target.files?.[0]
|
|
650
|
+
if (!file) return
|
|
651
|
+
uploadProgress.value = 10
|
|
652
|
+
const formData = new FormData()
|
|
653
|
+
formData.append("file", file)
|
|
654
|
+
try {
|
|
655
|
+
const res = await fetch("/api/storage/upload", {
|
|
656
|
+
method: "POST",
|
|
657
|
+
body: formData,
|
|
658
|
+
})
|
|
659
|
+
uploadProgress.value = 100
|
|
660
|
+
if (res.ok) {
|
|
661
|
+
const data = await res.json()
|
|
662
|
+
if (data.file) {
|
|
663
|
+
mediaItems.value.unshift(data.file)
|
|
664
|
+
} else {
|
|
665
|
+
await fetchMediaFiles()
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} finally {
|
|
669
|
+
setTimeout(() => {
|
|
670
|
+
uploadProgress.value = 0
|
|
671
|
+
}, 1000)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function pickImageForCover() {
|
|
676
|
+
imageTargetField.value = "coverImage"
|
|
677
|
+
openMediaLibrary.value = true
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function pickImageForField(fieldKey: string) {
|
|
681
|
+
imageTargetField.value = fieldKey
|
|
682
|
+
openMediaLibrary.value = true
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function selectMediaItem(url: string) {
|
|
686
|
+
if (imageTargetField.value === "coverImage") {
|
|
687
|
+
activeEntry.value.coverImage = url
|
|
688
|
+
} else if (imageTargetField.value) {
|
|
689
|
+
if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
|
|
690
|
+
activeEntry.value.metadata[imageTargetField.value] = url
|
|
691
|
+
} else {
|
|
692
|
+
insertImageMarkdown(url)
|
|
693
|
+
}
|
|
694
|
+
openMediaLibrary.value = false
|
|
695
|
+
imageTargetField.value = null
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function insertImageMarkdown(url: string) {
|
|
699
|
+
activeEntry.value.contentMd += `\n\n\n`
|
|
700
|
+
openMediaLibrary.value = false
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function formatJsonField(val: any) {
|
|
704
|
+
if (val === undefined || val === null) return ""
|
|
705
|
+
if (typeof val === "string") return val
|
|
706
|
+
try {
|
|
707
|
+
return JSON.stringify(val, null, 2)
|
|
708
|
+
} catch {
|
|
709
|
+
return String(val)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function updateJsonField(key: string, text: string) {
|
|
714
|
+
if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
|
|
715
|
+
try {
|
|
716
|
+
activeEntry.value.metadata[key] = JSON.parse(text)
|
|
717
|
+
} catch {
|
|
718
|
+
activeEntry.value.metadata[key] = text
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const collapsedBlocks = ref<Record<string, boolean>>({})
|
|
723
|
+
|
|
724
|
+
function getBlocksList(key: string | number): any[] {
|
|
725
|
+
if (!activeEntry.value.metadata) activeEntry.value.metadata = {}
|
|
726
|
+
if (!Array.isArray(activeEntry.value.metadata[key])) {
|
|
727
|
+
activeEntry.value.metadata[key] = []
|
|
728
|
+
}
|
|
729
|
+
return activeEntry.value.metadata[key]
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function isBlockCollapsed(fieldKey: string | number, bIdx: number): boolean {
|
|
733
|
+
return Boolean(collapsedBlocks.value[`${fieldKey}:${bIdx}`])
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function toggleBlockCollapse(fieldKey: string | number, bIdx: number) {
|
|
737
|
+
const k = `${fieldKey}:${bIdx}`
|
|
738
|
+
collapsedBlocks.value[k] = !collapsedBlocks.value[k]
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function moveBlock(fieldKey: string | number, bIdx: number, direction: number) {
|
|
742
|
+
const list = getBlocksList(fieldKey)
|
|
743
|
+
const target = bIdx + direction
|
|
744
|
+
if (target < 0 || target >= list.length) return
|
|
745
|
+
const item = list.splice(bIdx, 1)[0]
|
|
746
|
+
list.splice(target, 0, item)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function duplicateBlock(fieldKey: string | number, bIdx: number) {
|
|
750
|
+
const list = getBlocksList(fieldKey)
|
|
751
|
+
const block = list[bIdx]
|
|
752
|
+
if (!block) return
|
|
753
|
+
const clone = JSON.parse(JSON.stringify(block))
|
|
754
|
+
clone._id = crypto.randomUUID().slice(0, 8)
|
|
755
|
+
list.splice(bIdx + 1, 0, clone)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function deleteBlock(fieldKey: string | number, bIdx: number) {
|
|
759
|
+
const list = getBlocksList(fieldKey)
|
|
760
|
+
list.splice(bIdx, 1)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function addBlock(fieldKey: string | number, blockType: string, blockDef?: any) {
|
|
764
|
+
const list = getBlocksList(fieldKey)
|
|
765
|
+
const newBlock: Record<string, any> = {
|
|
766
|
+
_id: crypto.randomUUID().slice(0, 8),
|
|
767
|
+
_type: blockType,
|
|
768
|
+
}
|
|
769
|
+
if (blockDef?.fields) {
|
|
770
|
+
for (const [k, f] of Object.entries<any>(blockDef.fields)) {
|
|
771
|
+
if (f.default !== undefined) {
|
|
772
|
+
newBlock[k] = f.default
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
list.push(newBlock)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function getBlockMenuItems(fieldKey: string | number, blocksDict: Record<string, any>) {
|
|
780
|
+
return [
|
|
781
|
+
Object.entries(blocksDict).map(([type, def]) => ({
|
|
782
|
+
label: def.label || type,
|
|
783
|
+
icon: def.icon || "i-lucide-box",
|
|
784
|
+
onSelect: () => addBlock(fieldKey, type, def),
|
|
785
|
+
})),
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function updateBlockJsonField(block: any, fieldKey: string, text: string) {
|
|
790
|
+
try {
|
|
791
|
+
block[fieldKey] = JSON.parse(text)
|
|
792
|
+
} catch {
|
|
793
|
+
block[fieldKey] = text
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function copyToClipboard(text: string) {
|
|
798
|
+
navigator.clipboard.writeText(text)
|
|
799
|
+
alert("URL copied!")
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function formatDate(ts?: number | null) {
|
|
803
|
+
if (!ts) return "—"
|
|
804
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
805
|
+
month: "short",
|
|
806
|
+
day: "numeric",
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function formatDateTime(ts?: number | null) {
|
|
811
|
+
if (!ts) return ""
|
|
812
|
+
return new Date(ts).toLocaleString()
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function getStatusBadgeColor(status: string): "success" | "warning" | "info" | "error" | "neutral" {
|
|
816
|
+
if (status === "published") return "success"
|
|
817
|
+
if (status === "pending_review") return "warning"
|
|
818
|
+
if (status === "scheduled") return "info"
|
|
819
|
+
if (status === "archived") return "error"
|
|
820
|
+
return "neutral"
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function formatStatus(status: string) {
|
|
824
|
+
if (status === "pending_review") return "Pending Review"
|
|
825
|
+
if (status === "scheduled") return "Scheduled"
|
|
826
|
+
if (status === "published") return "Published"
|
|
827
|
+
if (status === "archived") return "Archived"
|
|
828
|
+
return "Draft"
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
let searchTimeout: any
|
|
832
|
+
function debouncedSearch() {
|
|
833
|
+
clearTimeout(searchTimeout)
|
|
834
|
+
searchTimeout = setTimeout(async () => {
|
|
835
|
+
if (!searchQuery.value) {
|
|
836
|
+
fetchEntries()
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
loading.value = true
|
|
840
|
+
try {
|
|
841
|
+
const res = await fetch(
|
|
842
|
+
`${apiPath}/search?q=${encodeURIComponent(searchQuery.value)}&collection=${selectedCollectionId.value}`,
|
|
843
|
+
)
|
|
844
|
+
const data = await res.json()
|
|
845
|
+
entries.value = data.results || []
|
|
846
|
+
} finally {
|
|
847
|
+
loading.value = false
|
|
848
|
+
}
|
|
849
|
+
}, 300)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function logout() {
|
|
853
|
+
fetch("/api/auth/logout", { method: "POST" }).then(() => {
|
|
854
|
+
window.location.reload()
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
onMounted(() => {
|
|
859
|
+
fetchUser()
|
|
860
|
+
fetchCollections()
|
|
861
|
+
})
|
|
862
|
+
</script>
|
|
863
|
+
|
|
864
|
+
<template>
|
|
865
|
+
<UDashboardGroup>
|
|
866
|
+
<!-- Left Sidebar: Collections & Management / Entry Settings -->
|
|
867
|
+
<UDashboardSidebar resizable collapsible :ui="{ header: 'border-b border-default' }">
|
|
868
|
+
<template #header="{ collapsed }">
|
|
869
|
+
<div
|
|
870
|
+
v-if="currentView === 'editor'"
|
|
871
|
+
class="flex items-center justify-between px-2 py-1 w-full overflow-hidden"
|
|
872
|
+
>
|
|
873
|
+
<div class="flex items-center gap-2 truncate">
|
|
874
|
+
<UButton
|
|
875
|
+
size="xs"
|
|
876
|
+
color="neutral"
|
|
877
|
+
variant="ghost"
|
|
878
|
+
icon="i-lucide-arrow-left"
|
|
879
|
+
@click="backToList"
|
|
880
|
+
/>
|
|
881
|
+
<span
|
|
882
|
+
v-if="!collapsed"
|
|
883
|
+
class="font-bold text-xs uppercase text-slate-300 tracking-wider truncate"
|
|
884
|
+
>
|
|
885
|
+
Entry Settings
|
|
886
|
+
</span>
|
|
887
|
+
</div>
|
|
888
|
+
<UBadge
|
|
889
|
+
v-if="!collapsed"
|
|
890
|
+
size="xs"
|
|
891
|
+
variant="subtle"
|
|
892
|
+
:color="getStatusBadgeColor(activeEntry.status)"
|
|
893
|
+
>
|
|
894
|
+
{{ formatStatus(activeEntry.status) }}
|
|
895
|
+
</UBadge>
|
|
896
|
+
</div>
|
|
897
|
+
<div v-else class="flex items-center gap-2 px-2 py-1 w-full overflow-hidden">
|
|
898
|
+
<span class="inline-block w-2.5 h-2.5 rounded-full bg-emerald-500 shrink-0"></span>
|
|
899
|
+
<span
|
|
900
|
+
v-if="!collapsed"
|
|
901
|
+
class="font-bold text-sm text-white flex items-center gap-1.5 truncate"
|
|
902
|
+
>
|
|
903
|
+
Qarpeo <UBadge size="xs" variant="subtle" color="primary">CMS</UBadge>
|
|
904
|
+
</span>
|
|
905
|
+
</div>
|
|
906
|
+
</template>
|
|
907
|
+
|
|
908
|
+
<!-- 1. Collections list (List Mode) -->
|
|
909
|
+
<div v-if="currentView === 'list'" class="space-y-1">
|
|
910
|
+
<UButton
|
|
911
|
+
label="Dashboard Overview"
|
|
912
|
+
size="xs"
|
|
913
|
+
color="neutral"
|
|
914
|
+
variant="ghost"
|
|
915
|
+
:active="!selectedCollectionId"
|
|
916
|
+
active-color="primary"
|
|
917
|
+
active-variant="subtle"
|
|
918
|
+
icon="i-lucide-layout-dashboard"
|
|
919
|
+
class="w-full justify-start mb-2"
|
|
920
|
+
@click="selectCollection('')"
|
|
921
|
+
/>
|
|
922
|
+
|
|
923
|
+
<div class="flex items-center justify-between py-1 mb-1">
|
|
924
|
+
<span class="text-xs uppercase font-semibold text-slate-400 tracking-wider"
|
|
925
|
+
>Collections</span
|
|
926
|
+
>
|
|
927
|
+
<UButton
|
|
928
|
+
label="New"
|
|
929
|
+
size="xs"
|
|
930
|
+
color="primary"
|
|
931
|
+
variant="ghost"
|
|
932
|
+
icon="i-lucide-plus"
|
|
933
|
+
@click="showNewCollectionModal = true"
|
|
934
|
+
/>
|
|
935
|
+
</div>
|
|
936
|
+
|
|
937
|
+
<UNavigationMenu
|
|
938
|
+
:items="collectionNavItems"
|
|
939
|
+
orientation="vertical"
|
|
940
|
+
:model-value="selectedCollectionId"
|
|
941
|
+
highlight
|
|
942
|
+
highlight-color="primary"
|
|
943
|
+
color="neutral"
|
|
944
|
+
/>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
947
|
+
<!-- 2. Entry Settings / Metadata Inputs (Editor Mode) -->
|
|
948
|
+
<div v-else-if="currentView === 'editor'" class="space-y-4 p-1 overflow-y-auto">
|
|
949
|
+
<UFormField label="Title" name="title">
|
|
950
|
+
<UInput v-model="activeEntry.title" class="w-full" @input="onTitleChange" />
|
|
951
|
+
</UFormField>
|
|
952
|
+
|
|
953
|
+
<UFormField label="Slug" name="slug">
|
|
954
|
+
<UInput v-model="activeEntry.slug" class="w-full font-mono text-xs" />
|
|
955
|
+
</UFormField>
|
|
956
|
+
|
|
957
|
+
<UFormField label="Description / Excerpt" name="description">
|
|
958
|
+
<UTextarea v-model="activeEntry.description" :rows="2" class="w-full" />
|
|
959
|
+
</UFormField>
|
|
960
|
+
|
|
961
|
+
<UFormField label="Cover Image" name="coverImage">
|
|
962
|
+
<div class="flex gap-2">
|
|
963
|
+
<UInput
|
|
964
|
+
v-model="activeEntry.coverImage"
|
|
965
|
+
placeholder="/api/storage/file/..."
|
|
966
|
+
class="flex-1"
|
|
967
|
+
/>
|
|
968
|
+
<UButton size="xs" color="neutral" variant="outline" @click="pickImageForCover"
|
|
969
|
+
>Pick</UButton
|
|
970
|
+
>
|
|
971
|
+
</div>
|
|
972
|
+
<div v-if="activeEntry.coverImage" class="mt-2">
|
|
973
|
+
<img
|
|
974
|
+
:src="activeEntry.coverImage"
|
|
975
|
+
class="w-full h-24 object-cover rounded border border-default"
|
|
976
|
+
/>
|
|
977
|
+
</div>
|
|
978
|
+
</UFormField>
|
|
979
|
+
|
|
980
|
+
<UFormField label="Tags (comma-separated)" name="tags">
|
|
981
|
+
<UInput v-model="activeEntry.tags" placeholder="news, edge, cloudflare" class="w-full" />
|
|
982
|
+
</UFormField>
|
|
983
|
+
|
|
984
|
+
<UFormField label="Order" name="order">
|
|
985
|
+
<UInput v-model.number="activeEntry.order" type="number" class="w-full" />
|
|
986
|
+
</UFormField>
|
|
987
|
+
|
|
988
|
+
<UFormField
|
|
989
|
+
label="Schedule Publish"
|
|
990
|
+
name="scheduledPublishDate"
|
|
991
|
+
:help="
|
|
992
|
+
activeEntry.publishedAt && activeEntry.status === 'scheduled'
|
|
993
|
+
? `Scheduled for ${formatDateTime(activeEntry.publishedAt)}`
|
|
994
|
+
: undefined
|
|
995
|
+
"
|
|
996
|
+
>
|
|
997
|
+
<UInput v-model="scheduledPublishDate" type="datetime-local" class="w-full" />
|
|
998
|
+
</UFormField>
|
|
999
|
+
|
|
1000
|
+
<!-- Custom Dynamic Fields -->
|
|
1001
|
+
<div v-if="hasCustomFields" class="pt-3 border-t border-default space-y-3">
|
|
1002
|
+
<div class="flex items-center justify-between">
|
|
1003
|
+
<span class="text-xs font-semibold text-slate-300 uppercase tracking-wider"
|
|
1004
|
+
>Custom Fields</span
|
|
1005
|
+
>
|
|
1006
|
+
<span class="text-[10px] text-slate-500 font-mono"
|
|
1007
|
+
>{{ Object.keys(currentCollectionFields).length }} fields</span
|
|
1008
|
+
>
|
|
1009
|
+
</div>
|
|
1010
|
+
|
|
1011
|
+
<UFormField
|
|
1012
|
+
v-for="(field, key) in currentCollectionFields"
|
|
1013
|
+
:key="key"
|
|
1014
|
+
:label="field.label || String(key)"
|
|
1015
|
+
:description="field.description"
|
|
1016
|
+
:required="field.required"
|
|
1017
|
+
:name="String(key)"
|
|
1018
|
+
>
|
|
1019
|
+
<!-- 1. String with options -->
|
|
1020
|
+
<template v-if="field.type === 'string' && field.options && field.options.length">
|
|
1021
|
+
<USelect
|
|
1022
|
+
v-model="activeEntry.metadata![key]"
|
|
1023
|
+
:items="field.options"
|
|
1024
|
+
label-key="label"
|
|
1025
|
+
value-key="value"
|
|
1026
|
+
placeholder="Select option..."
|
|
1027
|
+
class="w-full"
|
|
1028
|
+
/>
|
|
1029
|
+
</template>
|
|
1030
|
+
|
|
1031
|
+
<!-- 2. String standard -->
|
|
1032
|
+
<template v-else-if="field.type === 'string'">
|
|
1033
|
+
<UInput
|
|
1034
|
+
v-model="activeEntry.metadata![key]"
|
|
1035
|
+
:placeholder="field.default ? String(field.default) : ''"
|
|
1036
|
+
class="w-full"
|
|
1037
|
+
/>
|
|
1038
|
+
</template>
|
|
1039
|
+
|
|
1040
|
+
<!-- 3. Text multi-line -->
|
|
1041
|
+
<template v-else-if="field.type === 'text'">
|
|
1042
|
+
<UTextarea v-model="activeEntry.metadata![key]" :rows="2" class="w-full" />
|
|
1043
|
+
</template>
|
|
1044
|
+
|
|
1045
|
+
<!-- 4. Number -->
|
|
1046
|
+
<template v-else-if="field.type === 'number'">
|
|
1047
|
+
<UInput v-model.number="activeEntry.metadata![key]" type="number" class="w-full" />
|
|
1048
|
+
</template>
|
|
1049
|
+
|
|
1050
|
+
<!-- 5. Boolean -->
|
|
1051
|
+
<template v-else-if="field.type === 'boolean'">
|
|
1052
|
+
<div class="flex items-center gap-2 pt-1">
|
|
1053
|
+
<USwitch v-model="activeEntry.metadata![key]" />
|
|
1054
|
+
<span class="text-xs text-slate-300">{{
|
|
1055
|
+
activeEntry.metadata![key] ? "Enabled" : "Disabled"
|
|
1056
|
+
}}</span>
|
|
1057
|
+
</div>
|
|
1058
|
+
</template>
|
|
1059
|
+
|
|
1060
|
+
<!-- 6. Date -->
|
|
1061
|
+
<template v-else-if="field.type === 'date'">
|
|
1062
|
+
<UInput v-model="activeEntry.metadata![key]" type="date" class="w-full" />
|
|
1063
|
+
</template>
|
|
1064
|
+
|
|
1065
|
+
<!-- 7. Image -->
|
|
1066
|
+
<template v-else-if="field.type === 'image'">
|
|
1067
|
+
<div class="flex gap-2">
|
|
1068
|
+
<UInput
|
|
1069
|
+
v-model="activeEntry.metadata![key]"
|
|
1070
|
+
placeholder="/api/storage/file/..."
|
|
1071
|
+
class="flex-1"
|
|
1072
|
+
/>
|
|
1073
|
+
<UButton
|
|
1074
|
+
size="xs"
|
|
1075
|
+
color="neutral"
|
|
1076
|
+
variant="outline"
|
|
1077
|
+
@click="pickImageForField(String(key))"
|
|
1078
|
+
>Pick</UButton
|
|
1079
|
+
>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div v-if="activeEntry.metadata![key]" class="mt-1">
|
|
1082
|
+
<img
|
|
1083
|
+
:src="activeEntry.metadata![key]"
|
|
1084
|
+
class="w-16 h-16 object-cover rounded border border-default bg-muted"
|
|
1085
|
+
/>
|
|
1086
|
+
</div>
|
|
1087
|
+
</template>
|
|
1088
|
+
|
|
1089
|
+
<!-- 8. Tags -->
|
|
1090
|
+
<template v-else-if="field.type === 'tags'">
|
|
1091
|
+
<UInput
|
|
1092
|
+
v-model="activeEntry.metadata![key]"
|
|
1093
|
+
placeholder="tag1, tag2"
|
|
1094
|
+
class="w-full"
|
|
1095
|
+
/>
|
|
1096
|
+
</template>
|
|
1097
|
+
|
|
1098
|
+
<!-- 9. JSON -->
|
|
1099
|
+
<template v-else-if="field.type === 'json'">
|
|
1100
|
+
<UTextarea
|
|
1101
|
+
:model-value="formatJsonField(activeEntry.metadata![key])"
|
|
1102
|
+
:rows="3"
|
|
1103
|
+
class="w-full font-mono text-[11px]"
|
|
1104
|
+
@update:model-value="updateJsonField(String(key), $event)"
|
|
1105
|
+
/>
|
|
1106
|
+
</template>
|
|
1107
|
+
|
|
1108
|
+
<!-- 10. Blocks / Dynamic Zones (Page Builder) -->
|
|
1109
|
+
<template v-else-if="field.type === 'blocks'">
|
|
1110
|
+
<div class="space-y-3 pt-1">
|
|
1111
|
+
<div
|
|
1112
|
+
v-for="(block, bIdx) in getBlocksList(key)"
|
|
1113
|
+
:key="block._id || bIdx"
|
|
1114
|
+
class="border border-default rounded-lg bg-muted/20 overflow-hidden"
|
|
1115
|
+
>
|
|
1116
|
+
<!-- Block Header -->
|
|
1117
|
+
<div
|
|
1118
|
+
class="flex items-center justify-between px-3 py-2 bg-muted/50 border-b border-default select-none"
|
|
1119
|
+
>
|
|
1120
|
+
<div
|
|
1121
|
+
class="flex items-center gap-2 cursor-pointer flex-1"
|
|
1122
|
+
@click="toggleBlockCollapse(key, bIdx)"
|
|
1123
|
+
>
|
|
1124
|
+
<UIcon
|
|
1125
|
+
:name="
|
|
1126
|
+
isBlockCollapsed(key, bIdx)
|
|
1127
|
+
? 'i-lucide-chevron-right'
|
|
1128
|
+
: 'i-lucide-chevron-down'
|
|
1129
|
+
"
|
|
1130
|
+
class="w-4 h-4 text-muted-foreground"
|
|
1131
|
+
/>
|
|
1132
|
+
<UIcon
|
|
1133
|
+
:name="field.blocks?.[block._type]?.icon || 'i-lucide-box'"
|
|
1134
|
+
class="w-4 h-4 text-primary"
|
|
1135
|
+
/>
|
|
1136
|
+
<span class="font-medium text-xs">
|
|
1137
|
+
{{ field.blocks?.[block._type]?.label || block._type }}
|
|
1138
|
+
</span>
|
|
1139
|
+
<UBadge size="xs" variant="subtle" color="neutral">
|
|
1140
|
+
#{{ bIdx + 1 }}
|
|
1141
|
+
</UBadge>
|
|
1142
|
+
</div>
|
|
1143
|
+
|
|
1144
|
+
<div class="flex items-center gap-1">
|
|
1145
|
+
<UButton
|
|
1146
|
+
size="xs"
|
|
1147
|
+
color="neutral"
|
|
1148
|
+
variant="ghost"
|
|
1149
|
+
icon="i-lucide-arrow-up"
|
|
1150
|
+
:disabled="bIdx === 0"
|
|
1151
|
+
@click="moveBlock(key, bIdx, -1)"
|
|
1152
|
+
/>
|
|
1153
|
+
<UButton
|
|
1154
|
+
size="xs"
|
|
1155
|
+
color="neutral"
|
|
1156
|
+
variant="ghost"
|
|
1157
|
+
icon="i-lucide-arrow-down"
|
|
1158
|
+
:disabled="bIdx === getBlocksList(key).length - 1"
|
|
1159
|
+
@click="moveBlock(key, bIdx, 1)"
|
|
1160
|
+
/>
|
|
1161
|
+
<UButton
|
|
1162
|
+
size="xs"
|
|
1163
|
+
color="neutral"
|
|
1164
|
+
variant="ghost"
|
|
1165
|
+
icon="i-lucide-copy"
|
|
1166
|
+
@click="duplicateBlock(key, bIdx)"
|
|
1167
|
+
/>
|
|
1168
|
+
<UButton
|
|
1169
|
+
size="xs"
|
|
1170
|
+
color="error"
|
|
1171
|
+
variant="ghost"
|
|
1172
|
+
icon="i-lucide-trash-2"
|
|
1173
|
+
@click="deleteBlock(key, bIdx)"
|
|
1174
|
+
/>
|
|
1175
|
+
</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
|
|
1178
|
+
<!-- Block Fields -->
|
|
1179
|
+
<div v-show="!isBlockCollapsed(key, bIdx)" class="p-3 space-y-3 bg-card/30">
|
|
1180
|
+
<div
|
|
1181
|
+
v-for="(subField, subKey) in (field.blocks?.[block._type]?.fields || {})"
|
|
1182
|
+
:key="subKey"
|
|
1183
|
+
class="space-y-1"
|
|
1184
|
+
>
|
|
1185
|
+
<label class="text-[11px] font-medium text-muted-foreground">
|
|
1186
|
+
{{ subField.label || subKey }}
|
|
1187
|
+
<span v-if="subField.required" class="text-error">*</span>
|
|
1188
|
+
</label>
|
|
1189
|
+
|
|
1190
|
+
<template v-if="subField.type === 'string' && subField.options?.length">
|
|
1191
|
+
<USelect
|
|
1192
|
+
v-model="block[subKey]"
|
|
1193
|
+
:items="subField.options"
|
|
1194
|
+
label-key="label"
|
|
1195
|
+
value-key="value"
|
|
1196
|
+
class="w-full"
|
|
1197
|
+
/>
|
|
1198
|
+
</template>
|
|
1199
|
+
<template v-else-if="subField.type === 'string'">
|
|
1200
|
+
<UInput v-model="block[subKey]" class="w-full" />
|
|
1201
|
+
</template>
|
|
1202
|
+
<template v-else-if="subField.type === 'text'">
|
|
1203
|
+
<UTextarea v-model="block[subKey]" :rows="2" class="w-full" />
|
|
1204
|
+
</template>
|
|
1205
|
+
<template v-else-if="subField.type === 'number'">
|
|
1206
|
+
<UInput v-model.number="block[subKey]" type="number" class="w-full" />
|
|
1207
|
+
</template>
|
|
1208
|
+
<template v-else-if="subField.type === 'boolean'">
|
|
1209
|
+
<div class="flex items-center gap-2 pt-1">
|
|
1210
|
+
<USwitch v-model="block[subKey]" />
|
|
1211
|
+
<span class="text-xs text-muted-foreground">
|
|
1212
|
+
{{ block[subKey] ? "Enabled" : "Disabled" }}
|
|
1213
|
+
</span>
|
|
1214
|
+
</div>
|
|
1215
|
+
</template>
|
|
1216
|
+
<template v-else-if="subField.type === 'date'">
|
|
1217
|
+
<UInput v-model="block[subKey]" type="date" class="w-full" />
|
|
1218
|
+
</template>
|
|
1219
|
+
<template v-else-if="subField.type === 'image'">
|
|
1220
|
+
<UInput
|
|
1221
|
+
v-model="block[subKey]"
|
|
1222
|
+
placeholder="/api/storage/file/..."
|
|
1223
|
+
class="w-full"
|
|
1224
|
+
/>
|
|
1225
|
+
</template>
|
|
1226
|
+
<template v-else-if="subField.type === 'tags'">
|
|
1227
|
+
<UInput v-model="block[subKey]" placeholder="tag1, tag2" class="w-full" />
|
|
1228
|
+
</template>
|
|
1229
|
+
<template v-else-if="subField.type === 'json'">
|
|
1230
|
+
<UTextarea
|
|
1231
|
+
:model-value="formatJsonField(block[subKey])"
|
|
1232
|
+
:rows="2"
|
|
1233
|
+
class="w-full font-mono text-[11px]"
|
|
1234
|
+
@update:model-value="updateBlockJsonField(block, String(subKey), $event)"
|
|
1235
|
+
/>
|
|
1236
|
+
</template>
|
|
1237
|
+
</div>
|
|
1238
|
+
</div>
|
|
1239
|
+
</div>
|
|
1240
|
+
|
|
1241
|
+
<!-- Add Block Button -->
|
|
1242
|
+
<div
|
|
1243
|
+
v-if="field.blocks && Object.keys(field.blocks).length"
|
|
1244
|
+
class="flex flex-wrap items-center gap-2 pt-1"
|
|
1245
|
+
>
|
|
1246
|
+
<UDropdownMenu
|
|
1247
|
+
:items="getBlockMenuItems(key, field.blocks)"
|
|
1248
|
+
:content="{ align: 'start' }"
|
|
1249
|
+
>
|
|
1250
|
+
<UButton size="xs" color="primary" variant="soft" icon="i-lucide-plus">
|
|
1251
|
+
Add Block
|
|
1252
|
+
</UButton>
|
|
1253
|
+
</UDropdownMenu>
|
|
1254
|
+
<span class="text-[11px] text-muted-foreground">
|
|
1255
|
+
{{ getBlocksList(key).length }} block(s) added
|
|
1256
|
+
</span>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
</template>
|
|
1260
|
+
</UFormField>
|
|
1261
|
+
</div>
|
|
1262
|
+
</div>
|
|
1263
|
+
|
|
1264
|
+
<template #footer="{ collapsed }">
|
|
1265
|
+
<div v-if="currentView === 'editor'" class="p-2 border-t border-default w-full">
|
|
1266
|
+
<UButton
|
|
1267
|
+
size="xs"
|
|
1268
|
+
color="neutral"
|
|
1269
|
+
variant="ghost"
|
|
1270
|
+
icon="i-lucide-arrow-left"
|
|
1271
|
+
block
|
|
1272
|
+
@click="backToList"
|
|
1273
|
+
>
|
|
1274
|
+
<span v-if="!collapsed">Back to Collections</span>
|
|
1275
|
+
</UButton>
|
|
1276
|
+
</div>
|
|
1277
|
+
<div
|
|
1278
|
+
v-else-if="user"
|
|
1279
|
+
class="p-2 flex items-center justify-between w-full border-t border-default"
|
|
1280
|
+
>
|
|
1281
|
+
<div v-if="!collapsed" class="flex flex-col truncate pr-2">
|
|
1282
|
+
<span class="text-xs font-medium text-white truncate">{{
|
|
1283
|
+
user.name || user.email
|
|
1284
|
+
}}</span>
|
|
1285
|
+
<span class="text-[10px] text-slate-400 truncate">{{ user.role || "user" }}</span>
|
|
1286
|
+
</div>
|
|
1287
|
+
<UButton
|
|
1288
|
+
size="xs"
|
|
1289
|
+
color="error"
|
|
1290
|
+
variant="ghost"
|
|
1291
|
+
icon="i-lucide-log-out"
|
|
1292
|
+
@click="logout"
|
|
1293
|
+
/>
|
|
1294
|
+
</div>
|
|
1295
|
+
</template>
|
|
1296
|
+
</UDashboardSidebar>
|
|
1297
|
+
|
|
1298
|
+
<!-- Main Content Panel -->
|
|
1299
|
+
<UDashboardPanel>
|
|
1300
|
+
<template #header>
|
|
1301
|
+
<UDashboardNavbar>
|
|
1302
|
+
<template #title>
|
|
1303
|
+
<div class="flex items-center gap-2">
|
|
1304
|
+
<span class="font-semibold text-sm text-white">
|
|
1305
|
+
{{
|
|
1306
|
+
currentView === "editor"
|
|
1307
|
+
? activeEntry.title || "Untitled Entry"
|
|
1308
|
+
: currentCollection?.name || "Dashboard Overview"
|
|
1309
|
+
}}
|
|
1310
|
+
</span>
|
|
1311
|
+
<UBadge
|
|
1312
|
+
v-if="currentView === 'editor'"
|
|
1313
|
+
size="sm"
|
|
1314
|
+
variant="subtle"
|
|
1315
|
+
:color="getStatusBadgeColor(activeEntry.status)"
|
|
1316
|
+
>
|
|
1317
|
+
{{ formatStatus(activeEntry.status) }}
|
|
1318
|
+
</UBadge>
|
|
1319
|
+
<UBadge
|
|
1320
|
+
v-if="currentView === 'editor' && isGitSync"
|
|
1321
|
+
label="🌿 Git-sync active"
|
|
1322
|
+
size="xs"
|
|
1323
|
+
color="success"
|
|
1324
|
+
variant="subtle"
|
|
1325
|
+
/>
|
|
1326
|
+
<UBadge
|
|
1327
|
+
v-if="currentView === 'editor' && hasApproval"
|
|
1328
|
+
label="🛡️ Approval required"
|
|
1329
|
+
size="xs"
|
|
1330
|
+
color="primary"
|
|
1331
|
+
variant="subtle"
|
|
1332
|
+
/>
|
|
1333
|
+
</div>
|
|
1334
|
+
</template>
|
|
1335
|
+
|
|
1336
|
+
<template #right>
|
|
1337
|
+
<div class="flex items-center space-x-2">
|
|
1338
|
+
<!-- When in editor view, back button & editorial actions -->
|
|
1339
|
+
<template v-if="currentView === 'editor'">
|
|
1340
|
+
<UButton
|
|
1341
|
+
size="xs"
|
|
1342
|
+
color="neutral"
|
|
1343
|
+
variant="ghost"
|
|
1344
|
+
icon="i-lucide-arrow-left"
|
|
1345
|
+
@click="backToList"
|
|
1346
|
+
>
|
|
1347
|
+
Back
|
|
1348
|
+
</UButton>
|
|
1349
|
+
|
|
1350
|
+
<UButton
|
|
1351
|
+
size="xs"
|
|
1352
|
+
color="neutral"
|
|
1353
|
+
variant="outline"
|
|
1354
|
+
icon="i-lucide-link"
|
|
1355
|
+
@click="copyPreviewLink"
|
|
1356
|
+
>
|
|
1357
|
+
Share Preview
|
|
1358
|
+
</UButton>
|
|
1359
|
+
|
|
1360
|
+
<template v-if="!isFileLocked">
|
|
1361
|
+
<!-- Approval Workflow -->
|
|
1362
|
+
<template v-if="hasApproval">
|
|
1363
|
+
<UButton
|
|
1364
|
+
size="xs"
|
|
1365
|
+
color="neutral"
|
|
1366
|
+
variant="outline"
|
|
1367
|
+
:loading="saving"
|
|
1368
|
+
@click="saveEntry('draft')"
|
|
1369
|
+
>
|
|
1370
|
+
Save Draft
|
|
1371
|
+
</UButton>
|
|
1372
|
+
<UButton
|
|
1373
|
+
v-if="scheduledPublishDate && canApprove"
|
|
1374
|
+
size="xs"
|
|
1375
|
+
color="secondary"
|
|
1376
|
+
icon="i-lucide-clock"
|
|
1377
|
+
:loading="saving"
|
|
1378
|
+
@click="saveEntry('scheduled')"
|
|
1379
|
+
>
|
|
1380
|
+
Schedule
|
|
1381
|
+
</UButton>
|
|
1382
|
+
<UButton
|
|
1383
|
+
v-if="activeEntry.status === 'draft'"
|
|
1384
|
+
size="xs"
|
|
1385
|
+
color="primary"
|
|
1386
|
+
icon="i-lucide-send"
|
|
1387
|
+
:loading="saving"
|
|
1388
|
+
@click="submitForReview"
|
|
1389
|
+
>
|
|
1390
|
+
Submit for Review
|
|
1391
|
+
</UButton>
|
|
1392
|
+
<template v-else-if="activeEntry.status === 'pending_review'">
|
|
1393
|
+
<UButton
|
|
1394
|
+
v-if="canApprove"
|
|
1395
|
+
size="xs"
|
|
1396
|
+
color="error"
|
|
1397
|
+
variant="outline"
|
|
1398
|
+
:loading="saving"
|
|
1399
|
+
@click="rejectReview"
|
|
1400
|
+
>
|
|
1401
|
+
Request Changes
|
|
1402
|
+
</UButton>
|
|
1403
|
+
<UButton
|
|
1404
|
+
v-if="canApprove"
|
|
1405
|
+
size="xs"
|
|
1406
|
+
color="success"
|
|
1407
|
+
icon="i-lucide-check"
|
|
1408
|
+
:loading="saving"
|
|
1409
|
+
@click="saveEntry('published')"
|
|
1410
|
+
>
|
|
1411
|
+
Approve & Publish
|
|
1412
|
+
</UButton>
|
|
1413
|
+
<UBadge v-else color="warning" variant="subtle">
|
|
1414
|
+
⏳ Awaiting review
|
|
1415
|
+
{{
|
|
1416
|
+
isAuthor && !activeApproval?.allowSelfApproval
|
|
1417
|
+
? "(Self-approval disabled)"
|
|
1418
|
+
: ""
|
|
1419
|
+
}}
|
|
1420
|
+
</UBadge>
|
|
1421
|
+
</template>
|
|
1422
|
+
<template v-else-if="activeEntry.status === 'published'">
|
|
1423
|
+
<UButton
|
|
1424
|
+
size="xs"
|
|
1425
|
+
color="neutral"
|
|
1426
|
+
variant="outline"
|
|
1427
|
+
:loading="saving"
|
|
1428
|
+
@click="saveEntry('draft')"
|
|
1429
|
+
>
|
|
1430
|
+
Unpublish
|
|
1431
|
+
</UButton>
|
|
1432
|
+
<UButton
|
|
1433
|
+
v-if="canApprove"
|
|
1434
|
+
size="xs"
|
|
1435
|
+
color="success"
|
|
1436
|
+
:loading="saving"
|
|
1437
|
+
@click="saveEntry('published')"
|
|
1438
|
+
>
|
|
1439
|
+
Publish Changes
|
|
1440
|
+
</UButton>
|
|
1441
|
+
</template>
|
|
1442
|
+
</template>
|
|
1443
|
+
|
|
1444
|
+
<!-- Standard Workflow -->
|
|
1445
|
+
<template v-else>
|
|
1446
|
+
<UButton
|
|
1447
|
+
size="xs"
|
|
1448
|
+
color="neutral"
|
|
1449
|
+
variant="outline"
|
|
1450
|
+
:loading="saving"
|
|
1451
|
+
@click="saveEntry('draft')"
|
|
1452
|
+
>
|
|
1453
|
+
Save Draft
|
|
1454
|
+
</UButton>
|
|
1455
|
+
<UButton
|
|
1456
|
+
v-if="scheduledPublishDate"
|
|
1457
|
+
size="xs"
|
|
1458
|
+
color="secondary"
|
|
1459
|
+
icon="i-lucide-clock"
|
|
1460
|
+
:loading="saving"
|
|
1461
|
+
@click="saveEntry('scheduled')"
|
|
1462
|
+
>
|
|
1463
|
+
Schedule
|
|
1464
|
+
</UButton>
|
|
1465
|
+
<UButton
|
|
1466
|
+
size="xs"
|
|
1467
|
+
color="success"
|
|
1468
|
+
icon="i-lucide-upload"
|
|
1469
|
+
:loading="saving"
|
|
1470
|
+
@click="saveEntry('published')"
|
|
1471
|
+
>
|
|
1472
|
+
Publish
|
|
1473
|
+
</UButton>
|
|
1474
|
+
</template>
|
|
1475
|
+
</template>
|
|
1476
|
+
<UBadge v-else color="warning" variant="subtle">
|
|
1477
|
+
🔒 File-locked — edit source files
|
|
1478
|
+
</UBadge>
|
|
1479
|
+
</template>
|
|
1480
|
+
|
|
1481
|
+
<!-- Global actions -->
|
|
1482
|
+
<UButton
|
|
1483
|
+
size="xs"
|
|
1484
|
+
color="neutral"
|
|
1485
|
+
variant="outline"
|
|
1486
|
+
icon="i-lucide-image"
|
|
1487
|
+
@click="openMediaLibrary = true"
|
|
1488
|
+
>
|
|
1489
|
+
Media
|
|
1490
|
+
</UButton>
|
|
1491
|
+
|
|
1492
|
+
<UButton
|
|
1493
|
+
size="xs"
|
|
1494
|
+
color="neutral"
|
|
1495
|
+
variant="outline"
|
|
1496
|
+
icon="i-lucide-refresh-cw"
|
|
1497
|
+
:loading="syncing"
|
|
1498
|
+
@click="triggerSync"
|
|
1499
|
+
>
|
|
1500
|
+
Sync Files
|
|
1501
|
+
</UButton>
|
|
1502
|
+
|
|
1503
|
+
<UButton
|
|
1504
|
+
size="xs"
|
|
1505
|
+
color="neutral"
|
|
1506
|
+
variant="outline"
|
|
1507
|
+
icon="i-lucide-download"
|
|
1508
|
+
:loading="exporting"
|
|
1509
|
+
@click="triggerExport"
|
|
1510
|
+
>
|
|
1511
|
+
Export
|
|
1512
|
+
</UButton>
|
|
1513
|
+
|
|
1514
|
+
<UButton
|
|
1515
|
+
v-if="hasDeployTrigger"
|
|
1516
|
+
size="xs"
|
|
1517
|
+
color="primary"
|
|
1518
|
+
variant="solid"
|
|
1519
|
+
icon="i-lucide-rocket"
|
|
1520
|
+
:loading="deploying"
|
|
1521
|
+
@click="triggerDeploy"
|
|
1522
|
+
>
|
|
1523
|
+
Deploy
|
|
1524
|
+
</UButton>
|
|
1525
|
+
</div>
|
|
1526
|
+
</template>
|
|
1527
|
+
</UDashboardNavbar>
|
|
1528
|
+
|
|
1529
|
+
<!-- List View Toolbar inside DashboardPanel Header (When collection is selected) -->
|
|
1530
|
+
<UDashboardToolbar v-if="currentView === 'list' && selectedCollectionId">
|
|
1531
|
+
<template #left>
|
|
1532
|
+
<div class="flex items-center gap-3">
|
|
1533
|
+
<UInput
|
|
1534
|
+
v-model="searchQuery"
|
|
1535
|
+
placeholder="Search entries..."
|
|
1536
|
+
class="w-64"
|
|
1537
|
+
@input="debouncedSearch"
|
|
1538
|
+
/>
|
|
1539
|
+
<USelect
|
|
1540
|
+
v-model="statusFilter"
|
|
1541
|
+
:items="statusOptions"
|
|
1542
|
+
class="w-40"
|
|
1543
|
+
@update:model-value="fetchEntries"
|
|
1544
|
+
/>
|
|
1545
|
+
</div>
|
|
1546
|
+
</template>
|
|
1547
|
+
|
|
1548
|
+
<template #right>
|
|
1549
|
+
<div class="flex items-center gap-2">
|
|
1550
|
+
<UButton
|
|
1551
|
+
v-if="!isFileLocked"
|
|
1552
|
+
color="primary"
|
|
1553
|
+
icon="i-lucide-plus"
|
|
1554
|
+
size="sm"
|
|
1555
|
+
@click="createNewEntry"
|
|
1556
|
+
>
|
|
1557
|
+
New Entry
|
|
1558
|
+
</UButton>
|
|
1559
|
+
<UBadge v-else color="warning" variant="subtle" class="gap-1.5 py-1 px-2.5">
|
|
1560
|
+
🔒 File-locked — read-only
|
|
1561
|
+
</UBadge>
|
|
1562
|
+
</div>
|
|
1563
|
+
</template>
|
|
1564
|
+
</UDashboardToolbar>
|
|
1565
|
+
</template>
|
|
1566
|
+
|
|
1567
|
+
<template #body>
|
|
1568
|
+
<!-- 0. DASHBOARD OVERVIEW (When no collection is selected in URL) -->
|
|
1569
|
+
<div v-if="currentView === 'list' && !selectedCollectionId">
|
|
1570
|
+
<!-- Top Row: Metric Cards (flex-1) + Quick Actions sidebar -->
|
|
1571
|
+
<div class="flex flex-col md:flex-row gap-6 items-start">
|
|
1572
|
+
<!-- Left: Metric Cards -->
|
|
1573
|
+
<div class="flex-1 min-w-0">
|
|
1574
|
+
<ProseCardGroup>
|
|
1575
|
+
<ProseCard
|
|
1576
|
+
title="Total Collections"
|
|
1577
|
+
:description="collections.length"
|
|
1578
|
+
icon="i-lucide-folder"
|
|
1579
|
+
color="primary"
|
|
1580
|
+
to="https://nuxt.lemonsqueezy.com"
|
|
1581
|
+
target="_blank"
|
|
1582
|
+
/>
|
|
1583
|
+
<ProseCard
|
|
1584
|
+
title="Media Storage"
|
|
1585
|
+
description="Cloudflare R2"
|
|
1586
|
+
icon="i-lucide-image"
|
|
1587
|
+
color="primary"
|
|
1588
|
+
to="https://nuxt.lemonsqueezy.com"
|
|
1589
|
+
target="_blank"
|
|
1590
|
+
/>
|
|
1591
|
+
<ProseCard
|
|
1592
|
+
title="CMS Mode"
|
|
1593
|
+
description="Edge + Local DB"
|
|
1594
|
+
icon="i-lucide-database"
|
|
1595
|
+
color="primary"
|
|
1596
|
+
to="https://nuxt.lemonsqueezy.com"
|
|
1597
|
+
target="_blank"
|
|
1598
|
+
/>
|
|
1599
|
+
<ProseCard
|
|
1600
|
+
title="Deploy Trigger"
|
|
1601
|
+
:description="hasDeployTrigger ? 'Active' : 'Not configured'"
|
|
1602
|
+
icon="i-lucide-rocket"
|
|
1603
|
+
color="primary"
|
|
1604
|
+
to="https://nuxt.lemonsqueezy.com"
|
|
1605
|
+
target="_blank"
|
|
1606
|
+
/>
|
|
1607
|
+
</ProseCardGroup>
|
|
1608
|
+
</div>
|
|
1609
|
+
|
|
1610
|
+
<!-- Right: Quick Actions Box List -->
|
|
1611
|
+
<div class="w-full md:w-64 lg:w-72 shrink-0 space-y-4">
|
|
1612
|
+
<UCard>
|
|
1613
|
+
<template #header>
|
|
1614
|
+
<div class="flex items-center justify-between">
|
|
1615
|
+
<span class="text-xs font-semibold text-white uppercase tracking-wider"
|
|
1616
|
+
>Quick Actions</span
|
|
1617
|
+
>
|
|
1618
|
+
<UIcon name="i-lucide-zap" class="w-3.5 h-3.5 text-primary" />
|
|
1619
|
+
</div>
|
|
1620
|
+
</template>
|
|
1621
|
+
|
|
1622
|
+
<UButton
|
|
1623
|
+
size="sm"
|
|
1624
|
+
color="neutral"
|
|
1625
|
+
variant="ghost"
|
|
1626
|
+
icon="i-lucide-image"
|
|
1627
|
+
block
|
|
1628
|
+
class="justify-start text-xs py-2"
|
|
1629
|
+
@click="openMediaLibrary = true"
|
|
1630
|
+
>
|
|
1631
|
+
<div class="text-left flex-1 min-w-0">
|
|
1632
|
+
<p class="font-medium text-white truncate">Media Library</p>
|
|
1633
|
+
<p class="text-[10px] text-muted truncate">Upload & browse files</p>
|
|
1634
|
+
</div>
|
|
1635
|
+
</UButton>
|
|
1636
|
+
|
|
1637
|
+
<UButton
|
|
1638
|
+
size="sm"
|
|
1639
|
+
color="neutral"
|
|
1640
|
+
variant="ghost"
|
|
1641
|
+
icon="i-lucide-refresh-cw"
|
|
1642
|
+
block
|
|
1643
|
+
class="justify-start text-xs py-2"
|
|
1644
|
+
:loading="syncing"
|
|
1645
|
+
@click="triggerSync"
|
|
1646
|
+
>
|
|
1647
|
+
<div class="text-left flex-1 min-w-0">
|
|
1648
|
+
<p class="font-medium text-white truncate">Sync Files</p>
|
|
1649
|
+
<p class="text-[10px] text-muted truncate">Sync markdown content</p>
|
|
1650
|
+
</div>
|
|
1651
|
+
</UButton>
|
|
1652
|
+
|
|
1653
|
+
<UButton
|
|
1654
|
+
size="sm"
|
|
1655
|
+
color="neutral"
|
|
1656
|
+
variant="ghost"
|
|
1657
|
+
icon="i-lucide-download"
|
|
1658
|
+
block
|
|
1659
|
+
class="justify-start text-xs py-2"
|
|
1660
|
+
:loading="exporting"
|
|
1661
|
+
@click="triggerExport"
|
|
1662
|
+
>
|
|
1663
|
+
<div class="text-left flex-1 min-w-0">
|
|
1664
|
+
<p class="font-medium text-white truncate">Export Content</p>
|
|
1665
|
+
<p class="text-[10px] text-muted truncate">Download backup archive</p>
|
|
1666
|
+
</div>
|
|
1667
|
+
</UButton>
|
|
1668
|
+
|
|
1669
|
+
<UButton
|
|
1670
|
+
v-if="hasDeployTrigger"
|
|
1671
|
+
size="sm"
|
|
1672
|
+
color="neutral"
|
|
1673
|
+
variant="ghost"
|
|
1674
|
+
icon="i-lucide-rocket"
|
|
1675
|
+
block
|
|
1676
|
+
class="justify-start text-xs py-2"
|
|
1677
|
+
:loading="deploying"
|
|
1678
|
+
@click="triggerDeploy"
|
|
1679
|
+
>
|
|
1680
|
+
<div class="text-left flex-1 min-w-0">
|
|
1681
|
+
<p class="font-medium text-white truncate">Deploy Site</p>
|
|
1682
|
+
<p class="text-[10px] text-muted truncate">Trigger remote build</p>
|
|
1683
|
+
</div>
|
|
1684
|
+
</UButton>
|
|
1685
|
+
</UCard>
|
|
1686
|
+
</div>
|
|
1687
|
+
</div>
|
|
1688
|
+
|
|
1689
|
+
<!-- Collections Section (full width below) -->
|
|
1690
|
+
<div class="space-y-4">
|
|
1691
|
+
<UDashboardToolbar>
|
|
1692
|
+
<div>
|
|
1693
|
+
<h3 class="font-semibold text-sm text-white">Content Collections</h3>
|
|
1694
|
+
<p class="text-xs text-muted">Choose a collection to manage or create entries</p>
|
|
1695
|
+
</div>
|
|
1696
|
+
<UButton
|
|
1697
|
+
label="New Collection"
|
|
1698
|
+
icon="i-lucide-plus"
|
|
1699
|
+
@click="showNewCollectionModal = true"
|
|
1700
|
+
/>
|
|
1701
|
+
</UDashboardToolbar>
|
|
1702
|
+
<UBlogPosts class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
1703
|
+
<UBlogPost
|
|
1704
|
+
v-for="col in collections"
|
|
1705
|
+
:key="col.id"
|
|
1706
|
+
:description="col.description || 'No description provided'"
|
|
1707
|
+
:title="col.name"
|
|
1708
|
+
:badge="{
|
|
1709
|
+
label: `${col.source || 'db'} | ${col.mode === 'file-lock' ? '🔒 File-locked' : col.mode === 'git-sync' ? '🌿 Git-sync' : '💾 Database'}`,
|
|
1710
|
+
}"
|
|
1711
|
+
:to="{ query: { ...route.query, collection: col.id } }"
|
|
1712
|
+
/>
|
|
1713
|
+
</UBlogPosts>
|
|
1714
|
+
</div>
|
|
1715
|
+
</div>
|
|
1716
|
+
|
|
1717
|
+
<!-- 1. ENTRIES LIST VIEW (When a collection is selected in URL) -->
|
|
1718
|
+
<div v-else-if="currentView === 'list' && selectedCollectionId">
|
|
1719
|
+
<div class="flex-1 overflow-auto p-4 min-w-0">
|
|
1720
|
+
<UTable :data="entries" :columns="tableColumns" :loading="loading" class="w-full">
|
|
1721
|
+
<template #empty>
|
|
1722
|
+
<div class="text-center py-12 text-slate-500 text-sm">
|
|
1723
|
+
No entries found. Click "+ New Entry" to create one.
|
|
1724
|
+
</div>
|
|
1725
|
+
</template>
|
|
1726
|
+
|
|
1727
|
+
<template #title-cell="{ row }">
|
|
1728
|
+
<span
|
|
1729
|
+
class="font-medium text-white cursor-pointer hover:text-indigo-400"
|
|
1730
|
+
@click="editEntry(row.original)"
|
|
1731
|
+
>
|
|
1732
|
+
{{ row.original.title }}
|
|
1733
|
+
</span>
|
|
1734
|
+
</template>
|
|
1735
|
+
|
|
1736
|
+
<template #slug-cell="{ row }">
|
|
1737
|
+
<span class="font-mono text-xs text-slate-400">
|
|
1738
|
+
{{ row.original.slug }}
|
|
1739
|
+
</span>
|
|
1740
|
+
</template>
|
|
1741
|
+
|
|
1742
|
+
<template #status-cell="{ row }">
|
|
1743
|
+
<UBadge
|
|
1744
|
+
size="xs"
|
|
1745
|
+
variant="subtle"
|
|
1746
|
+
:color="getStatusBadgeColor(row.original.status)"
|
|
1747
|
+
>
|
|
1748
|
+
{{ formatStatus(row.original.status) }}
|
|
1749
|
+
</UBadge>
|
|
1750
|
+
</template>
|
|
1751
|
+
|
|
1752
|
+
<template #updatedAt-cell="{ row }">
|
|
1753
|
+
<span class="text-xs text-slate-500">
|
|
1754
|
+
{{ formatDate(row.original.updatedAt) }}
|
|
1755
|
+
</span>
|
|
1756
|
+
</template>
|
|
1757
|
+
|
|
1758
|
+
<template #actions-cell="{ row }">
|
|
1759
|
+
<div class="flex items-center justify-end space-x-2">
|
|
1760
|
+
<UButton
|
|
1761
|
+
size="xs"
|
|
1762
|
+
color="neutral"
|
|
1763
|
+
variant="ghost"
|
|
1764
|
+
@click="editEntry(row.original)"
|
|
1765
|
+
>
|
|
1766
|
+
Edit
|
|
1767
|
+
</UButton>
|
|
1768
|
+
<UButton
|
|
1769
|
+
v-if="!isFileLocked"
|
|
1770
|
+
:label="row.original.status === 'published' ? 'Unpublish' : 'Publish'"
|
|
1771
|
+
size="xs"
|
|
1772
|
+
:color="row.original.status === 'published' ? 'warning' : 'success'"
|
|
1773
|
+
variant="ghost"
|
|
1774
|
+
@click="togglePublish(row.original)"
|
|
1775
|
+
/>
|
|
1776
|
+
<UButton
|
|
1777
|
+
v-if="!isFileLocked"
|
|
1778
|
+
label="Delete"
|
|
1779
|
+
size="xs"
|
|
1780
|
+
color="error"
|
|
1781
|
+
variant="ghost"
|
|
1782
|
+
@click="deleteEntry(row.original.id)"
|
|
1783
|
+
/>
|
|
1784
|
+
</div>
|
|
1785
|
+
</template>
|
|
1786
|
+
</UTable>
|
|
1787
|
+
</div>
|
|
1788
|
+
</div>
|
|
1789
|
+
|
|
1790
|
+
<!-- 2. EDITOR VIEW -->
|
|
1791
|
+
<div v-else-if="currentView === 'editor'">
|
|
1792
|
+
<!-- Main UEditor Column -->
|
|
1793
|
+
<div class="flex-1 flex flex-col overflow-hidden min-w-0">
|
|
1794
|
+
<UEditor
|
|
1795
|
+
v-model="activeEntry.contentMd"
|
|
1796
|
+
content-type="markdown"
|
|
1797
|
+
placeholder="Start writing your markdown content here..."
|
|
1798
|
+
class="flex-1 flex flex-col overflow-hidden w-full min-w-0"
|
|
1799
|
+
:ui="{
|
|
1800
|
+
root: 'flex-1 flex flex-col overflow-hidden',
|
|
1801
|
+
content: 'flex-1 overflow-y-auto p-4 sm:p-6 min-w-0',
|
|
1802
|
+
}"
|
|
1803
|
+
>
|
|
1804
|
+
<template #default="{ editor }">
|
|
1805
|
+
<div
|
|
1806
|
+
class="border-b border-default bg-default/80 px-3 py-1.5 flex items-center justify-between shrink-0 flex-wrap gap-2"
|
|
1807
|
+
>
|
|
1808
|
+
<UEditorToolbar :editor="editor" :items="editorToolbarItems" />
|
|
1809
|
+
<div class="flex items-center gap-2">
|
|
1810
|
+
<UButton
|
|
1811
|
+
size="xs"
|
|
1812
|
+
:color="showPreview ? 'primary' : 'neutral'"
|
|
1813
|
+
:variant="showPreview ? 'subtle' : 'ghost'"
|
|
1814
|
+
icon="i-lucide-eye"
|
|
1815
|
+
@click="showPreview = !showPreview"
|
|
1816
|
+
>
|
|
1817
|
+
{{ showPreview ? "Hide Preview" : "Preview" }}
|
|
1818
|
+
</UButton>
|
|
1819
|
+
</div>
|
|
1820
|
+
</div>
|
|
1821
|
+
</template>
|
|
1822
|
+
</UEditor>
|
|
1823
|
+
</div>
|
|
1824
|
+
|
|
1825
|
+
<!-- Optional Live Preview Pane (Toggleable) -->
|
|
1826
|
+
<div
|
|
1827
|
+
v-if="showPreview"
|
|
1828
|
+
class="w-1/2 border-l border-default flex flex-col overflow-hidden bg-muted/20 shrink-0"
|
|
1829
|
+
>
|
|
1830
|
+
<!-- Header Bar -->
|
|
1831
|
+
<div
|
|
1832
|
+
class="h-10 bg-muted/40 border-b border-default px-3 flex items-center justify-between text-xs shrink-0 gap-2"
|
|
1833
|
+
>
|
|
1834
|
+
<div class="flex items-center gap-1.5">
|
|
1835
|
+
<div
|
|
1836
|
+
class="inline-flex rounded-md p-0.5 bg-muted/60 border border-default text-[11px]"
|
|
1837
|
+
>
|
|
1838
|
+
<button
|
|
1839
|
+
type="button"
|
|
1840
|
+
class="px-2 py-0.5 rounded font-medium transition-colors"
|
|
1841
|
+
:class="
|
|
1842
|
+
previewMode === 'doc'
|
|
1843
|
+
? 'bg-background text-foreground shadow-xs'
|
|
1844
|
+
: 'text-muted hover:text-foreground'
|
|
1845
|
+
"
|
|
1846
|
+
@click="previewMode = 'doc'"
|
|
1847
|
+
>
|
|
1848
|
+
Document
|
|
1849
|
+
</button>
|
|
1850
|
+
<button
|
|
1851
|
+
type="button"
|
|
1852
|
+
class="px-2 py-0.5 rounded font-medium transition-colors"
|
|
1853
|
+
:class="
|
|
1854
|
+
previewMode === 'page'
|
|
1855
|
+
? 'bg-background text-foreground shadow-xs'
|
|
1856
|
+
: 'text-muted hover:text-foreground'
|
|
1857
|
+
"
|
|
1858
|
+
@click="previewMode = 'page'"
|
|
1859
|
+
>
|
|
1860
|
+
Live Page
|
|
1861
|
+
</button>
|
|
1862
|
+
</div>
|
|
1863
|
+
|
|
1864
|
+
<!-- Device switcher for iframe -->
|
|
1865
|
+
<div v-if="previewMode === 'page'" class="flex items-center gap-0.5 ml-2">
|
|
1866
|
+
<UButton
|
|
1867
|
+
size="xs"
|
|
1868
|
+
:color="previewDevice === 'desktop' ? 'primary' : 'neutral'"
|
|
1869
|
+
:variant="previewDevice === 'desktop' ? 'subtle' : 'ghost'"
|
|
1870
|
+
icon="i-lucide-monitor"
|
|
1871
|
+
title="Desktop (100%)"
|
|
1872
|
+
@click="previewDevice = 'desktop'"
|
|
1873
|
+
/>
|
|
1874
|
+
<UButton
|
|
1875
|
+
size="xs"
|
|
1876
|
+
:color="previewDevice === 'tablet' ? 'primary' : 'neutral'"
|
|
1877
|
+
:variant="previewDevice === 'tablet' ? 'subtle' : 'ghost'"
|
|
1878
|
+
icon="i-lucide-tablet"
|
|
1879
|
+
title="Tablet (768px)"
|
|
1880
|
+
@click="previewDevice = 'tablet'"
|
|
1881
|
+
/>
|
|
1882
|
+
<UButton
|
|
1883
|
+
size="xs"
|
|
1884
|
+
:color="previewDevice === 'mobile' ? 'primary' : 'neutral'"
|
|
1885
|
+
:variant="previewDevice === 'mobile' ? 'subtle' : 'ghost'"
|
|
1886
|
+
icon="i-lucide-smartphone"
|
|
1887
|
+
title="Mobile (375px)"
|
|
1888
|
+
@click="previewDevice = 'mobile'"
|
|
1889
|
+
/>
|
|
1890
|
+
</div>
|
|
1891
|
+
</div>
|
|
1892
|
+
|
|
1893
|
+
<div class="flex items-center gap-1">
|
|
1894
|
+
<UButton
|
|
1895
|
+
v-if="previewMode === 'page'"
|
|
1896
|
+
size="xs"
|
|
1897
|
+
color="neutral"
|
|
1898
|
+
variant="ghost"
|
|
1899
|
+
icon="i-lucide-refresh-cw"
|
|
1900
|
+
:loading="previewLoading"
|
|
1901
|
+
title="Reload preview"
|
|
1902
|
+
@click="loadPagePreview"
|
|
1903
|
+
/>
|
|
1904
|
+
<UButton
|
|
1905
|
+
size="xs"
|
|
1906
|
+
:color="previewCopied ? 'success' : 'neutral'"
|
|
1907
|
+
:variant="previewCopied ? 'subtle' : 'ghost'"
|
|
1908
|
+
:icon="previewCopied ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
1909
|
+
title="Copy preview link"
|
|
1910
|
+
@click="copyPreviewLink"
|
|
1911
|
+
>
|
|
1912
|
+
{{ previewCopied ? "Copied!" : "Share" }}
|
|
1913
|
+
</UButton>
|
|
1914
|
+
<UButton
|
|
1915
|
+
size="xs"
|
|
1916
|
+
color="neutral"
|
|
1917
|
+
variant="ghost"
|
|
1918
|
+
icon="i-lucide-external-link"
|
|
1919
|
+
title="Open preview in new tab"
|
|
1920
|
+
@click="openPreviewInNewTab"
|
|
1921
|
+
/>
|
|
1922
|
+
<UButton
|
|
1923
|
+
size="xs"
|
|
1924
|
+
color="neutral"
|
|
1925
|
+
variant="ghost"
|
|
1926
|
+
icon="i-lucide-x"
|
|
1927
|
+
@click="showPreview = false"
|
|
1928
|
+
/>
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
|
|
1932
|
+
<!-- Preview Body: Document Mode -->
|
|
1933
|
+
<div v-if="previewMode === 'doc'" class="flex-1 p-6 overflow-y-auto space-y-4">
|
|
1934
|
+
<!-- Cover image header if present -->
|
|
1935
|
+
<div
|
|
1936
|
+
v-if="activeEntry.coverImage"
|
|
1937
|
+
class="rounded-lg overflow-hidden border border-default bg-muted max-h-56"
|
|
1938
|
+
>
|
|
1939
|
+
<img
|
|
1940
|
+
:src="activeEntry.coverImage"
|
|
1941
|
+
class="w-full h-full object-cover"
|
|
1942
|
+
alt="Cover preview"
|
|
1943
|
+
/>
|
|
1944
|
+
</div>
|
|
1945
|
+
|
|
1946
|
+
<!-- Header info -->
|
|
1947
|
+
<div class="space-y-1">
|
|
1948
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
1949
|
+
<UBadge
|
|
1950
|
+
size="xs"
|
|
1951
|
+
variant="subtle"
|
|
1952
|
+
:color="getStatusBadgeColor(activeEntry.status)"
|
|
1953
|
+
>
|
|
1954
|
+
{{ formatStatus(activeEntry.status) }}
|
|
1955
|
+
</UBadge>
|
|
1956
|
+
<span class="text-xs text-muted font-mono"
|
|
1957
|
+
>/{{ selectedCollectionId }}/{{ activeEntry.slug }}</span
|
|
1958
|
+
>
|
|
1959
|
+
<span v-if="activeEntry.tags" class="text-xs text-muted"
|
|
1960
|
+
>#{{ activeEntry.tags }}</span
|
|
1961
|
+
>
|
|
1962
|
+
</div>
|
|
1963
|
+
<h1 class="text-2xl font-bold tracking-tight text-foreground">
|
|
1964
|
+
{{ activeEntry.title || "Untitled Entry" }}
|
|
1965
|
+
</h1>
|
|
1966
|
+
<p v-if="activeEntry.description" class="text-sm text-muted">
|
|
1967
|
+
{{ activeEntry.description }}
|
|
1968
|
+
</p>
|
|
1969
|
+
</div>
|
|
1970
|
+
|
|
1971
|
+
<hr class="border-default" />
|
|
1972
|
+
|
|
1973
|
+
<!-- Rendered Markdown Content -->
|
|
1974
|
+
<ContentRenderer
|
|
1975
|
+
:value="activeEntry.contentMd"
|
|
1976
|
+
class="prose dark:prose-invert max-w-none text-sm"
|
|
1977
|
+
/>
|
|
1978
|
+
</div>
|
|
1979
|
+
|
|
1980
|
+
<!-- Preview Body: Page Mode (Iframe) -->
|
|
1981
|
+
<div
|
|
1982
|
+
v-else
|
|
1983
|
+
class="flex-1 flex flex-col items-center justify-center overflow-hidden bg-muted/30 p-2 sm:p-4"
|
|
1984
|
+
>
|
|
1985
|
+
<div
|
|
1986
|
+
class="h-full flex flex-col bg-background rounded-lg border border-default shadow-md overflow-hidden transition-all duration-200"
|
|
1987
|
+
:class="{
|
|
1988
|
+
'w-full': previewDevice === 'desktop',
|
|
1989
|
+
'w-[768px] max-w-full': previewDevice === 'tablet',
|
|
1990
|
+
'w-[375px] max-w-full': previewDevice === 'mobile',
|
|
1991
|
+
}"
|
|
1992
|
+
>
|
|
1993
|
+
<!-- Simulated browser mini bar -->
|
|
1994
|
+
<div
|
|
1995
|
+
class="h-7 bg-muted/60 border-b border-default px-2.5 flex items-center justify-between text-[11px] text-muted shrink-0"
|
|
1996
|
+
>
|
|
1997
|
+
<div class="flex items-center gap-1.5">
|
|
1998
|
+
<span class="w-2 h-2 rounded-full bg-red-400 inline-block"></span>
|
|
1999
|
+
<span class="w-2 h-2 rounded-full bg-amber-400 inline-block"></span>
|
|
2000
|
+
<span class="w-2 h-2 rounded-full bg-emerald-400 inline-block"></span>
|
|
2001
|
+
</div>
|
|
2002
|
+
<span class="font-mono text-[10px] text-muted truncate max-w-[260px]">{{
|
|
2003
|
+
previewIframeUrl || "Loading preview..."
|
|
2004
|
+
}}</span>
|
|
2005
|
+
<span class="text-[10px] text-muted capitalize">{{ previewDevice }}</span>
|
|
2006
|
+
</div>
|
|
2007
|
+
|
|
2008
|
+
<div
|
|
2009
|
+
v-if="previewLoading"
|
|
2010
|
+
class="flex-1 flex items-center justify-center p-6 text-xs text-muted"
|
|
2011
|
+
>
|
|
2012
|
+
<UIcon name="i-lucide-loader-2" class="w-5 h-5 animate-spin mr-2" />
|
|
2013
|
+
Generating preview token & loading live page...
|
|
2014
|
+
</div>
|
|
2015
|
+
<iframe
|
|
2016
|
+
v-else-if="previewIframeUrl"
|
|
2017
|
+
:src="previewIframeUrl"
|
|
2018
|
+
class="flex-1 w-full h-full border-0 bg-background"
|
|
2019
|
+
/>
|
|
2020
|
+
<div
|
|
2021
|
+
v-else
|
|
2022
|
+
class="flex-1 flex flex-col items-center justify-center p-6 text-center text-xs text-muted space-y-2"
|
|
2023
|
+
>
|
|
2024
|
+
<UIcon name="i-lucide-file-question" class="w-8 h-8 text-muted" />
|
|
2025
|
+
<p>Save entry first or click reload to generate live preview.</p>
|
|
2026
|
+
<UButton
|
|
2027
|
+
size="xs"
|
|
2028
|
+
color="primary"
|
|
2029
|
+
label="Generate Preview"
|
|
2030
|
+
@click="loadPagePreview"
|
|
2031
|
+
/>
|
|
2032
|
+
</div>
|
|
2033
|
+
</div>
|
|
2034
|
+
</div>
|
|
2035
|
+
</div>
|
|
2036
|
+
</div>
|
|
2037
|
+
</template>
|
|
2038
|
+
</UDashboardPanel>
|
|
2039
|
+
|
|
2040
|
+
<!-- Modals (outside panel, inside group) -->
|
|
2041
|
+
<UModal
|
|
2042
|
+
v-model:open="openMediaLibrary"
|
|
2043
|
+
title="Media Library (Cloudflare R2)"
|
|
2044
|
+
description="Upload, browse, and manage images stored in Cloudflare R2"
|
|
2045
|
+
:ui="{ content: 'sm:max-w-3xl' }"
|
|
2046
|
+
>
|
|
2047
|
+
<template #body>
|
|
2048
|
+
<div class="space-y-4">
|
|
2049
|
+
<!-- Toolbar: Search + Refresh + Upload Trigger -->
|
|
2050
|
+
<div class="flex items-center justify-between gap-2 flex-wrap">
|
|
2051
|
+
<div class="flex items-center gap-2 flex-1 min-w-[200px]">
|
|
2052
|
+
<UInput
|
|
2053
|
+
v-model="mediaSearchQuery"
|
|
2054
|
+
placeholder="Search images in R2..."
|
|
2055
|
+
icon="i-lucide-search"
|
|
2056
|
+
size="xs"
|
|
2057
|
+
class="w-full max-w-xs"
|
|
2058
|
+
/>
|
|
2059
|
+
<UButton
|
|
2060
|
+
size="xs"
|
|
2061
|
+
color="neutral"
|
|
2062
|
+
variant="ghost"
|
|
2063
|
+
icon="i-lucide-refresh-cw"
|
|
2064
|
+
:loading="mediaLoading"
|
|
2065
|
+
title="Refresh media files"
|
|
2066
|
+
@click="fetchMediaFiles"
|
|
2067
|
+
/>
|
|
2068
|
+
<span class="text-xs text-muted">
|
|
2069
|
+
{{ filteredMediaItems.length }} file{{ filteredMediaItems.length === 1 ? "" : "s" }}
|
|
2070
|
+
</span>
|
|
2071
|
+
</div>
|
|
2072
|
+
|
|
2073
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
2074
|
+
<input
|
|
2075
|
+
type="file"
|
|
2076
|
+
class="hidden"
|
|
2077
|
+
id="cms-file-upload"
|
|
2078
|
+
accept="image/*"
|
|
2079
|
+
@change="handleFileUpload"
|
|
2080
|
+
/>
|
|
2081
|
+
<label
|
|
2082
|
+
for="cms-file-upload"
|
|
2083
|
+
class="cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs bg-primary hover:bg-primary/90 text-white font-medium shadow-xs transition-colors"
|
|
2084
|
+
>
|
|
2085
|
+
<UIcon name="i-lucide-upload" class="w-3.5 h-3.5" />
|
|
2086
|
+
Upload Image
|
|
2087
|
+
</label>
|
|
2088
|
+
</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
|
|
2091
|
+
<!-- Upload Progress indicator -->
|
|
2092
|
+
<div
|
|
2093
|
+
v-if="uploadProgress > 0"
|
|
2094
|
+
class="p-2.5 rounded-md bg-primary/10 border border-primary/20 flex items-center justify-between text-xs text-primary"
|
|
2095
|
+
>
|
|
2096
|
+
<span>Uploading image to Cloudflare R2...</span>
|
|
2097
|
+
<span class="font-mono font-medium">{{ uploadProgress }}%</span>
|
|
2098
|
+
</div>
|
|
2099
|
+
|
|
2100
|
+
<!-- Image Grid -->
|
|
2101
|
+
<div
|
|
2102
|
+
v-if="mediaLoading && mediaItems.length === 0"
|
|
2103
|
+
class="py-12 flex flex-col items-center justify-center text-xs text-muted"
|
|
2104
|
+
>
|
|
2105
|
+
<UIcon name="i-lucide-loader-2" class="w-6 h-6 animate-spin mb-2" />
|
|
2106
|
+
Loading images from Cloudflare R2...
|
|
2107
|
+
</div>
|
|
2108
|
+
|
|
2109
|
+
<div
|
|
2110
|
+
v-else-if="filteredMediaItems.length === 0"
|
|
2111
|
+
class="py-12 text-center text-xs text-muted border border-dashed border-default rounded-lg bg-muted/10"
|
|
2112
|
+
>
|
|
2113
|
+
<UIcon name="i-lucide-image" class="w-8 h-8 text-muted mx-auto mb-2" />
|
|
2114
|
+
<p v-if="mediaSearchQuery">No images match your search "{{ mediaSearchQuery }}".</p>
|
|
2115
|
+
<p v-else>No images found in Cloudflare R2 storage. Upload an image to get started.</p>
|
|
2116
|
+
</div>
|
|
2117
|
+
|
|
2118
|
+
<div
|
|
2119
|
+
v-else
|
|
2120
|
+
class="max-h-[380px] overflow-y-auto grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 p-1"
|
|
2121
|
+
>
|
|
2122
|
+
<div
|
|
2123
|
+
v-for="item in filteredMediaItems"
|
|
2124
|
+
:key="item.key || item.url"
|
|
2125
|
+
class="group relative flex flex-col bg-muted/20 hover:bg-muted/40 rounded-lg border border-default overflow-hidden transition-all duration-150"
|
|
2126
|
+
>
|
|
2127
|
+
<!-- Thumbnail -->
|
|
2128
|
+
<div
|
|
2129
|
+
class="aspect-square bg-muted/60 relative overflow-hidden flex items-center justify-center"
|
|
2130
|
+
>
|
|
2131
|
+
<img
|
|
2132
|
+
:src="item.url"
|
|
2133
|
+
:alt="item.pathname || item.key"
|
|
2134
|
+
class="w-full h-full object-cover transition-transform group-hover:scale-105 duration-200"
|
|
2135
|
+
loading="lazy"
|
|
2136
|
+
/>
|
|
2137
|
+
<!-- Quick Action Overlay on hover -->
|
|
2138
|
+
<div
|
|
2139
|
+
class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1.5 p-2"
|
|
2140
|
+
>
|
|
2141
|
+
<UButton
|
|
2142
|
+
size="xs"
|
|
2143
|
+
color="primary"
|
|
2144
|
+
label="Select"
|
|
2145
|
+
@click="selectMediaItem(item.url)"
|
|
2146
|
+
/>
|
|
2147
|
+
<UButton
|
|
2148
|
+
size="xs"
|
|
2149
|
+
color="neutral"
|
|
2150
|
+
variant="subtle"
|
|
2151
|
+
icon="i-lucide-trash-2"
|
|
2152
|
+
title="Delete from R2"
|
|
2153
|
+
@click="deleteMediaItem(item)"
|
|
2154
|
+
/>
|
|
2155
|
+
</div>
|
|
2156
|
+
</div>
|
|
2157
|
+
|
|
2158
|
+
<!-- Metadata footer -->
|
|
2159
|
+
<div class="p-2 flex flex-col gap-1 min-w-0">
|
|
2160
|
+
<p
|
|
2161
|
+
class="text-[11px] font-medium text-foreground truncate"
|
|
2162
|
+
:title="item.pathname || item.key"
|
|
2163
|
+
>
|
|
2164
|
+
{{ (item.pathname || item.key || "").split("/").pop() }}
|
|
2165
|
+
</p>
|
|
2166
|
+
<div class="flex items-center justify-between text-[10px] text-muted">
|
|
2167
|
+
<span>{{ formatBytes(item.size) }}</span>
|
|
2168
|
+
<div class="flex items-center gap-1">
|
|
2169
|
+
<button
|
|
2170
|
+
type="button"
|
|
2171
|
+
class="hover:text-foreground text-[10px]"
|
|
2172
|
+
:title="mediaCopiedUrl === item.url ? 'Copied!' : 'Copy URL'"
|
|
2173
|
+
@click.stop="copyMediaUrl(item.url)"
|
|
2174
|
+
>
|
|
2175
|
+
{{ mediaCopiedUrl === item.url ? "✓ Copied" : "Copy" }}
|
|
2176
|
+
</button>
|
|
2177
|
+
<span>·</span>
|
|
2178
|
+
<button
|
|
2179
|
+
type="button"
|
|
2180
|
+
class="hover:text-foreground text-[10px]"
|
|
2181
|
+
title="Insert Markdown"
|
|
2182
|
+
@click.stop="insertImageMarkdown(item.url)"
|
|
2183
|
+
>
|
|
2184
|
+
Insert
|
|
2185
|
+
</button>
|
|
2186
|
+
</div>
|
|
2187
|
+
</div>
|
|
2188
|
+
</div>
|
|
2189
|
+
</div>
|
|
2190
|
+
</div>
|
|
2191
|
+
</div>
|
|
2192
|
+
</template>
|
|
2193
|
+
|
|
2194
|
+
<template #footer>
|
|
2195
|
+
<div class="flex items-center justify-between w-full">
|
|
2196
|
+
<span class="text-xs text-muted">
|
|
2197
|
+
Directly connected to Cloudflare R2 bucket binding (<span class="font-mono">BLOB</span>)
|
|
2198
|
+
</span>
|
|
2199
|
+
<UButton
|
|
2200
|
+
label="Close"
|
|
2201
|
+
size="xs"
|
|
2202
|
+
color="neutral"
|
|
2203
|
+
variant="ghost"
|
|
2204
|
+
@click="openMediaLibrary = false"
|
|
2205
|
+
/>
|
|
2206
|
+
</div>
|
|
2207
|
+
</template>
|
|
2208
|
+
</UModal>
|
|
2209
|
+
|
|
2210
|
+
<!-- New Collection Modal -->
|
|
2211
|
+
<UModal
|
|
2212
|
+
v-model:open="showNewCollectionModal"
|
|
2213
|
+
title="Create New Collection"
|
|
2214
|
+
description="Add a new content collection to the CMS"
|
|
2215
|
+
>
|
|
2216
|
+
<template #body>
|
|
2217
|
+
<div class="space-y-3">
|
|
2218
|
+
<UFormField label="Collection ID" name="collectionId" required>
|
|
2219
|
+
<UInput v-model="newCol.id" placeholder="e.g. team-members" class="w-full" />
|
|
2220
|
+
</UFormField>
|
|
2221
|
+
<UFormField label="Name" name="name" required>
|
|
2222
|
+
<UInput v-model="newCol.name" placeholder="e.g. Team Members" class="w-full" />
|
|
2223
|
+
</UFormField>
|
|
2224
|
+
<UFormField label="Description" name="description">
|
|
2225
|
+
<UInput
|
|
2226
|
+
v-model="newCol.description"
|
|
2227
|
+
placeholder="Optional description"
|
|
2228
|
+
class="w-full"
|
|
2229
|
+
/>
|
|
2230
|
+
</UFormField>
|
|
2231
|
+
</div>
|
|
2232
|
+
</template>
|
|
2233
|
+
|
|
2234
|
+
<template #footer>
|
|
2235
|
+
<div class="flex justify-end space-x-2 w-full">
|
|
2236
|
+
<UButton
|
|
2237
|
+
size="xs"
|
|
2238
|
+
color="neutral"
|
|
2239
|
+
variant="ghost"
|
|
2240
|
+
@click="showNewCollectionModal = false"
|
|
2241
|
+
>
|
|
2242
|
+
Cancel
|
|
2243
|
+
</UButton>
|
|
2244
|
+
<UButton size="xs" color="primary" @click="createCollection"> Create </UButton>
|
|
2245
|
+
</div>
|
|
2246
|
+
</template>
|
|
2247
|
+
</UModal>
|
|
2248
|
+
</UDashboardGroup>
|
|
2249
|
+
</template>
|