wip(org): Working on admin & content creation pages
This commit is contained in:
parent
61da3e1dab
commit
50b6747584
26 changed files with 1221 additions and 3383 deletions
200
sites/shared/components/admin.mjs
Normal file
200
sites/shared/components/admin.mjs
Normal file
|
@ -0,0 +1,200 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { cloudflareConfig } from 'shared/config/cloudflare.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { formatNumber } from 'shared/utils.mjs'
|
||||
import Link from 'next/link'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { Collapse } from 'shared/components/collapse.mjs'
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import { Json } from 'shared/components/json.mjs'
|
||||
import { AccountRole } from 'shared/components/account/role.mjs'
|
||||
import { AccountStatus } from 'shared/components/account/status.mjs'
|
||||
import { Loading } from 'shared/components/spinner.mjs'
|
||||
|
||||
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
const roles = ['user', 'curator', 'bughunter', 'support', 'admin']
|
||||
|
||||
export const ImpersonateButton = ({ userId }) => {
|
||||
const toast = useToast()
|
||||
const backend = useBackend()
|
||||
const { impersonate } = useAccount()
|
||||
|
||||
if (!userId) return null
|
||||
|
||||
const impersonateUser = async () => {
|
||||
const result = await backend.adminImpersonateUser(userId)
|
||||
if (result.success) {
|
||||
impersonate(result.data)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="btn btn-primary" onClick={impersonateUser}>
|
||||
Impersonate
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const Row = ({ title, val }) => (
|
||||
<tr className="py-1">
|
||||
<td className="text-sm px-2 text-right font-bold">{title}</td>
|
||||
<td className="text-sm">{val}</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
export const Hits = ({ results, t }) => (
|
||||
<>
|
||||
{results && results.username && results.username.length > 0 && (
|
||||
<>
|
||||
<h2>Results based on username</h2>
|
||||
{results.username.map((user) => (
|
||||
<User user={user} key={user.id} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{results && results.email && results.email.length > 0 && (
|
||||
<>
|
||||
<h2>Results based on E-mail address</h2>
|
||||
{results.email.map((user) => (
|
||||
<User user={user} key={user.id} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
export const ShowUser = ({ user, button = null }) => (
|
||||
<div className="flex flex-row w-full gap-4">
|
||||
<div
|
||||
className="w-52 h-52 bg-base-100 rounded-lg shadow shrink-0"
|
||||
style={{
|
||||
backgroundImage: `url(${cloudflareConfig.url}${user.img}/sq500)`,
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
></div>
|
||||
<div className="w-full">
|
||||
<h6 className="flex flex-row items-center gap-2 flex-wrap">
|
||||
{user.username}
|
||||
<span className="font-light">|</span>
|
||||
<AccountRole role={user.role} />
|
||||
<span className="font-light">|</span>
|
||||
<AccountStatus status={user.status} />
|
||||
<span className="font-light">|</span>
|
||||
{user.id}
|
||||
</h6>
|
||||
<div className="flex flex-row flex-wrap gap-4 w-full">
|
||||
<div className="max-w-xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<Row title="Email" val={user.email} />
|
||||
<Row title="Initial" val={user.initial} />
|
||||
<Row title="GitHub" val={user.github} />
|
||||
<Row title="MFA" val={user.mfaEnabled ? 'Yes' : 'No'} />
|
||||
<Row title="Passhash" val={user.passwordType} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="max-w-xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<Row title="Patron" val={user.patron} />
|
||||
<Row title="Consent" val={user.consent} />
|
||||
<Row title="Control" val={user.control} />
|
||||
<Row title="Calls (jwt)" val={user.jwtCalls} />
|
||||
<Row title="Calls (key)" val={user.keyCalls} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="max-w-xs flex flex-col gap-2">{button}</div>
|
||||
</div>
|
||||
<div className="max-w-full truncate">
|
||||
<Markdown>{user.bio}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const User = ({ user }) => (
|
||||
<div className="my-8">
|
||||
<ShowUser
|
||||
user={user}
|
||||
button={
|
||||
<Link href={`/admin/user/${user.id}`} className="btn btn-primary">
|
||||
Manage user
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ManageUser = ({ userId }) => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const toast = useToast()
|
||||
|
||||
// State
|
||||
const [user, setUser] = useState({})
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
const result = await backend.adminLoadUser(userId)
|
||||
if (result.success) setUser(result.data.user)
|
||||
}
|
||||
loadUser()
|
||||
}, [userId])
|
||||
|
||||
const updateUser = async (data) => {
|
||||
const result = await backend.adminUpdateUser({ id: userId, data })
|
||||
if (result.success) {
|
||||
toast.for.settingsSaved()
|
||||
setUser(result.data.user)
|
||||
} else toast.for.backendError()
|
||||
}
|
||||
|
||||
return user.id ? (
|
||||
<div className="my-8">
|
||||
<ShowUser user={user} button={<ImpersonateButton userId={user.id} />} />
|
||||
<div className="flex flex-row flex-wrap gap-2 my-2">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
className="btn btn-primary btn-outline btn-sm"
|
||||
onClick={() => updateUser({ role })}
|
||||
disabled={role === user.role}
|
||||
>
|
||||
Assign {role} role
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap gap-2 my-2">
|
||||
{user.mfaEnabled && (
|
||||
<button
|
||||
className="btn btn-warning btn-outline btn-sm"
|
||||
onClick={() => updateUser({ mfaEnabled: false })}
|
||||
>
|
||||
Disable MFA
|
||||
</button>
|
||||
)}
|
||||
{Object.keys(freeSewingConfig.statuses).map((status) => (
|
||||
<button
|
||||
className="btn btn-warning btn-outline btn-sm"
|
||||
onClick={() => updateUser({ status })}
|
||||
disabled={Number(status) === user.status}
|
||||
>
|
||||
Set {freeSewingConfig.statuses[status].name.toUpperCase()} status
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{user.id ? <Json js={user} /> : null}
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue