Skip to content

Commit 0bfde9d

Browse files
committed
Subscribers pass data
1 parent 183507f commit 0bfde9d

File tree

7 files changed

+139
-69
lines changed

7 files changed

+139
-69
lines changed

assets/app.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ import App from './vue/App.vue';
44
import { router } from './router';
55

66
const appElement = document.getElementById('vue-app');
7+
78
if (appElement) {
89
const app = createApp(App);
910
app.use(router);
1011
app.mount('#vue-app');
1112
}
1213

14+
const subscribersElement = document.getElementById('vue-subscribers');
15+
16+
if (subscribersElement) {
17+
const app = createApp(App);
18+
app.use(router);
19+
app.provide('subscribers', JSON.parse(subscribersElement.dataset.subscribers))
20+
app.provide('pagination', JSON.parse(subscribersElement.dataset.pagination))
21+
app.mount('#vue-subscribers');
22+
}
23+

assets/vue/components/subscribers/SubscriberDirectory.vue

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,72 @@
2323
</div>
2424
<SubscriberFilters />
2525
</div>
26-
<SubscriberTable />
26+
<SubscriberTable :subscribers="subscribers" />
27+
<div class="p-6 border-t border-slate-200 flex justify-between items-center text-sm text-slate-500">
28+
<div>
29+
Showing <span class="font-medium text-slate-900">{{ subscribers.length }}</span> of <span class="font-medium text-slate-900">{{ pagination.total }}</span> subscribers
30+
</div>
31+
<div class="flex gap-2">
32+
<button
33+
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
34+
:disabled="pagination.isFirstPage"
35+
@click="previousPage"
36+
>
37+
Previous
38+
</button>
39+
<button
40+
class="px-4 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
41+
:disabled="!pagination.hasMore"
42+
@click="nextPage"
43+
>
44+
Next
45+
</button>
46+
</div>
47+
</div>
2748
</div>
2849
</template>
2950

3051
<script setup>
3152
import BaseIcon from '../base/BaseIcon.vue'
3253
import SubscriberFilters from './SubscriberFilters.vue'
3354
import SubscriberTable from './SubscriberTable.vue'
55+
import { inject, ref } from 'vue'
56+
57+
const initialSubscribers = inject('subscribers')
58+
const initialPagination = inject('pagination')
59+
60+
const subscribers = ref(initialSubscribers)
61+
const pagination = ref(initialPagination)
62+
63+
const fetchSubscribers = async (afterId = null) => {
64+
const url = new URL('/subscribers', window.location.origin)
65+
if (afterId !== null) {
66+
url.searchParams.append('after_id', afterId)
67+
}
68+
69+
try {
70+
const response = await fetch(url, {
71+
headers: {
72+
'Accept': 'application/json'
73+
}
74+
})
75+
const data = await response.json()
76+
subscribers.value = data.items
77+
pagination.value = data.pagination
78+
} catch (error) {
79+
console.error('Failed to fetch subscribers:', error)
80+
}
81+
}
82+
83+
const nextPage = () => {
84+
if (pagination.value.hasMore) {
85+
fetchSubscribers(pagination.value.afterId)
86+
}
87+
}
88+
89+
const previousPage = () => {
90+
if (!pagination.value.isFirstPage) {
91+
fetchSubscribers(pagination.value.prevId === 0 ? null : pagination.value.prevId)
92+
}
93+
}
3494
</script>

assets/vue/components/subscribers/SubscriberFilters.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ const activeFilter = ref('all')
2323
2424
const filters = [
2525
{ id: 'all', label: 'All' },
26-
{ id: 'active', label: 'Active' },
2726
{ id: 'unconfirmed', label: 'Unconfirmed' },
28-
{ id: 'bounced', label: 'Bounced' },
29-
{ id: 'unsubscribed', label: 'Unsubscribed' },
27+
{ id: 'blacklisted', label: 'Blacklisted' },
28+
{ id: 'confirmed', label: 'Confirmed' },
29+
{ id: 'non-blacklisted', label: 'Non-Blacklisted' },
3030
]
3131
</script>

assets/vue/components/subscribers/SubscriberTable.vue

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,45 @@
33
<table class="w-full text-left text-sm">
44
<thead class="bg-slate-50 text-slate-500 font-medium">
55
<tr>
6+
<th class="px-6 py-4">ID</th>
67
<th class="px-6 py-4">Email</th>
7-
<th class="px-6 py-4">Name</th>
88
<th class="px-6 py-4">Status</th>
99
<th class="px-6 py-4 text-right">Lists</th>
10-
<th class="px-6 py-4 text-right">Opens</th>
11-
<th class="px-6 py-4">Joined</th>
10+
<th class="px-6 py-4">Created</th>
1211
<th class="px-6 py-4 w-10"></th>
1312
</tr>
1413
</thead>
1514
<tbody class="divide-y divide-slate-200">
1615
<tr
17-
v-for="subscriber in subscribers"
18-
:key="subscriber.email"
16+
v-for="subscriber in props.subscribers"
17+
:key="subscriber.id"
1918
class="hover:bg-slate-50 transition-colors group"
2019
>
20+
<td class="px-6 py-4 text-slate-600 ">
21+
{{ subscriber.id }}
22+
</td>
2123
<td class="px-6 py-4 font-mono text-sm text-slate-900">
2224
{{ subscriber.email }}
2325
</td>
24-
<td class="px-6 py-4 text-slate-600 ">
25-
{{ subscriber.name }}
26-
</td>
2726
<td class="px-6 py-4">
2827
<span
2928
class="px-2.5 py-0.5 rounded-full text-xs font-medium"
30-
:class="statusClasses[subscriber.status]"
29+
:class="subscriber.confirmed ? statusClasses.active : statusClasses.unconfirmed"
3130
>
32-
{{ subscriber.status }}
31+
{{ subscriber.confirmed ? 'Confirmed' : 'Unconfirmed' }}
32+
</span>
33+
<span
34+
v-if="subscriber.blacklisted"
35+
class="ml-2 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700"
36+
>
37+
Blacklisted
3338
</span>
3439
</td>
3540
<td class="px-6 py-4 text-right text-slate-600">
36-
{{ subscriber.lists }}
37-
</td>
38-
<td class="px-6 py-4 text-right text-slate-600">
39-
{{ subscriber.opens }}
41+
{{ subscriber.listCount }}
4042
</td>
4143
<td class="px-6 py-4 text-slate-600">
42-
{{ subscriber.joined }}
44+
{{ new Date(subscriber.createdAt.date).toLocaleDateString() }}
4345
</td>
4446
<td class="px-6 py-4 text-right">
4547
<button class="text-slate-400 hover:text-slate-600">
@@ -54,6 +56,7 @@
5456

5557
<script setup>
5658
import BaseIcon from '../base/BaseIcon.vue'
59+
import { inject } from 'vue'
5760
5861
const statusClasses = {
5962
active: 'bg-emerald-100 text-emerald-700',
@@ -62,46 +65,10 @@ const statusClasses = {
6265
unsubscribed: 'bg-slate-100 text-slate-600',
6366
}
6467
65-
const subscribers = [
66-
{
67-
email: 'alice.johnson@example.com',
68-
name: 'Alice Johnson',
69-
status: 'active',
70-
lists: 3,
71-
opens: 45,
72-
joined: '2023-01-15',
73-
},
74-
{
75-
email: 'bob.smith@example.com',
76-
name: 'Bob Smith',
77-
status: 'active',
78-
lists: 2,
79-
opens: 32,
80-
joined: '2023-02-20',
81-
},
82-
{
83-
email: 'carol.williams@example.com',
84-
name: 'Carol Williams',
85-
status: 'unconfirmed',
86-
lists: 1,
87-
opens: 0,
88-
joined: '2023-03-10',
89-
},
90-
{
91-
email: 'david.brown@example.com',
92-
name: 'David Brown',
93-
status: 'bounced',
94-
lists: 2,
95-
opens: 28,
96-
joined: '2022-12-05',
97-
},
98-
{
99-
email: 'emma.davis@example.com',
100-
name: 'Emma Davis',
101-
status: 'active',
102-
lists: 4,
103-
opens: 67,
104-
joined: '2023-01-22',
105-
},
106-
]
68+
const props = defineProps({
69+
subscribers: {
70+
type: Array,
71+
required: true
72+
}
73+
})
10774
</script>

assets/vue/views/SubscribersView.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@
1717
import AdminLayout from '../layouts/AdminLayout.vue'
1818
import SubscriberDirectory from '../components/subscribers/SubscriberDirectory.vue'
1919
import ActivitySidebar from '../components/subscribers/ActivitySidebar.vue'
20+
import { inject } from 'vue'
21+
22+
const subscribers = inject('subscribers')
23+
const pagination = inject('pagination')
2024
</script>

src/Controller/SubscribersController.php

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,59 @@ public function __construct(private readonly SubscribersClient $subscribersClien
2020
#[Route('/subscribers', name: 'subscribers', methods: ['GET'])]
2121
public function index(Request $request): Response
2222
{
23-
$afterId = (int) $request->query->get('after_id');
24-
$limit = max(1, (int) $request->query->get('limit', 25));
23+
$afterId = $request->query->get('after_id') !== null ? (int) $request->query->get('after_id') : null;
24+
$limit = max(1, (int) $request->query->get('limit', 10));
2525

26-
/** @var $collection*/
2726
$collection = $this->subscribersClient->getSubscribers($afterId, $limit);
2827

28+
$history = $request->getSession()->get('subscribers_history', []);
29+
if ($afterId === null) {
30+
$history = [];
31+
}
32+
if ($afterId !== null && !in_array($afterId, $history, true)) {
33+
$history[] = $afterId;
34+
$request->getSession()->set('subscribers_history', $history);
35+
}
36+
37+
$prevId = null;
38+
if ($afterId !== null) {
39+
$index = array_search($afterId, $history, true);
40+
if ($index === 0) {
41+
$prevId = 0;
42+
} elseif ($index > 0) {
43+
$prevId = $history[$index - 1];
44+
}
45+
}
46+
2947
$initialData = [
3048
'items' => array_map(static function (Subscriber $subscriber) {
3149
return [
3250
'id' => $subscriber->id,
3351
'email' => $subscriber->email,
3452
'confirmed' => $subscriber->confirmed,
3553
'blacklisted' => $subscriber->blacklisted,
54+
'createdAt' => $subscriber->createdAt,
3655
'uniqueId' => $subscriber->uniqueId,
56+
'listCount' => count($subscriber->subscribedLists),
3757
];
3858
}, $collection->items ?? []),
3959
'pagination' => [
4060
'limit' => $collection->pagination->limit,
4161
'afterId' => $collection->pagination->nextCursor,
4262
'hasMore' => $collection->pagination->hasMore ,
4363
'total' => $collection->pagination->total,
64+
'prevId' => $prevId,
65+
'isFirstPage' => $afterId === null || $afterId === 0,
4466
],
4567
];
4668

69+
if ($request->isXmlHttpRequest() || $request->headers->get('Accept') === 'application/json') {
70+
return $this->json($initialData);
71+
}
72+
4773
return $this->render('subscribers/index.html.twig', [
48-
'initial_subscribers' => $initialData,
74+
'subscribers' => $initialData['items'],
75+
'pagination' => $initialData['pagination'],
4976
]);
5077
}
5178
}

templates/subscribers/index.html.twig

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
{% block title %}phpList - Subscribers{% endblock %}
44

55
{% block body %}
6-
<div id="vue-app"></div>
6+
<div id="vue-subscribers"
7+
data-subscribers="{{ subscribers|json_encode|e('html_attr') }}"
8+
data-pagination="{{ pagination|json_encode|e('html_attr') }}"
9+
>
10+
></div>
711

8-
<script>
9-
window.__INITIAL_SUBSCRIBERS__ = {{ initial_subscribers|json_encode(constant('JSON_UNESCAPED_UNICODE') b-or constant('JSON_UNESCAPED_SLASHES'))|raw }};
10-
</script>
1112
{% endblock %}

0 commit comments

Comments
 (0)