Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "🔍 Running PHPStan..."
php vendor/bin/phpstan analyse -l 5 src/ tests/ || exit 1

echo "📏 Running PHPMD..."
php vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml || exit 1

echo "🧹 Running PHPCS..."
php vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/ || exit 1
#echo "🔍 Running PHPStan..."
#php vendor/bin/phpstan analyse -l 5 src/ tests/ || exit 1
#
#echo "📏 Running PHPMD..."
#php vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml || exit 1
#
#echo "🧹 Running PHPCS..."
#php vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/ || exit 1
7 changes: 5 additions & 2 deletions assets/app.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
console.log('app.js loaded');
import { createApp } from 'vue';
import App from './vue/App.vue';
import { router } from './router';

// Mount the main app if the element exists
const appElement = document.getElementById('vue-app');
if (appElement) {
createApp(App).mount('#vue-app');
const app = createApp(App);
app.use(router);
app.mount('#vue-app');
}

Binary file added assets/images/logo-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/router/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router';

export const router = createRouter({
history: createWebHistory(),
routes: [ /* ... */ ],
Comment thread
TatevikGr marked this conversation as resolved.
Outdated
});
Comment on lines +23 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm campaign edit title mismatch between backend title and router meta title.
rg -nP "Edit Campaign|to\.meta\.title|document\.title|campaignId" -C3 assets src tests

Repository: phpList/web-frontend

Length of output: 23299


Make the campaign ID visible in the edit page title.

The backend renders Edit Campaign #123`` for /campaigns/123/edit, but the router's static `meta.title` overwrites it with just `Edit Campaign` after navigation, losing the ID. The Vue component already computes the correct title internally, so the router hook needs to support dynamic meta titles.

The proposed fix—making meta.title a function that accepts the route and checking for it in the router.afterEach hook—will solve this.

Proposed fix
-        { path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: CampaignEditView, meta: { title: 'Edit Campaign' } },
+        {
+            path: '/campaigns/:campaignId/edit',
+            name: 'campaign-edit',
+            component: CampaignEditView,
+            meta: { title: (route) => `Edit Campaign #${route.params.campaignId}` },
+        },
@@
 router.afterEach((to) => {
     const defaultTitle = 'phpList';
-    const pageTitle = to.meta.title;
+    const pageTitle = typeof to.meta.title === 'function' ? to.meta.title(to) : to.meta.title;
     document.title = pageTitle ? `${defaultTitle} - ${pageTitle}` : defaultTitle;
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/router/index.js` around lines 17 - 27, The router currently overwrites
dynamic titles because meta.title is a static string for the route
'/campaigns/:campaignId/edit' (name: 'campaign-edit') and router.afterEach
always uses to.meta.title; change meta.title to accept either a string or a
function and update the router.afterEach hook (router.afterEach) to detect if
to.meta.title is a function and, if so, call it with the current route (to) to
get the pageTitle (falling back to the string value or defaultTitle) so
CampaignEditView can surface the computed "Edit Campaign `#123`" title without
losing support for static titles.

64 changes: 45 additions & 19 deletions assets/vue/App.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
<template>
<div>
<h2>Hello from Vue</h2>
<p>{{ message }}</p>
</div>
<aside class="sidebar">
<div class="sidebar__top">
<SidebarLogo />

<nav class="sidebar__nav">
<SidebarNavSection
v-for="section in sections"
:key="section.id"
v-bind="section"
/>
</nav>
</div>
</aside>
</template>

<script>
export default {
name: 'App',
data() {
return {
message: 'This is a reusable component!'
}
<script setup>
import SidebarLogo from './SidebarLogo.vue'
import SidebarNavSection from './SidebarNavSection.vue'

const sections = [
{
id: 'general',
label: 'General',
items: [
{ label: 'Dashboard', icon: 'grid', route: '/dashboard', badge: null },
{ label: 'Subscribers', icon: 'users', route: '/subscribers' },
{ label: 'Lists & Segments', icon: 'list', route: '/lists' },
],
},
{
id: 'marketing',
label: 'Marketing',
items: [
{ label: 'Campaigns', icon: 'plane', route: '/campaigns' },
{ label: 'Templates', icon: 'layout', route: '/templates' },
],
},
created() {
console.log('App component created');
{
id: 'analytics',
label: 'Analytics',
items: [
{ label: 'Bounces & Reports', icon: 'chart', route: '/reports' },
],
},
mounted() {
console.log('App component mounted');
{
id: 'system',
label: 'System',
items: [{ label: 'Settings', icon: 'settings', route: '/settings' }],
},
updated() {
console.log('App component updated');
}
}
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</script>
52 changes: 52 additions & 0 deletions assets/vue/AppSidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<aside class="sidebar">
<div class="sidebar__top">
<SidebarLogo />

<nav class="sidebar__nav">
<SidebarNavSection
v-for="section in sections"
:key="section.id"
v-bind="section"
/>
</nav>
</div>
</aside>
</template>

<script setup>
import SidebarLogo from './SidebarLogo.vue'
import SidebarNavSection from './SidebarNavSection.vue'

const sections = [
{
id: 'general',
label: 'General',
items: [
{ label: 'Dashboard', icon: 'grid', route: '/dashboard', badge: null },
{ label: 'Subscribers', icon: 'users', route: '/subscribers' },
{ label: 'Lists & Segments', icon: 'list', route: '/lists' },
],
},
{
id: 'marketing',
label: 'Marketing',
items: [
{ label: 'Campaigns', icon: 'plane', route: '/campaigns', badge: 3 },
{ label: 'Templates', icon: 'layout', route: '/templates' },
],
},
{
id: 'analytics',
label: 'Analytics',
items: [
{ label: 'Bounces & Reports', icon: 'chart-bar', route: '/reports' },
],
},
{
id: 'system',
label: 'System',
items: [{ label: 'Settings', icon: 'settings', route: '/settings' }],
},
]
</script>
14 changes: 14 additions & 0 deletions assets/vue/BaseCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div class="card" :class="[`card--${variant}`]">
<slot />
</div>
</template>

<script setup>
const props = defineProps({
variant: {
type: String,
default: 'default', // default | subtle | danger | ...
},
})
</script>
136 changes: 136 additions & 0 deletions assets/vue/CampaignsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<template>
<table class="campaigns-table">
<thead>
<tr>
<th>Campaign Name</th>
<th>Status</th>
<th>Date</th>
<th>Open Rate</th>
<th>Click Rate</th>
</tr>
</thead>

<tbody>
<tr
v-for="row in rows"
:key="row.id"
>
<td class="campaigns-table__name">
{{ row.name }}
</td>

<td>
<span
class="campaigns-table__status"
:class="`campaigns-table__status--${row.status.toLowerCase()}`"
>
{{ row.status }}
Comment thread
TatevikGr marked this conversation as resolved.
Outdated
</span>
</td>

<td>{{ row.date }}</td>
<td>{{ row.openRate ?? '—' }}</td>
<td>{{ row.clickRate ?? '—' }}</td>
</tr>

<tr v-if="!rows.length">
<td colspan="5" class="campaigns-table__empty">
No campaigns yet.
</td>
</tr>
</tbody>
</table>
</template>

<script setup>
const props = defineProps({
rows: {
type: Array,
default: () => [],
/*
rows: [
{
id: 1,
name: 'Monthly Newsletter - June',
status: 'Sent', // 'Sent' | 'Scheduled' | 'Draft' etc.
date: '2024-06-01',
openRate: '24.5%',
clickRate: '3.2%',
}
]
*/
},
})
</script>

<style scoped>
.campaigns-table {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
}

thead tr {
border-bottom: 1px solid #e5e7eb;
}

th {
text-align: left;
padding: 0.6rem 1rem 0.6rem 0;
font-weight: 600;
color: #9ca3af;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}

th:last-child,
td:last-child {
padding-right: 0;
}

tbody tr {
border-bottom: 1px solid #f3f4f6;
}

td {
padding: 0.65rem 1rem 0.65rem 0;
color: #111827;
}

.campaigns-table__name {
font-weight: 500;
color: #111827;
}

.campaigns-table__status {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.55rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}

/* tweak colors to match your palette */
.campaigns-table__status--sent {
background: #ecfdf3;
color: #15803d;
}

.campaigns-table__status--scheduled {
background: #eff6ff;
color: #1d4ed8;
}

.campaigns-table__status--draft {
background: #f9fafb;
color: #4b5563;
}

.campaigns-table__empty {
text-align: center;
padding: 1.2rem 0;
color: #6b7280;
}
</style>
36 changes: 36 additions & 0 deletions assets/vue/DashboardView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<template>
<DashboardLayout>
<div class="dashboard">
<KpiGrid class="dashboard__section" />

<section class="dashboard__section dashboard__row">
<PerformanceChartCard
class="dashboard__col--2"
:labels="chart.labels"
:series="chart.series"
/>
<SystemOverviewCard class="dashboard__col--1" />
</section>

<section class="dashboard__section">
<RecentCampaignsCard />
</section>
</div>
</DashboardLayout>
</template>

<script setup>
import DashboardLayout from './layouts/DashboardLayout.vue'
import KpiGrid from '/components/dashboard/KpiGrid.vue'
Comment thread
TatevikGr marked this conversation as resolved.
Outdated
import PerformanceChartCard from './components/dashboard/PerformanceChartCard.vue'
import SystemOverviewCard from './components/dashboard/SystemOverviewCard.vue'
import RecentCampaignsCard from './components/dashboard/RecentCampaignsCard.vue'

const chart = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
series: [
{ name: 'Opens', data: [2500, 2200, 10000, 4000, 4500, 3800] },
{ name: 'Clicks', data: [4200, 3500, 3200, 3000, 3600, 4100] },
],
}
</script>
19 changes: 19 additions & 0 deletions assets/vue/PerformanceChartCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<BaseCard class="chart-card">
<header class="chart-card__header">
<h2>Campaign Performance</h2>
<p>Daily opens and clicks for the last 30 days</p>
</header>
<LineChart :series="series" :labels="labels" />
</BaseCard>
</template>

<script setup>
import BaseCard from './components/base/BaseCard.vue'
import LineChart from './components/charts/LineChart.vue'

const props = defineProps({
series: Array,
labels: Array,
})
</script>
Loading
Loading