List users on admin page (#28)
* Admin page using gridjs * Move hook into separate file * Link to each user's Manifold and Firestore /user entry * Gate admin access to Austin/James/Stephen * Don't leak the existence of /admin * Add a custom 404 page that directs to Discord. * Fix broken window.location.href on NextJS server
This commit is contained in:
parent
07709cdccb
commit
279437ba08
12
web/hooks/use-users.ts
Normal file
12
web/hooks/use-users.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { listenForAllUsers, User } from '../lib/firebase/users'
|
||||||
|
|
||||||
|
export const useUsers = () => {
|
||||||
|
const [users, setUsers] = useState<User[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listenForAllUsers(setUsers)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from 'firebase/auth'
|
} from 'firebase/auth'
|
||||||
|
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
|
import { listenForValues } from './utils'
|
||||||
export type { User }
|
export type { User }
|
||||||
|
|
||||||
export const STARTING_BALANCE = 1000
|
export const STARTING_BALANCE = 1000
|
||||||
|
@ -111,3 +112,16 @@ export async function uploadData(
|
||||||
await uploadBytes(uploadRef, data, metadata)
|
await uploadBytes(uploadRef, data, metadata)
|
||||||
return await getDownloadURL(uploadRef)
|
return await getDownloadURL(uploadRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAllUsers() {
|
||||||
|
const userCollection = collection(db, 'users')
|
||||||
|
const q = query(userCollection)
|
||||||
|
const docs = await getDocs(q)
|
||||||
|
return docs.docs.map((doc) => doc.data() as User)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenForAllUsers(setUsers: (users: User[]) => void) {
|
||||||
|
const userCollection = collection(db, 'users')
|
||||||
|
const q = query(userCollection)
|
||||||
|
listenForValues(q, setUsers)
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
"daisyui": "1.16.4",
|
"daisyui": "1.16.4",
|
||||||
"dayjs": "1.10.7",
|
"dayjs": "1.10.7",
|
||||||
"firebase": "9.6.0",
|
"firebase": "9.6.0",
|
||||||
|
"gridjs": "^5.0.2",
|
||||||
|
"gridjs-react": "^5.0.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"next": "12.0.7",
|
"next": "12.0.7",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
|
24
web/pages/404.tsx
Normal file
24
web/pages/404.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useEffect } from 'gridjs'
|
||||||
|
import { Page } from '../components/page'
|
||||||
|
import { Title } from '../components/title'
|
||||||
|
|
||||||
|
export default function Custom404() {
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<Title text="404: Oops!" />
|
||||||
|
<p>Nothing exists at this location.</p>
|
||||||
|
<p>If you didn't expect this, let us know on Discord!</p>
|
||||||
|
<br />
|
||||||
|
<iframe
|
||||||
|
src="https://discord.com/widget?id=915138780216823849&theme=dark"
|
||||||
|
width="350"
|
||||||
|
height="500"
|
||||||
|
allowTransparency={true}
|
||||||
|
frameBorder="0"
|
||||||
|
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import { Page } from '../../components/page'
|
||||||
import { contractTextDetails } from '../../components/contract-card'
|
import { contractTextDetails } from '../../components/contract-card'
|
||||||
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
import { Bet, listAllBets } from '../../lib/firebase/bets'
|
||||||
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
import { Comment, listAllComments } from '../../lib/firebase/comments'
|
||||||
|
import Custom404 from '../404'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: any }) {
|
export async function getStaticProps(props: { params: any }) {
|
||||||
const { username, contractSlug } = props.params
|
const { username, contractSlug } = props.params
|
||||||
|
@ -62,7 +63,7 @@ export default function ContractPage(props: {
|
||||||
const { bets, comments } = props
|
const { bets, comments } = props
|
||||||
|
|
||||||
if (!contract) {
|
if (!contract) {
|
||||||
return <div>Contract not found...</div>
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { creatorId, isResolved, resolution, question } = contract
|
const { creatorId, isResolved, resolution, question } = contract
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Error from 'next/error'
|
|
||||||
|
|
||||||
import { getUserByUsername, User } from '../../lib/firebase/users'
|
import { getUserByUsername, User } from '../../lib/firebase/users'
|
||||||
import { UserPage } from '../../components/user-page'
|
import { UserPage } from '../../components/user-page'
|
||||||
import { useUser } from '../../hooks/use-user'
|
import { useUser } from '../../hooks/use-user'
|
||||||
|
import Custom404 from '../404'
|
||||||
|
|
||||||
export default function UserProfile() {
|
export default function UserProfile() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -18,13 +18,11 @@ export default function UserProfile() {
|
||||||
|
|
||||||
const currentUser = useUser()
|
const currentUser = useUser()
|
||||||
|
|
||||||
const errorMessage = `Who is this "${username}" you speak of..`
|
|
||||||
|
|
||||||
if (user === 'loading') return <></>
|
if (user === 'loading') return <></>
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<UserPage user={user} currentUser={currentUser || undefined} />
|
<UserPage user={user} currentUser={currentUser || undefined} />
|
||||||
) : (
|
) : (
|
||||||
<Error statusCode={404} title={errorMessage} />
|
<Custom404 />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
91
web/pages/admin.tsx
Normal file
91
web/pages/admin.tsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { Page } from '../components/page'
|
||||||
|
import { Grid } from 'gridjs-react'
|
||||||
|
import 'gridjs/dist/theme/mermaid.css'
|
||||||
|
import { html } from 'gridjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useUsers } from '../hooks/use-users'
|
||||||
|
import { useUser } from '../hooks/use-user'
|
||||||
|
import Error from 'next/error'
|
||||||
|
import Custom404 from './404'
|
||||||
|
|
||||||
|
function avatarHtml(avatarUrl: string) {
|
||||||
|
return `<img
|
||||||
|
class="h-10 w-10 rounded-full bg-gray-400 flex items-center justify-center"
|
||||||
|
src="${avatarUrl}"
|
||||||
|
alt=""
|
||||||
|
/>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsersTable() {
|
||||||
|
let users = useUsers()
|
||||||
|
// Sort users by createdTime descending, by default
|
||||||
|
users = users.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
data={users}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
id: 'avatarUrl',
|
||||||
|
name: 'Avatar',
|
||||||
|
formatter: (cell) => html(avatarHtml(cell as string)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'username',
|
||||||
|
name: 'Username',
|
||||||
|
formatter: (cell) =>
|
||||||
|
html(`<a
|
||||||
|
class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
||||||
|
href="/${cell}">@${cell}</a>`),
|
||||||
|
},
|
||||||
|
'Email',
|
||||||
|
{
|
||||||
|
id: 'createdTime',
|
||||||
|
name: 'Created Time',
|
||||||
|
formatter: (cell) =>
|
||||||
|
html(
|
||||||
|
`<span class="whitespace-nowrap">${dayjs(cell as number).format(
|
||||||
|
'MMM D, h:mma'
|
||||||
|
)}</span>`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balance',
|
||||||
|
name: 'Balance',
|
||||||
|
formatter: (cell) => (cell as number).toFixed(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
name: 'ID',
|
||||||
|
formatter: (cell) =>
|
||||||
|
html(`<a
|
||||||
|
class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
||||||
|
href="https://console.firebase.google.com/project/mantic-markets/firestore/data/~2Fusers~2F${cell}">${cell}</a>`),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
search={true}
|
||||||
|
sort={true}
|
||||||
|
pagination={{
|
||||||
|
enabled: true,
|
||||||
|
limit: 25,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Admin() {
|
||||||
|
const user = useUser()
|
||||||
|
const adminIds = [
|
||||||
|
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
|
||||||
|
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
|
||||||
|
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
|
||||||
|
]
|
||||||
|
const isAdmin = adminIds.includes(user?.id || '')
|
||||||
|
return isAdmin ? (
|
||||||
|
<Page wide>
|
||||||
|
<UsersTable />
|
||||||
|
</Page>
|
||||||
|
) : (
|
||||||
|
<Custom404 />
|
||||||
|
)
|
||||||
|
}
|
|
@ -2597,6 +2597,19 @@ graceful-fs@^4.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||||
|
|
||||||
|
gridjs-react@^5.0.2:
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/gridjs-react/-/gridjs-react-5.0.2.tgz#5c561b89a391615fc8242223649e56808fcdd824"
|
||||||
|
integrity sha512-K8bEUL8MuRRbu6o9hCc/bn+H7UfstN0yRIb/UMzVIbJ7ROR0ns9UA63mr4xcMP5keqolCPuLuM1manlH17BZCA==
|
||||||
|
|
||||||
|
gridjs@^5.0.2:
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/gridjs/-/gridjs-5.0.2.tgz#27c4bd7399d07770e68b45bcca20c3b1fea828b6"
|
||||||
|
integrity sha512-7EaMc4/IhqRcSbdOYvHOlHETciWmbbhMU6rDr2e0UfzIXSkb7X3Lhf9+bGv+v4JaWnJW5GBillgIIHrAvIIoFg==
|
||||||
|
dependencies:
|
||||||
|
preact "^10.5.12"
|
||||||
|
tslib "^2.0.1"
|
||||||
|
|
||||||
has-bigints@^1.0.1:
|
has-bigints@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
||||||
|
@ -3771,6 +3784,11 @@ postcss@^8.1.6, postcss@^8.3.7:
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.1"
|
source-map-js "^1.0.1"
|
||||||
|
|
||||||
|
preact@^10.5.12:
|
||||||
|
version "10.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/preact/-/preact-10.6.4.tgz#ad12c409ff1b4316158486e0a7b8d43636f7ced8"
|
||||||
|
integrity sha512-WyosM7pxGcndU8hY0OQlLd54tOU+qmG45QXj2dAYrL11HoyU/EzOSTlpJsirbBr1QW7lICxSsVJJmcmUglovHQ==
|
||||||
|
|
||||||
prelude-ls@^1.2.1:
|
prelude-ls@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
|
@ -4621,7 +4639,7 @@ tslib@^1.8.1, tslib@^1.9.0:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.3, tslib@^2.1.0:
|
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user