feat(org): Added admin subscribers page
This commit is contained in:
parent
71b57adaf7
commit
f1e407d789
3 changed files with 157 additions and 18 deletions
|
@ -16,6 +16,23 @@ import { SearchIcon } from 'shared/components/icons.mjs'
|
||||||
// Translation namespaces used on this page
|
// Translation namespaces used on this page
|
||||||
const namespaces = nsMerge(pageNs, authNs)
|
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 AdminPage = ({ page }) => {
|
||||||
const { t } = useTranslation(namespaces)
|
const { t } = useTranslation(namespaces)
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
|
@ -39,25 +56,26 @@ const AdminPage = ({ page }) => {
|
||||||
return (
|
return (
|
||||||
<PageWrapper {...page} title="Administration">
|
<PageWrapper {...page} title="Administration">
|
||||||
<AuthWrapper requiredRole="support">
|
<AuthWrapper requiredRole="support">
|
||||||
<p>
|
<div className="flex flex-row gap-8 items-start w-full">
|
||||||
Other admin links:
|
<div className="grow">
|
||||||
<PageLink href="/admin/csets" txt="Curated measurement sets" />
|
<h5>Search users</h5>
|
||||||
</p>
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<h5>Search users</h5>
|
<input
|
||||||
<div className="flex flex-row gap-2 items-center">
|
autoFocus
|
||||||
<input
|
value={q}
|
||||||
autoFocus
|
onChange={(evt) => setQ(evt.target.value)}
|
||||||
value={q}
|
className="input w-full input-bordered flex flex-row"
|
||||||
onChange={(evt) => setQ(evt.target.value)}
|
type="text"
|
||||||
className="input w-full input-bordered flex flex-row"
|
placeholder="Username, ID, or E-mail address"
|
||||||
type="text"
|
/>
|
||||||
placeholder="Username, ID, or E-mail address"
|
<button onClick={search} className="btn btn-primary">
|
||||||
/>
|
<SearchIcon />
|
||||||
<button onClick={search} className="btn btn-primary">
|
</button>
|
||||||
<SearchIcon />
|
</div>
|
||||||
</button>
|
{loading ? <Loading /> : <Hits {...{ backend, t, results }} />}
|
||||||
|
</div>
|
||||||
|
<AdminMenu />
|
||||||
</div>
|
</div>
|
||||||
{loading ? <Loading /> : <Hits {...{ backend, t, results }} />}
|
|
||||||
</AuthWrapper>
|
</AuthWrapper>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
|
|
113
sites/org/pages/admin/subscribers.mjs
Normal file
113
sites/org/pages/admin/subscribers.mjs
Normal 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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -597,6 +597,14 @@ Backend.prototype.adminImpersonateUser = async function (id) {
|
||||||
return responseHandler(await api.get(`/admin/impersonate/${id}/jwt`, this.auth))
|
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
|
* Verify an admin account while impersonating another user
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue