diff --git a/.gitignore b/.gitignore
index 4f648764..caa8269a 100755
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@
/node_modules
/drivers/
.codex
+.claude/
diff --git a/assets/router/index.js b/assets/router/index.js
index fa9fab49..d65e1dae 100644
--- a/assets/router/index.js
+++ b/assets/router/index.js
@@ -1,39 +1,25 @@
import { createRouter, createWebHistory } from 'vue-router';
-import DashboardView from '../vue/views/DashboardView.vue'
-import SubscribersView from '../vue/views/SubscribersView.vue'
-import ListsView from '../vue/views/ListsView.vue'
-import ListSubscribersView from '../vue/views/ListSubscribersView.vue'
-import CampaignsView from '../vue/views/CampaignsView.vue'
-import CampaignEditView from '../vue/views/CampaignEditView.vue'
-import StuckCampaignsView from '../vue/views/StuckCampaignsView.vue'
-import TemplatesView from '../vue/views/TemplatesView.vue'
-import TemplateEditView from '../vue/views/TemplateEditView.vue'
-import BouncesView from '../vue/views/BouncesView.vue'
-import AnalyticsView from '../vue/views/AnalyticsView.vue'
-import PublicPagesView from '../vue/views/PublicPagesView.vue'
-import PublicPageEditView from '../vue/views/PublicPageEditView.vue'
-import SettingsView from '../vue/views/SettingsView.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
- { path: '/', name: 'dashboard', component: DashboardView, meta: { title: 'Dashboard' } },
- { path: '/subscribers', name: 'subscribers', component: SubscribersView, meta: { title: 'Subscribers' } },
- { path: '/lists', name: 'lists', component: ListsView, meta: { title: 'Lists' } },
- { path: '/campaigns', name: 'campaigns', component: CampaignsView, meta: { title: 'Campaigns' } },
- { path: '/campaigns/stuck', name: 'stuck-campaigns', component: StuckCampaignsView, meta: { title: 'Stuck Campaigns' } },
- { path: '/templates', name: 'templates', component: TemplatesView, meta: { title: 'Templates' } },
- { path: '/templates/create', name: 'template-create', component: TemplateEditView, meta: { title: 'Create Template' } },
- { path: '/templates/:templateId/edit', name: 'template-edit', component: TemplateEditView, meta: { title: 'Edit Template' } },
- { path: '/campaigns/create', name: 'campaign-create', component: CampaignEditView, meta: { title: 'Create Campaign' } },
- { path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: CampaignEditView, meta: { title: 'Edit Campaign' } },
- { path: '/lists/:listId/subscribers', name: 'list-subscribers', component: ListSubscribersView, meta: { title: 'List Subscribers' } },
- { path: '/bounces', name: 'bounces', component: BouncesView, meta: { title: 'Bounces' } },
- { path: '/analytics', name: 'analytics', component: AnalyticsView, meta: { title: 'Analytics' } },
- { path: '/public', name: 'public-pages', component: PublicPagesView, meta: { title: 'Public Pages' } },
- { path: '/public/create', name: 'public-page-create', component: PublicPageEditView, meta: { title: 'Create Public Page' } },
- { path: '/public/:pageId/edit', name: 'public-page-edit', component: PublicPageEditView, meta: { title: 'Edit Public Page' } },
- { path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } },
+ { path: '/', name: 'dashboard', component: () => import('../vue/views/DashboardView.vue'), meta: { title: 'Dashboard' } },
+ { path: '/subscribers', name: 'subscribers', component: () => import('../vue/views/SubscribersView.vue'), meta: { title: 'Subscribers' } },
+ { path: '/lists', name: 'lists', component: () => import('../vue/views/ListsView.vue'), meta: { title: 'Lists' } },
+ { path: '/campaigns', name: 'campaigns', component: () => import('../vue/views/CampaignsView.vue'), meta: { title: 'Campaigns' } },
+ { path: '/campaigns/stuck', name: 'stuck-campaigns', component: () => import('../vue/views/StuckCampaignsView.vue'), meta: { title: 'Stuck Campaigns' } },
+ { path: '/templates', name: 'templates', component: () => import('../vue/views/TemplatesView.vue'), meta: { title: 'Templates' } },
+ { path: '/templates/create', name: 'template-create', component: () => import('../vue/views/TemplateEditView.vue'), meta: { title: 'Create Template' } },
+ { path: '/templates/:templateId/edit', name: 'template-edit', component: () => import('../vue/views/TemplateEditView.vue'), meta: { title: 'Edit Template' } },
+ { path: '/campaigns/create', name: 'campaign-create', component: () => import('../vue/views/CampaignEditView.vue'), meta: { title: 'Create Campaign' } },
+ { path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: () => import('../vue/views/CampaignEditView.vue'), meta: { title: 'Edit Campaign' } },
+ { path: '/lists/:listId/subscribers', name: 'list-subscribers', component: () => import('../vue/views/ListSubscribersView.vue'), meta: { title: 'List Subscribers' } },
+ { path: '/bounces', name: 'bounces', component: () => import('../vue/views/BouncesView.vue'), meta: { title: 'Bounces' } },
+ { path: '/analytics', name: 'analytics', component: () => import('../vue/views/AnalyticsView.vue'), meta: { title: 'Analytics' } },
+ { path: '/public', name: 'public-pages', component: () => import('../vue/views/PublicPagesView.vue'), meta: { title: 'Public Pages' } },
+ { path: '/public/create', name: 'public-page-create', component: () => import('../vue/views/PublicPageEditView.vue'), meta: { title: 'Create Public Page' } },
+ { path: '/public/:pageId/edit', name: 'public-page-edit', component: () => import('../vue/views/PublicPageEditView.vue'), meta: { title: 'Edit Public Page' } },
+ { path: '/settings', name: 'settings', component: () => import('../vue/views/SettingsView.vue'), meta: { title: 'Settings' } },
{ path: '/:pathMatch(.*)*', redirect: '/' },
],
});
diff --git a/assets/vue/components/base/ActionButton.vue b/assets/vue/components/base/ActionButton.vue
new file mode 100644
index 00000000..7c145a61
--- /dev/null
+++ b/assets/vue/components/base/ActionButton.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/vue/components/base/BaseBadge.vue b/assets/vue/components/base/BaseBadge.vue
index 53f2fec4..2ac0796b 100644
--- a/assets/vue/components/base/BaseBadge.vue
+++ b/assets/vue/components/base/BaseBadge.vue
@@ -11,7 +11,7 @@ import { computed } from 'vue'
const props = defineProps({
variant: {
type: String,
- default: 'neutral', // neutral | counter
+ default: 'neutral', // neutral | counter | success | danger | info | warning | purple
},
})
@@ -20,6 +20,16 @@ const badgeClass = computed(() => {
switch (props.variant) {
case 'counter':
return `${base} bg-indigo-50 dark:bg-indigo-500/10 text-ext-wf3 dark:text-indigo-300 border border-indigo-100 dark:border-indigo-500/20`;
+ case 'success':
+ return `${base} bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400`;
+ case 'danger':
+ return `${base} bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400`;
+ case 'info':
+ return `${base} bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400`;
+ case 'warning':
+ return `${base} bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400`;
+ case 'purple':
+ return `${base} bg-purple-100 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400`;
case 'neutral':
default:
return `${base} bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-slate-200`;
diff --git a/assets/vue/components/base/BaseButton.vue b/assets/vue/components/base/BaseButton.vue
index 10e50561..c840b22b 100644
--- a/assets/vue/components/base/BaseButton.vue
+++ b/assets/vue/components/base/BaseButton.vue
@@ -5,27 +5,35 @@
type="button"
v-bind="$attrs"
>
+
\ No newline at end of file
diff --git a/assets/vue/components/base/BaseIcon.vue b/assets/vue/components/base/BaseIcon.vue
index 573d2f84..dc11ff50 100644
--- a/assets/vue/components/base/BaseIcon.vue
+++ b/assets/vue/components/base/BaseIcon.vue
@@ -18,6 +18,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
+
+ // when true, skips the default slate/indigo color so the icon inherits
+ // currentColor from its parent (e.g. white text on a solid button)
+ inheritColor: {
+ type: Boolean,
+ default: false,
+ },
});
// Simple internal SVG registry
@@ -99,6 +106,8 @@ const icons = {
moon: ``,
start: ``,
+
+ plus: ``,
};
const svg = computed(() => icons[props.name] || "");
@@ -111,7 +120,9 @@ const wrapperClass = computed(() => {
"transition-colors",
];
- if (props.active) {
+ if (props.inheritColor) {
+ // no color class: SVG's stroke="currentColor" picks up the parent's text color
+ } else if (props.active) {
classes.push("text-ext-wf3 dark:text-indigo-400");
} else {
classes.push("text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300");
diff --git a/assets/vue/components/base/BaseModal.vue b/assets/vue/components/base/BaseModal.vue
new file mode 100644
index 00000000..2de8cd75
--- /dev/null
+++ b/assets/vue/components/base/BaseModal.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/vue/components/bounces/BounceOverview.vue b/assets/vue/components/bounces/BounceOverview.vue
index c368bf77..9e00691b 100644
--- a/assets/vue/components/bounces/BounceOverview.vue
+++ b/assets/vue/components/bounces/BounceOverview.vue
@@ -17,84 +17,49 @@
-
-
-
-
- | ID |
- Date |
- Subscriber |
- Campaign |
- Status |
- Comment |
-
-
-
-
- | Loading bounces... |
-
-
- | {{ errorMessage }} |
-
-
- | No bounces found. |
-
-
- | #{{ bounce.id }} |
- {{ bounce.formattedDate }} |
- {{ bounce.email }} |
- {{ bounce.subject }} |
-
-
- {{ bounce.status }}
-
- |
- {{ bounce.comment }} |
-
-
-
-
-
-
- Loading bounces...
-
-
- {{ errorMessage }}
-
-
- No bounces found.
-
-
-
-
#{{ bounce.id }}
-
- {{ bounce.status }}
-
-
-
{{ bounce.formattedDate }}
-
{{ bounce.email }}
-
{{ bounce.subject }}
-
{{ bounce.comment }}
+
+
+ ID |
+ Date |
+ Subscriber |
+ Campaign |
+ Status |
+ Comment |
+
+
+
+ #{{ bounce.id }} |
+ {{ bounce.formattedDate }} |
+ {{ bounce.email }} |
+ {{ bounce.subject }} |
+
+
+ {{ bounce.status }}
+
+ |
+ {{ bounce.comment }} |
+
+
+
+
+
#{{ bounce.id }}
+
+ {{ bounce.status }}
+
-
-
+
{{ bounce.formattedDate }}
+
{{ bounce.email }}
+
{{ bounce.subject }}
+
{{ bounce.comment }}
+
+
@@ -126,6 +91,8 @@
diff --git a/assets/vue/components/campaigns/StuckCampaignsList.vue b/assets/vue/components/campaigns/StuckCampaignsList.vue
index 0f37bc37..39996886 100644
--- a/assets/vue/components/campaigns/StuckCampaignsList.vue
+++ b/assets/vue/components/campaigns/StuckCampaignsList.vue
@@ -20,134 +20,50 @@
-
-
-
-
- | Subject |
- Status |
- Stuck for |
- Actions |
-
-
-
-
-
- | Loading stuck campaigns... |
-
-
-
- | {{ errorMessage }} |
-
-
-
- | No stuck campaigns right now. |
-
-
-
- |
-
- {{ campaign.subject || `Campaign #${campaign.id}` }}
-
- |
-
-
- {{ campaign.statusLabel }}
-
- |
- {{ campaign.stuckFor }} |
-
-
-
-
-
- {{ getActionFeedback(campaign.id)?.message }}
-
- |
-
-
-
-
-
-
- Loading stuck campaigns...
-
-
-
- {{ errorMessage }}
-
-
-
- No stuck campaigns right now.
-
-
-
-
-
- {{ campaign.subject || `Campaign #${campaign.id}` }}
-
-
- {{ campaign.statusLabel }}
-
-
-
-
- Stuck for: {{ campaign.stuckFor }}
-
-
-
-