@stacksjs/defaults 0.70.206 → 0.70.208
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/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- package/views/system-tray/index.stx +147 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
@extends('layouts/default')
|
|
2
|
+
|
|
3
|
+
@section('content')
|
|
4
|
+
@title('Dashboard - Campaigns')
|
|
5
|
+
<PageLayout>
|
|
6
|
+
<div class="sm:flex sm:items-center sm:justify-between">
|
|
7
|
+
<PageHeader title="Campaigns" description="Plan, launch, and monitor marketing campaigns across channels" />
|
|
8
|
+
<div class="mt-4 sm:mt-0">
|
|
9
|
+
<button @click="openNewCampaignModal()" class="inline-flex items-center px-3 py-2 font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md shadow-sm">
|
|
10
|
+
<div class="mr-2 h-5 w-5 i-hugeicons-plus-sign"></div>
|
|
11
|
+
New Campaign
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 mt-6">
|
|
17
|
+
<div class="flex overflow-hidden items-center p-5 bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
18
|
+
<div class="p-2 h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-md">
|
|
19
|
+
<div class="h-6 w-6 text-blue-600 dark:text-blue-300 i-hugeicons-rocket-01"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="ml-5">
|
|
22
|
+
<dt class="font-medium text-gray-500 text-sm dark:text-neutral-400">Total Campaigns</dt>
|
|
23
|
+
<dd class="font-medium text-gray-900 text-lg dark:text-white">{{ totalCampaigns() }}</dd>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="flex overflow-hidden items-center p-5 bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
27
|
+
<div class="p-2 h-10 w-10 bg-green-100 dark:bg-green-900 rounded-md">
|
|
28
|
+
<div class="h-6 w-6 text-green-600 dark:text-green-300 i-hugeicons-checkmark-circle-02"></div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="ml-5">
|
|
31
|
+
<dt class="font-medium text-gray-500 text-sm dark:text-neutral-400">Active</dt>
|
|
32
|
+
<dd class="font-medium text-gray-900 text-lg dark:text-white">{{ activeCount() }}</dd>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex overflow-hidden items-center p-5 bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
36
|
+
<div class="p-2 h-10 w-10 bg-blue-100 dark:bg-blue-900 rounded-md">
|
|
37
|
+
<div class="h-6 w-6 text-blue-600 dark:text-blue-300 i-hugeicons-clock-01"></div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="ml-5">
|
|
40
|
+
<dt class="font-medium text-gray-500 text-sm dark:text-neutral-400">Scheduled</dt>
|
|
41
|
+
<dd class="font-medium text-gray-900 text-lg dark:text-white">{{ scheduledCount() }}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="flex overflow-hidden items-center p-5 bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
45
|
+
<div class="p-2 h-10 w-10 bg-yellow-100 dark:bg-yellow-900 rounded-md">
|
|
46
|
+
<div class="h-6 w-6 text-yellow-600 dark:text-yellow-300 i-hugeicons-edit-01"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="ml-5">
|
|
49
|
+
<dt class="font-medium text-gray-500 text-sm dark:text-neutral-400">Drafts</dt>
|
|
50
|
+
<dd class="font-medium text-gray-900 text-lg dark:text-white">{{ draftCount() }}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="grid gap-5 grid-cols-1 lg:grid-cols-2 mt-6">
|
|
56
|
+
<div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
57
|
+
<div class="p-6">
|
|
58
|
+
<h3 class="font-medium text-base text-gray-900 dark:text-white">Goal Progress (Active)</h3>
|
|
59
|
+
<div class="mt-4 h-72"><canvas id="campaigns_goal_chart"></canvas></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow">
|
|
63
|
+
<div class="p-6">
|
|
64
|
+
<h3 class="font-medium text-base text-gray-900 dark:text-white">Budget vs Spent</h3>
|
|
65
|
+
<div class="mt-4 h-72"><canvas id="campaigns_budget_chart"></canvas></div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mt-8">
|
|
71
|
+
<div class="relative flex-1 max-w-sm">
|
|
72
|
+
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
|
|
73
|
+
<div class="h-5 w-5 text-gray-400 i-hugeicons-search-01"></div>
|
|
74
|
+
</div>
|
|
75
|
+
<input x-model="searchQuery" type="text" placeholder="Search campaigns..." class="block pl-10 pr-3 py-1.5 w-full text-gray-900 sm:leading-6 sm:text-sm dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600" />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex flex-col gap-4 sm:flex-row">
|
|
79
|
+
<select x-model="statusFilter" class="block pl-3 pr-10 py-1.5 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600">
|
|
80
|
+
<option value="all">All Statuses</option>
|
|
81
|
+
<template :for="status in campaignStatuses">
|
|
82
|
+
<option :value="status">{{ status }}</option>
|
|
83
|
+
</template>
|
|
84
|
+
</select>
|
|
85
|
+
<select x-model="typeFilter" class="block pl-3 pr-10 py-1.5 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600">
|
|
86
|
+
<option value="all">All Types</option>
|
|
87
|
+
<template :for="type in campaignTypes">
|
|
88
|
+
<option :value="type">{{ type }}</option>
|
|
89
|
+
</template>
|
|
90
|
+
</select>
|
|
91
|
+
<select x-model="sortBy" class="block pl-3 pr-10 py-1.5 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600">
|
|
92
|
+
<option value="startDate">Sort by Start Date</option>
|
|
93
|
+
<option value="name">Sort by Name</option>
|
|
94
|
+
<option value="budget">Sort by Budget</option>
|
|
95
|
+
<option value="goalProgress">Sort by Progress</option>
|
|
96
|
+
</select>
|
|
97
|
+
<select x-model="sortOrder" class="block pl-3 pr-10 py-1.5 text-gray-900 sm:leading-6 sm:text-sm dark:text-white dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600">
|
|
98
|
+
<option value="desc">Descending</option>
|
|
99
|
+
<option value="asc">Ascending</option>
|
|
100
|
+
</select>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 mt-6">
|
|
105
|
+
<template :for="campaign in paginatedCampaigns">
|
|
106
|
+
<div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700 shadow-sm hover:shadow-md transition-shadow">
|
|
107
|
+
<div class="p-6">
|
|
108
|
+
<div class="flex items-start justify-between">
|
|
109
|
+
<div>
|
|
110
|
+
<h3 class="font-medium text-base text-gray-900 dark:text-white">{{ campaign.name }}</h3>
|
|
111
|
+
<div class="flex items-center mt-1 space-x-2">
|
|
112
|
+
<span class="{{ typeBadgeClass(campaign.type) }}">{{ campaign.type }}</span>
|
|
113
|
+
<span class="{{ statusBadgeClass(campaign.status) }}">{{ campaign.status }}</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<button @click="openCampaignActions(campaign.id)" class="text-gray-400 hover:text-gray-600 dark:hover:text-white">
|
|
117
|
+
<div class="h-5 w-5 i-hugeicons-more-horizontal-circle-01"></div>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="flex items-center mt-4 text-gray-500 text-sm dark:text-neutral-400">
|
|
122
|
+
<img :src="campaign.ownerAvatar" :alt="campaign.owner" class="h-6 w-6 rounded-full" />
|
|
123
|
+
<span class="ml-2">{{ campaign.owner }}</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="grid gap-3 grid-cols-2 mt-4 text-sm">
|
|
127
|
+
<div>
|
|
128
|
+
<p class="text-gray-500 dark:text-neutral-400">Start</p>
|
|
129
|
+
<p class="font-medium text-gray-900 dark:text-white">{{ formatDate(campaign.startDate) }}</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div>
|
|
132
|
+
<p class="text-gray-500 dark:text-neutral-400">End</p>
|
|
133
|
+
<p class="font-medium text-gray-900 dark:text-white">{{ formatDate(campaign.endDate) }}</p>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<p class="text-gray-500 dark:text-neutral-400">Budget</p>
|
|
137
|
+
<p class="font-medium text-gray-900 dark:text-white">{{ formatCurrency(campaign.budget) }}</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<p class="text-gray-500 dark:text-neutral-400">Spent</p>
|
|
141
|
+
<p class="font-medium text-gray-900 dark:text-white">{{ formatCurrency(campaign.spent) }}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="mt-4">
|
|
146
|
+
<div class="flex items-center justify-between text-sm">
|
|
147
|
+
<span class="text-gray-500 dark:text-neutral-400">{{ campaign.goal }}</span>
|
|
148
|
+
<span class="font-medium text-gray-900 dark:text-white">{{ formatGoalProgress(campaign) }}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="overflow-hidden mt-1 h-2 bg-gray-200 dark:bg-neutral-700 rounded-full">
|
|
151
|
+
<div class="h-full bg-blue-500 rounded-full" style="width: {{ goalPercentage(campaign) }}%"></div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div :show="campaign.channels.length > 0" class="flex flex-wrap gap-1 mt-4">
|
|
156
|
+
<template :for="channel in campaign.channels">
|
|
157
|
+
<span class="inline-flex items-center px-2 py-0.5 font-medium text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded">{{ channel }}</span>
|
|
158
|
+
</template>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="flex justify-end mt-4 space-x-2">
|
|
162
|
+
<button @click="duplicateCampaign(campaign.id)" class="px-2.5 py-1.5 text-gray-700 text-sm dark:text-neutral-300 hover:bg-gray-50 dark:hover:bg-neutral-700 rounded-md" title="Duplicate">
|
|
163
|
+
<div class="h-4 w-4 i-hugeicons-copy"></div>
|
|
164
|
+
</button>
|
|
165
|
+
<button @click="openEditCampaignModal(campaign.id)" class="px-2.5 py-1.5 text-blue-600 text-sm dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-md" title="Edit">
|
|
166
|
+
<div class="h-4 w-4 i-hugeicons-edit-01"></div>
|
|
167
|
+
</button>
|
|
168
|
+
<button @click="deleteCampaign(campaign.id)" class="px-2.5 py-1.5 text-red-600 text-sm dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-md" title="Delete">
|
|
169
|
+
<div class="h-4 w-4 i-hugeicons-waste"></div>
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div :show="paginatedCampaigns().length === 0" class="py-12 text-center">
|
|
178
|
+
<div class="mx-auto h-12 w-12 text-gray-400 i-hugeicons-rocket-01"></div>
|
|
179
|
+
<h3 class="mt-2 font-semibold text-gray-900 text-sm dark:text-white">No campaigns found</h3>
|
|
180
|
+
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Try adjusting your filters or create a new campaign.</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div :show="totalPages() > 1" class="flex items-center justify-between mt-6">
|
|
184
|
+
<p class="text-gray-700 text-sm dark:text-neutral-300">Page {{ currentPage() }} of {{ totalPages() }}</p>
|
|
185
|
+
<nav class="inline-flex -space-x-px rounded-md shadow-sm isolate">
|
|
186
|
+
<button @click="prevPage()" :disabled="currentPage() === 1" class="inline-flex relative items-center px-2 py-2 text-gray-400 dark:text-neutral-500 hover:bg-gray-50 dark:hover:bg-neutral-700 ring-1 ring-gray-300 ring-inset rounded-l-md dark:ring-neutral-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
187
|
+
<div class="h-5 w-5 i-hugeicons-arrow-left-01"></div>
|
|
188
|
+
</button>
|
|
189
|
+
<button @click="nextPage()" :disabled="currentPage() === totalPages()" class="inline-flex relative items-center px-2 py-2 text-gray-400 dark:text-neutral-500 hover:bg-gray-50 dark:hover:bg-neutral-700 ring-1 ring-gray-300 ring-inset rounded-r-md dark:ring-neutral-600 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
190
|
+
<div class="h-5 w-5 i-hugeicons-arrow-right-01"></div>
|
|
191
|
+
</button>
|
|
192
|
+
</nav>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div :show="showCampaignModal()" @click.self="closeCampaignModal()" class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black bg-opacity-50">
|
|
196
|
+
<div class="overflow-y-auto p-6 max-h-[90vh] w-full sm:max-w-2xl bg-white dark:bg-neutral-800 rounded-lg shadow-xl">
|
|
197
|
+
<div class="flex items-start justify-between mb-4">
|
|
198
|
+
<h3 class="font-semibold text-gray-900 text-lg dark:text-white">{{ isEditMode() ? 'Edit Campaign' : 'New Campaign' }}</h3>
|
|
199
|
+
<button @click="closeCampaignModal()" class="text-gray-400 dark:text-neutral-300 hover:text-gray-600">
|
|
200
|
+
<i class="h-5 w-5 i-hugeicons-cancel-01"></i>
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="space-y-4">
|
|
205
|
+
<div>
|
|
206
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Name</label>
|
|
207
|
+
<input x-model="formName" type="text" placeholder="Campaign name" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
208
|
+
</div>
|
|
209
|
+
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
|
210
|
+
<div>
|
|
211
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Type</label>
|
|
212
|
+
<select x-model="formType" class="block mt-2 pl-3 pr-10 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600">
|
|
213
|
+
<template :for="type in campaignTypes">
|
|
214
|
+
<option :value="type">{{ type }}</option>
|
|
215
|
+
</template>
|
|
216
|
+
</select>
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Status</label>
|
|
220
|
+
<select x-model="formStatus" class="block mt-2 pl-3 pr-10 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600">
|
|
221
|
+
<template :for="status in campaignStatuses">
|
|
222
|
+
<option :value="status">{{ status }}</option>
|
|
223
|
+
</template>
|
|
224
|
+
</select>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
|
228
|
+
<div>
|
|
229
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Start Date</label>
|
|
230
|
+
<input x-model="formStartDate" type="date" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
231
|
+
</div>
|
|
232
|
+
<div>
|
|
233
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">End Date</label>
|
|
234
|
+
<input x-model="formEndDate" type="date" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
|
238
|
+
<div>
|
|
239
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Budget</label>
|
|
240
|
+
<input x-model="formBudget" type="number" min="0" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Goal</label>
|
|
244
|
+
<input x-model="formGoal" type="text" placeholder="Sales / Engagement / etc" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div>
|
|
248
|
+
<label class="block font-medium text-gray-900 text-sm dark:text-white">Audience</label>
|
|
249
|
+
<input x-model="formAudience" type="text" placeholder="e.g. All Customers" class="block mt-2 py-1.5 w-full text-gray-900 sm:text-sm dark:text-white dark:bg-neutral-700 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 focus:ring-2 focus:ring-blue-600 shadow-sm" />
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div class="flex justify-end mt-6 space-x-2">
|
|
254
|
+
<button @click="closeCampaignModal()" class="inline-flex justify-center px-3 py-2 font-semibold text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-700 hover:bg-gray-50 dark:hover:bg-neutral-600 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-600 shadow-sm">Cancel</button>
|
|
255
|
+
<button @click="saveCampaign()" :disabled="!formName()" class="inline-flex justify-center px-3 py-2 font-semibold text-sm text-white bg-blue-600 hover:bg-blue-500 rounded-md shadow-sm disabled:opacity-50">{{ isEditMode() ? 'Save' : 'Create' }}</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</PageLayout>
|
|
260
|
+
|
|
261
|
+
<script client>
|
|
262
|
+
// Filter options — moved out of the former `<script server>` block
|
|
263
|
+
// (stacksjs/stacks#1838).
|
|
264
|
+
const campaignTypes = ['Email', 'SMS', 'Seasonal', 'Discount', 'Product Launch', 'App Promotion']
|
|
265
|
+
const campaignStatuses = ['Active', 'Scheduled', 'Draft', 'Completed', 'Paused']
|
|
266
|
+
|
|
267
|
+
interface Campaign {
|
|
268
|
+
id: number
|
|
269
|
+
uuid: string
|
|
270
|
+
name: string
|
|
271
|
+
type: string
|
|
272
|
+
status: string
|
|
273
|
+
startDate: string
|
|
274
|
+
endDate: string | null
|
|
275
|
+
budget: number
|
|
276
|
+
spent: number
|
|
277
|
+
goal: string
|
|
278
|
+
goalTarget: number
|
|
279
|
+
goalProgress: number
|
|
280
|
+
audience: string
|
|
281
|
+
channels: string[]
|
|
282
|
+
owner: string
|
|
283
|
+
ownerAvatar: string
|
|
284
|
+
createdAt: string
|
|
285
|
+
lastModified: string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface ChartLike { destroy: () => void, update: () => void, data: { labels: string[], datasets: Array<{ label?: string, data: number[] }> } }
|
|
289
|
+
type ChartCtor = new (canvas: HTMLElement, config: { type: string, data: unknown, options?: unknown }) => ChartLike
|
|
290
|
+
|
|
291
|
+
const initialCampaigns: Campaign[] = [
|
|
292
|
+
{ id: 1, uuid: '11ce', name: 'Summer Sale Promotion', type: 'Discount', status: 'Active', startDate: '2023-06-01', endDate: '2023-08-31', budget: 5000, spent: 2300, goal: 'Sales', goalTarget: 25000, goalProgress: 12500, audience: 'All Customers', channels: ['Email', 'Social Media', 'SMS'], owner: 'Alex Johnson', ownerAvatar: 'https://randomuser.me/api/portraits/men/32.jpg', createdAt: '2023-05-15', lastModified: '2023-06-10' },
|
|
293
|
+
{ id: 2, uuid: '22ce', name: 'New Product Launch - Premium Coffee', type: 'Product Launch', status: 'Scheduled', startDate: '2023-09-15', endDate: '2023-10-15', budget: 8000, spent: 0, goal: 'Awareness', goalTarget: 50000, goalProgress: 0, audience: 'Coffee Enthusiasts', channels: ['Email', 'Social Media', 'Paid Ads'], owner: 'Sarah Miller', ownerAvatar: 'https://randomuser.me/api/portraits/women/44.jpg', createdAt: '2023-08-01', lastModified: '2023-08-15' },
|
|
294
|
+
{ id: 3, uuid: '33ce', name: 'Customer Loyalty Program', type: 'Email', status: 'Active', startDate: '2023-01-01', endDate: null, budget: 12000, spent: 5400, goal: 'Retention', goalTarget: 75, goalProgress: 65, audience: 'Existing Customers', channels: ['Email'], owner: 'David Chen', ownerAvatar: 'https://randomuser.me/api/portraits/men/67.jpg', createdAt: '2022-12-01', lastModified: '2023-07-15' },
|
|
295
|
+
{ id: 4, uuid: '44ce', name: 'Holiday Special Discount', type: 'Discount', status: 'Draft', startDate: '2023-12-01', endDate: '2023-12-31', budget: 6000, spent: 0, goal: 'Sales', goalTarget: 40000, goalProgress: 0, audience: 'All Customers', channels: ['Email', 'Social Media', 'Website'], owner: 'Emily Wilson', ownerAvatar: 'https://randomuser.me/api/portraits/women/23.jpg', createdAt: '2022-12-10', lastModified: '2023-06-15' },
|
|
296
|
+
{ id: 5, uuid: '55ce', name: 'Back to School Campaign', type: 'Seasonal', status: 'Completed', startDate: '2022-08-01', endDate: '2022-09-15', budget: 4500, spent: 4500, goal: 'Sales', goalTarget: 30000, goalProgress: 35200, audience: 'Students & Parents', channels: ['Email', 'Social Media', 'Paid Ads'], owner: 'Michael Thompson', ownerAvatar: 'https://randomuser.me/api/portraits/men/42.jpg', createdAt: '2022-07-05', lastModified: '2022-09-20' },
|
|
297
|
+
{ id: 6, uuid: '66ce', name: 'Spring Collection Launch', type: 'Product Launch', status: 'Completed', startDate: '2023-03-01', endDate: '2023-04-15', budget: 7500, spent: 7200, goal: 'Sales', goalTarget: 45000, goalProgress: 42000, audience: 'All Customers', channels: ['Email', 'Social Media', 'Website'], owner: 'Jessica Lee', ownerAvatar: 'https://randomuser.me/api/portraits/women/56.jpg', createdAt: '2023-02-10', lastModified: '2023-04-20' },
|
|
298
|
+
{ id: 7, uuid: '77ce', name: 'App Download Promotion', type: 'App Promotion', status: 'Active', startDate: '2023-07-01', endDate: '2023-09-30', budget: 6000, spent: 2800, goal: 'Downloads', goalTarget: 10000, goalProgress: 6500, audience: 'All Customers', channels: ['Email', 'Social Media', 'Paid Ads', 'Website'], owner: 'Robert Garcia', ownerAvatar: 'https://randomuser.me/api/portraits/men/78.jpg', createdAt: '2023-06-15', lastModified: '2023-07-25' },
|
|
299
|
+
{ id: 8, uuid: '88ce', name: 'Black Friday Sale', type: 'Discount', status: 'Draft', startDate: '2023-11-24', endDate: '2023-11-27', budget: 15000, spent: 0, goal: 'Sales', goalTarget: 100000, goalProgress: 0, audience: 'All Customers', channels: ['Email', 'Social Media', 'Paid Ads'], owner: 'Sophia Martinez', ownerAvatar: 'https://randomuser.me/api/portraits/women/90.jpg', createdAt: '2023-08-10', lastModified: '2023-08-10' },
|
|
300
|
+
{ id: 9, uuid: '99ce', name: 'Monthly Newsletter', type: 'Email', status: 'Scheduled', startDate: '2023-12-25', endDate: '2023-12-25', budget: 500, spent: 0, goal: 'Engagement', goalTarget: 2000, goalProgress: 0, audience: 'All Subscribers', channels: ['Email'], owner: 'Sarah Miller', ownerAvatar: 'https://randomuser.me/api/portraits/women/44.jpg', createdAt: '2023-12-10', lastModified: '2023-12-10' },
|
|
301
|
+
{ id: 10, uuid: 'aace', name: 'Holiday Sale Announcement', type: 'Email', status: 'Active', startDate: '2023-12-15', endDate: '2023-12-31', budget: 1200, spent: 800, goal: 'Sales', goalTarget: 50000, goalProgress: 32000, audience: 'All Customers', channels: ['Email'], owner: 'Alex Johnson', ownerAvatar: 'https://randomuser.me/api/portraits/men/32.jpg', createdAt: '2023-12-01', lastModified: '2023-12-05' },
|
|
302
|
+
{ id: 11, uuid: 'bbce', name: 'Flash Sale Alert', type: 'SMS', status: 'Scheduled', startDate: '2023-12-20', endDate: '2023-12-20', budget: 300, spent: 0, goal: 'Sales', goalTarget: 10000, goalProgress: 0, audience: 'Opted-in Customers', channels: ['SMS'], owner: 'David Chen', ownerAvatar: 'https://randomuser.me/api/portraits/men/67.jpg', createdAt: '2023-12-15', lastModified: '2023-12-15' },
|
|
303
|
+
{ id: 12, uuid: 'ccce', name: 'Order Delivery Notification', type: 'SMS', status: 'Active', startDate: '2023-12-01', endDate: null, budget: 200, spent: 120, goal: 'Customer Service', goalTarget: 500, goalProgress: 320, audience: 'Recent Customers', channels: ['SMS'], owner: 'Emily Wilson', ownerAvatar: 'https://randomuser.me/api/portraits/women/23.jpg', createdAt: '2023-11-25', lastModified: '2023-11-30' },
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
const campaigns = state<Campaign[]>(initialCampaigns)
|
|
307
|
+
const searchQuery = state('')
|
|
308
|
+
const statusFilter = state('all')
|
|
309
|
+
const typeFilter = state('all')
|
|
310
|
+
const sortBy = state<'startDate' | 'name' | 'budget' | 'goalProgress'>('startDate')
|
|
311
|
+
const sortOrder = state<'asc' | 'desc'>('desc')
|
|
312
|
+
const currentPage = state(1)
|
|
313
|
+
const itemsPerPage = 9
|
|
314
|
+
|
|
315
|
+
const showCampaignModal = state(false)
|
|
316
|
+
const isEditMode = state(false)
|
|
317
|
+
const editingCampaignId = state<number | null>(null)
|
|
318
|
+
const formName = state('')
|
|
319
|
+
const formType = state('Email')
|
|
320
|
+
const formStatus = state('Draft')
|
|
321
|
+
const formStartDate = state('')
|
|
322
|
+
const formEndDate = state('')
|
|
323
|
+
const formBudget = state('0')
|
|
324
|
+
const formGoal = state('Sales')
|
|
325
|
+
const formAudience = state('All Customers')
|
|
326
|
+
|
|
327
|
+
const totalCampaigns = derived(() => campaigns().length)
|
|
328
|
+
const activeCount = derived(() => campaigns().filter(c => c.status === 'Active').length)
|
|
329
|
+
const scheduledCount = derived(() => campaigns().filter(c => c.status === 'Scheduled').length)
|
|
330
|
+
const draftCount = derived(() => campaigns().filter(c => c.status === 'Draft').length)
|
|
331
|
+
|
|
332
|
+
const filteredCampaigns = derived(() => {
|
|
333
|
+
const q = searchQuery().toLowerCase()
|
|
334
|
+
const status = statusFilter()
|
|
335
|
+
const type = typeFilter()
|
|
336
|
+
const sort = sortBy()
|
|
337
|
+
const order = sortOrder()
|
|
338
|
+
return campaigns()
|
|
339
|
+
.filter((c) => {
|
|
340
|
+
const matchesSearch = !q || c.name.toLowerCase().includes(q) || c.owner.toLowerCase().includes(q)
|
|
341
|
+
const matchesStatus = status === 'all' || c.status === status
|
|
342
|
+
const matchesType = type === 'all' || c.type === type
|
|
343
|
+
return matchesSearch && matchesStatus && matchesType
|
|
344
|
+
})
|
|
345
|
+
.sort((a, b) => {
|
|
346
|
+
let cmp = 0
|
|
347
|
+
if (sort === 'name')
|
|
348
|
+
cmp = a.name.localeCompare(b.name)
|
|
349
|
+
else if (sort === 'startDate')
|
|
350
|
+
cmp = new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
|
351
|
+
else if (sort === 'budget')
|
|
352
|
+
cmp = a.budget - b.budget
|
|
353
|
+
else
|
|
354
|
+
cmp = a.goalProgress - b.goalProgress
|
|
355
|
+
return order === 'asc' ? cmp : -cmp
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const totalPages = derived(() => Math.max(1, Math.ceil(filteredCampaigns().length / itemsPerPage)))
|
|
360
|
+
|
|
361
|
+
const paginatedCampaigns = derived(() => {
|
|
362
|
+
const start = (currentPage() - 1) * itemsPerPage
|
|
363
|
+
return filteredCampaigns().slice(start, start + itemsPerPage)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
effect(() => {
|
|
367
|
+
filteredCampaigns()
|
|
368
|
+
currentPage.set(1)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
effect(() => {
|
|
372
|
+
if (currentPage() > totalPages())
|
|
373
|
+
currentPage.set(totalPages())
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
function formatDate(date: string | null) {
|
|
377
|
+
if (!date)
|
|
378
|
+
return 'Ongoing'
|
|
379
|
+
return new Date(date).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function formatCurrency(value: number) {
|
|
383
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }).format(value)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function formatGoalProgress(c: Campaign) {
|
|
387
|
+
const goal = c.goal.toLowerCase()
|
|
388
|
+
if (goal.includes('sales') || goal.includes('budget'))
|
|
389
|
+
return `${formatCurrency(c.goalProgress)} / ${formatCurrency(c.goalTarget)}`
|
|
390
|
+
if (goal.includes('retention') || goal.includes('rate'))
|
|
391
|
+
return `${c.goalProgress}% / ${c.goalTarget}%`
|
|
392
|
+
return `${c.goalProgress.toLocaleString()} / ${c.goalTarget.toLocaleString()}`
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function goalPercentage(c: Campaign) {
|
|
396
|
+
if (c.goalTarget === 0)
|
|
397
|
+
return 0
|
|
398
|
+
return Math.min(100, Math.round((c.goalProgress / c.goalTarget) * 100))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function statusBadgeClass(status: string) {
|
|
402
|
+
const base = 'inline-flex items-center px-2 py-0.5 font-medium text-xs rounded-full ring-1 ring-inset'
|
|
403
|
+
switch (status) {
|
|
404
|
+
case 'Active': return `${base} bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-900/30 dark:text-green-400`
|
|
405
|
+
case 'Scheduled': return `${base} bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400`
|
|
406
|
+
case 'Draft': return `${base} bg-yellow-50 text-yellow-700 ring-yellow-600/20 dark:bg-yellow-900/30 dark:text-yellow-400`
|
|
407
|
+
case 'Completed': return `${base} bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-neutral-900/30 dark:text-neutral-400`
|
|
408
|
+
case 'Paused': return `${base} bg-orange-50 text-orange-700 ring-orange-600/20 dark:bg-orange-900/30 dark:text-orange-400`
|
|
409
|
+
default: return `${base} bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-neutral-900/30 dark:text-neutral-400`
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function typeBadgeClass(type: string) {
|
|
414
|
+
const base = 'inline-flex items-center px-2 py-0.5 font-medium text-xs rounded'
|
|
415
|
+
switch (type) {
|
|
416
|
+
case 'Email': return `${base} bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200`
|
|
417
|
+
case 'SMS': return `${base} bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-200`
|
|
418
|
+
case 'Discount': return `${base} bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-200`
|
|
419
|
+
case 'Seasonal': return `${base} bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-200`
|
|
420
|
+
case 'Product Launch': return `${base} bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-200`
|
|
421
|
+
case 'App Promotion': return `${base} bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-200`
|
|
422
|
+
default: return `${base} bg-gray-100 text-gray-700 dark:bg-neutral-700 dark:text-neutral-200`
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function nextPage() {
|
|
427
|
+
if (currentPage() < totalPages())
|
|
428
|
+
currentPage.set(currentPage() + 1)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function prevPage() {
|
|
432
|
+
if (currentPage() > 1)
|
|
433
|
+
currentPage.set(currentPage() - 1)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function openNewCampaignModal() {
|
|
437
|
+
editingCampaignId.set(null)
|
|
438
|
+
isEditMode.set(false)
|
|
439
|
+
formName.set('')
|
|
440
|
+
formType.set('Email')
|
|
441
|
+
formStatus.set('Draft')
|
|
442
|
+
formStartDate.set(new Date().toISOString().split('T')[0]!)
|
|
443
|
+
formEndDate.set('')
|
|
444
|
+
formBudget.set('0')
|
|
445
|
+
formGoal.set('Sales')
|
|
446
|
+
formAudience.set('All Customers')
|
|
447
|
+
showCampaignModal.set(true)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function openEditCampaignModal(id: number) {
|
|
451
|
+
const c = campaigns().find(x => x.id === id)
|
|
452
|
+
if (!c)
|
|
453
|
+
return
|
|
454
|
+
editingCampaignId.set(id)
|
|
455
|
+
isEditMode.set(true)
|
|
456
|
+
formName.set(c.name)
|
|
457
|
+
formType.set(c.type)
|
|
458
|
+
formStatus.set(c.status)
|
|
459
|
+
formStartDate.set(c.startDate)
|
|
460
|
+
formEndDate.set(c.endDate ?? '')
|
|
461
|
+
formBudget.set(String(c.budget))
|
|
462
|
+
formGoal.set(c.goal)
|
|
463
|
+
formAudience.set(c.audience)
|
|
464
|
+
showCampaignModal.set(true)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function closeCampaignModal() {
|
|
468
|
+
showCampaignModal.set(false)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function saveCampaign() {
|
|
472
|
+
const name = formName().trim()
|
|
473
|
+
if (!name)
|
|
474
|
+
return
|
|
475
|
+
const budget = Number.parseFloat(formBudget()) || 0
|
|
476
|
+
const endDate = formEndDate() || null
|
|
477
|
+
if (isEditMode()) {
|
|
478
|
+
const id = editingCampaignId()
|
|
479
|
+
campaigns.set(campaigns().map(c => c.id === id
|
|
480
|
+
? { ...c, name, type: formType(), status: formStatus(), startDate: formStartDate() || c.startDate, endDate, budget, goal: formGoal(), audience: formAudience(), lastModified: new Date().toISOString().split('T')[0]! }
|
|
481
|
+
: c))
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
const nextId = Math.max(0, ...campaigns().map(c => c.id)) + 1
|
|
485
|
+
campaigns.set([...campaigns(), {
|
|
486
|
+
id: nextId,
|
|
487
|
+
uuid: crypto.randomUUID().substring(0, 8),
|
|
488
|
+
name,
|
|
489
|
+
type: formType(),
|
|
490
|
+
status: formStatus(),
|
|
491
|
+
startDate: formStartDate() || new Date().toISOString().split('T')[0]!,
|
|
492
|
+
endDate,
|
|
493
|
+
budget,
|
|
494
|
+
spent: 0,
|
|
495
|
+
goal: formGoal(),
|
|
496
|
+
goalTarget: 0,
|
|
497
|
+
goalProgress: 0,
|
|
498
|
+
audience: formAudience(),
|
|
499
|
+
channels: [formType()],
|
|
500
|
+
owner: 'Marketing Team',
|
|
501
|
+
ownerAvatar: 'https://randomuser.me/api/portraits/women/44.jpg',
|
|
502
|
+
createdAt: new Date().toISOString().split('T')[0]!,
|
|
503
|
+
lastModified: new Date().toISOString().split('T')[0]!,
|
|
504
|
+
}])
|
|
505
|
+
}
|
|
506
|
+
closeCampaignModal()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function deleteCampaign(id: number) {
|
|
510
|
+
campaigns.set(campaigns().filter(c => c.id !== id))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function duplicateCampaign(id: number) {
|
|
514
|
+
const original = campaigns().find(c => c.id === id)
|
|
515
|
+
if (!original)
|
|
516
|
+
return
|
|
517
|
+
const nextId = Math.max(0, ...campaigns().map(c => c.id)) + 1
|
|
518
|
+
campaigns.set([...campaigns(), {
|
|
519
|
+
...original,
|
|
520
|
+
id: nextId,
|
|
521
|
+
name: `${original.name} (Copy)`,
|
|
522
|
+
status: 'Draft',
|
|
523
|
+
spent: 0,
|
|
524
|
+
goalProgress: 0,
|
|
525
|
+
createdAt: new Date().toISOString().split('T')[0]!,
|
|
526
|
+
lastModified: new Date().toISOString().split('T')[0]!,
|
|
527
|
+
}])
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function openCampaignActions(id: number) {
|
|
531
|
+
openEditCampaignModal(id)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let chartHandles: Array<ChartLike | null> = []
|
|
535
|
+
|
|
536
|
+
function destroyCharts() {
|
|
537
|
+
for (const h of chartHandles)
|
|
538
|
+
h?.destroy()
|
|
539
|
+
chartHandles = []
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function buildCharts() {
|
|
543
|
+
destroyCharts()
|
|
544
|
+
const { Chart, registerables } = await import('@stacksjs/charts')
|
|
545
|
+
Chart.register(...registerables)
|
|
546
|
+
const Ctor = Chart as unknown as ChartCtor
|
|
547
|
+
|
|
548
|
+
const active = campaigns().filter(c => c.status === 'Active').slice(0, 6)
|
|
549
|
+
const goalChart = useChart({
|
|
550
|
+
Chart: Ctor,
|
|
551
|
+
id: 'campaigns_goal_chart',
|
|
552
|
+
type: 'bar',
|
|
553
|
+
data: {
|
|
554
|
+
labels: active.map(c => c.name.length > 20 ? `${c.name.slice(0, 17)}…` : c.name),
|
|
555
|
+
datasets: [{
|
|
556
|
+
label: 'Progress %',
|
|
557
|
+
data: active.map(c => goalPercentage(c)),
|
|
558
|
+
backgroundColor: 'rgba(59, 130, 246, 0.5)',
|
|
559
|
+
borderColor: 'rgba(59, 130, 246, 1)',
|
|
560
|
+
borderWidth: 1,
|
|
561
|
+
}],
|
|
562
|
+
},
|
|
563
|
+
options: {
|
|
564
|
+
responsive: true,
|
|
565
|
+
maintainAspectRatio: false,
|
|
566
|
+
indexAxis: 'y',
|
|
567
|
+
plugins: { legend: { display: false } },
|
|
568
|
+
scales: { x: { beginAtZero: true, max: 100, ticks: { callback: (v: number | string) => `${v}%` } } },
|
|
569
|
+
},
|
|
570
|
+
})
|
|
571
|
+
chartHandles.push(goalChart.instance as ChartLike | null)
|
|
572
|
+
|
|
573
|
+
const top = filteredCampaigns().slice(0, 6)
|
|
574
|
+
const budgetChart = useChart({
|
|
575
|
+
Chart: Ctor,
|
|
576
|
+
id: 'campaigns_budget_chart',
|
|
577
|
+
type: 'bar',
|
|
578
|
+
data: {
|
|
579
|
+
labels: top.map(c => c.name.length > 20 ? `${c.name.slice(0, 17)}…` : c.name),
|
|
580
|
+
datasets: [
|
|
581
|
+
{ label: 'Budget', data: top.map(c => c.budget), backgroundColor: 'rgba(99, 102, 241, 0.5)' },
|
|
582
|
+
{ label: 'Spent', data: top.map(c => c.spent), backgroundColor: 'rgba(244, 63, 94, 0.7)' },
|
|
583
|
+
],
|
|
584
|
+
},
|
|
585
|
+
options: {
|
|
586
|
+
responsive: true,
|
|
587
|
+
maintainAspectRatio: false,
|
|
588
|
+
plugins: { legend: { position: 'top' } },
|
|
589
|
+
scales: { y: { beginAtZero: true, ticks: { callback: (v: number | string) => `$${Number(v).toLocaleString()}` } } },
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
chartHandles.push(budgetChart.instance as ChartLike | null)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
onMount(async () => {
|
|
596
|
+
await buildCharts()
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
onDestroy(() => {
|
|
600
|
+
destroyCharts()
|
|
601
|
+
})
|
|
602
|
+
</script>
|
|
603
|
+
|
|
604
|
+
@endsection
|