1
0
Fork 0

feat(org): Added admin subscribers page

This commit is contained in:
joostdecock 2024-03-23 15:50:24 +01:00
parent 71b57adaf7
commit f1e407d789
3 changed files with 157 additions and 18 deletions

View file

@ -16,6 +16,23 @@ import { SearchIcon } from 'shared/components/icons.mjs'
// Translation namespaces used on this page
const namespaces = nsMerge(pageNs, authNs)
const AdminMenu = () => (
<aside className="max-w-sm">
<h4>Admin Links</h4>
<ul className="list list-inside list-disc ml-4">
<li>
<PageLink href="/admin" txt="Manage Users" />
</li>
<li>
<PageLink href="/admin/csets" txt="Manage curated measurement sets" />
</li>
<li>
<PageLink href="/admin/subscribers" txt="Manage newsletter Subscribers" />
</li>
</ul>
</aside>
)
const AdminPage = ({ page }) => {
const { t } = useTranslation(namespaces)
const backend = useBackend()
@ -39,25 +56,26 @@ const AdminPage = ({ page }) => {
return (
<PageWrapper {...page} title="Administration">
<AuthWrapper requiredRole="support">
<p>
Other admin links:
<PageLink href="/admin/csets" txt="Curated measurement sets" />
</p>
<h5>Search users</h5>
<div className="flex flex-row gap-2 items-center">
<input
autoFocus
value={q}
onChange={(evt) => setQ(evt.target.value)}
className="input w-full input-bordered flex flex-row"
type="text"
placeholder="Username, ID, or E-mail address"
/>
<button onClick={search} className="btn btn-primary">
<SearchIcon />
</button>
<div className="flex flex-row gap-8 items-start w-full">
<div className="grow">
<h5>Search users</h5>
<div className="flex flex-row gap-2 items-center">
<input
autoFocus
value={q}
onChange={(evt) => setQ(evt.target.value)}
className="input w-full input-bordered flex flex-row"
type="text"
placeholder="Username, ID, or E-mail address"
/>
<button onClick={search} className="btn btn-primary">
<SearchIcon />
</button>
</div>
{loading ? <Loading /> : <Hits {...{ backend, t, results }} />}
</div>
<AdminMenu />
</div>
{loading ? <Loading /> : <Hits {...{ backend, t, results }} />}
</AuthWrapper>
</PageWrapper>
)

View file

@ -0,0 +1,113 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge, getSearchParam } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
import { useState, useEffect } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { SearchIcon } from 'shared/components/icons.mjs'
// Translation namespaces used on this page
const ns = nsMerge(pageNs, authNs)
const SubscribersPage = ({ page }) => {
const { t } = useTranslation(ns)
const [subscribers, setSubscribers] = useState()
const [q, setQ] = useState()
const [hits, setHits] = useState([])
const backend = useBackend()
const loadSubscribers = async () => {
const result = await backend.adminLoadSubscribers()
if (result.success) setSubscribers(result.data.subscribers)
}
const search = async () => {
if (!subscribers) await loadSubscribers()
const found = []
for (const lang in subscribers) {
found.push(
...subscribers[lang]
.filter((sub) => sub.email.toLowerCase().includes(q.toLowerCase()))
.map((sub) => ({ ...sub, lang }))
)
}
setHits(found)
}
const unsubscribe = async (ehash) => {
await backend.newsletterUnsubscribe(ehash)
await loadSubscribers()
await search()
}
return (
<PageWrapper {...page} title="Subscribers">
<AuthWrapper requiredRole="admin">
{subscribers ? (
<>
<h5>Search subscribers</h5>
<div className="flex flex-row gap-2 items-center">
<input
autoFocus
value={q}
onChange={(evt) => setQ(evt.target.value)}
className="input w-full input-bordered flex flex-row"
type="text"
placeholder="Username, ID, or E-mail address"
/>
<button onClick={search} className="btn btn-primary">
<SearchIcon />
</button>
</div>
<table className="table">
<thead>
<tr>
<th className="text-right">Email</th>
<th className="w=12">Language</th>
<th>Unsubscribe</th>
</tr>
</thead>
<tbody>
{hits.map((hit, i) => (
<tr key={i}>
<td className="text-right">
<b>{hit.email}</b>
</td>
<td className="w-12">{hit.lang.toUpperCase()}</td>
<td className="w-full">
<button className="btn btn-link" onClick={() => unsubscribe(hit.ehash)}>
Unsubscribe
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
) : (
<button className="btn btn-primary btn-lg" onClick={loadSubscribers}>
Load Subscribers
</button>
)}
</AuthWrapper>
</PageWrapper>
)
}
export default SubscribersPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['admin', 'subscribers'],
},
},
}
}

View file

@ -597,6 +597,14 @@ Backend.prototype.adminImpersonateUser = async function (id) {
return responseHandler(await api.get(`/admin/impersonate/${id}/jwt`, this.auth))
}
/*
* Load newsletter subscribers (admin method)
*/
Backend.prototype.adminLoadSubscribers = async function () {
console.log(this.auth)
return responseHandler(await api.get(`/admin/subscribers/jwt`, this.auth))
}
/*
* Verify an admin account while impersonating another user
*/