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 @@ + + + \ 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 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- Loading bounces... -
-
- {{ errorMessage }} -
-
- No bounces found. -
-
-
-

#{{ 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 @@
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- Loading stuck campaigns... -
- -
- {{ errorMessage }} -
- -
- No stuck campaigns right now. -
- -
-
- - {{ campaign.subject || `Campaign #${campaign.id}` }} - - - {{ campaign.statusLabel }} - -
- -

- Stuck for: {{ campaign.stuckFor }} -

- -
-
@@ -166,6 +122,9 @@ import { onMounted, ref } from 'vue' import { campaignClient } from '../../api' import BaseIcon from '../base/BaseIcon.vue' +import BaseBadge from '../base/BaseBadge.vue' +import ActionButton from '../base/ActionButton.vue' +import BaseDataTable from '../base/BaseDataTable.vue' const campaigns = ref([]) const isLoading = ref(false) diff --git a/assets/vue/components/campaigns/ViewCampaignModal.vue b/assets/vue/components/campaigns/ViewCampaignModal.vue index 4ec1e200..59791f1e 100644 --- a/assets/vue/components/campaigns/ViewCampaignModal.vue +++ b/assets/vue/components/campaigns/ViewCampaignModal.vue @@ -1,22 +1,10 @@ diff --git a/assets/vue/components/lists/AddSubscribersModal.vue b/assets/vue/components/lists/AddSubscribersModal.vue index 2cf7d0b1..6bafb337 100644 --- a/assets/vue/components/lists/AddSubscribersModal.vue +++ b/assets/vue/components/lists/AddSubscribersModal.vue @@ -1,26 +1,11 @@ \ No newline at end of file diff --git a/assets/vue/components/settings/CreateAdminAttributeModal.vue b/assets/vue/components/settings/CreateAdminAttributeModal.vue index 7cc95891..96303552 100644 --- a/assets/vue/components/settings/CreateAdminAttributeModal.vue +++ b/assets/vue/components/settings/CreateAdminAttributeModal.vue @@ -1,23 +1,10 @@ + \ No newline at end of file diff --git a/assets/vue/components/settings/SettingsAdmins.vue b/assets/vue/components/settings/SettingsAdmins.vue index 33e16aae..54ec0c60 100644 --- a/assets/vue/components/settings/SettingsAdmins.vue +++ b/assets/vue/components/settings/SettingsAdmins.vue @@ -6,154 +6,81 @@

Manage administrator accounts and permissions

- +
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-

#{{ admin.id }}

-

{{ admin.loginName }}

-

{{ admin.email }}

-
- - - {{ admin.superUser ? 'Super' : 'User' }} - -
- -

- Created: {{ formatDate(admin.createdAt) }} -

- -
-
@@ -177,7 +104,10 @@ import { onMounted, ref } from 'vue' import CreateAdminModal from './CreateAdminModal.vue' import EditAdminModal from './EditAdminModal.vue' import { fetchAllAdmins, adminClient } from '../../api' -import BaseIcon from "../base/BaseIcon.vue"; +import BaseButton from "../base/BaseButton.vue"; +import BaseBadge from "../base/BaseBadge.vue"; +import ActionButton from "../base/ActionButton.vue"; +import BaseDataTable from "../base/BaseDataTable.vue"; const admins = ref([]) const isLoading = ref(false) diff --git a/assets/vue/components/settings/SettingsSubscriberAttributes.vue b/assets/vue/components/settings/SettingsSubscriberAttributes.vue index 506c179b..24e969c2 100644 --- a/assets/vue/components/settings/SettingsSubscriberAttributes.vue +++ b/assets/vue/components/settings/SettingsSubscriberAttributes.vue @@ -1,304 +1,32 @@ + \ No newline at end of file diff --git a/assets/vue/components/subscribers/ImportResult.vue b/assets/vue/components/subscribers/ImportResult.vue index 8522af67..6cf20eec 100644 --- a/assets/vue/components/subscribers/ImportResult.vue +++ b/assets/vue/components/subscribers/ImportResult.vue @@ -1,20 +1,10 @@ diff --git a/assets/vue/components/templates/TemplateLibrary.vue b/assets/vue/components/templates/TemplateLibrary.vue index 9c303fc9..d746a68f 100644 --- a/assets/vue/components/templates/TemplateLibrary.vue +++ b/assets/vue/components/templates/TemplateLibrary.vue @@ -15,10 +15,7 @@ type="button" @click="goToCreateTemplate" > - + New Template
@@ -63,24 +60,13 @@
- + - +
@@ -88,31 +74,13 @@
- + + +