2022-05-09 13:04:36 +00:00
|
|
|
import { Page } from 'web/components/page'
|
2022-02-14 00:09:30 +00:00
|
|
|
import { Grid, _ as r } from 'gridjs-react'
|
2022-01-16 03:09:15 +00:00
|
|
|
import 'gridjs/dist/theme/mermaid.css'
|
|
|
|
import { html } from 'gridjs'
|
|
|
|
import dayjs from 'dayjs'
|
2022-05-09 13:04:36 +00:00
|
|
|
import { usePrivateUsers, useUsers } from 'web/hooks/use-users'
|
2022-01-16 03:09:15 +00:00
|
|
|
import Custom404 from './404'
|
2022-05-09 13:04:36 +00:00
|
|
|
import { useContracts } from 'web/hooks/use-contracts'
|
2022-05-22 08:36:05 +00:00
|
|
|
import { mapKeys } from 'lodash'
|
2022-05-09 13:04:36 +00:00
|
|
|
import { useAdmin } from 'web/hooks/use-admin'
|
|
|
|
import { contractPath } from 'web/lib/firebase/contracts'
|
2022-07-19 07:50:11 +00:00
|
|
|
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
2022-08-12 04:18:54 +00:00
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
import { getFirstDayProfit } from 'web/lib/firebase/users'
|
2022-07-19 07:50:11 +00:00
|
|
|
|
|
|
|
export const getServerSideProps = redirectIfLoggedOut('/')
|
2022-01-16 03:09:15 +00:00
|
|
|
|
|
|
|
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() {
|
2022-05-26 21:41:24 +00:00
|
|
|
const users = useUsers()
|
|
|
|
const privateUsers = usePrivateUsers()
|
2022-01-24 06:48:06 +00:00
|
|
|
|
|
|
|
// Map private users by user id
|
2022-05-22 08:36:05 +00:00
|
|
|
const privateUsersById = mapKeys(privateUsers, 'id')
|
2022-08-12 04:18:54 +00:00
|
|
|
|
|
|
|
const [profitByUser, setProfitByUser] = useState<{
|
|
|
|
[userId: string]: number
|
|
|
|
}>({})
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
Promise.all(users.map((user) => getFirstDayProfit(user.id))).then(
|
|
|
|
(firstDayProfits) => {
|
|
|
|
setProfitByUser(
|
|
|
|
Object.fromEntries(
|
|
|
|
users.map((user, i) => [
|
|
|
|
user.id,
|
|
|
|
user.profitCached.allTime - firstDayProfits[i],
|
|
|
|
])
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}, [users])
|
2022-01-24 06:48:06 +00:00
|
|
|
|
|
|
|
// For each user, set their email from the PrivateUser
|
2022-05-26 21:41:24 +00:00
|
|
|
const fullUsers = users
|
|
|
|
.map((user) => {
|
2022-08-07 23:12:17 +00:00
|
|
|
return {
|
|
|
|
email: privateUsersById[user.id]?.email,
|
2022-08-12 04:18:54 +00:00
|
|
|
profit: profitByUser[user.id] ?? 0,
|
2022-08-07 23:12:17 +00:00
|
|
|
...user,
|
|
|
|
}
|
2022-05-26 21:41:24 +00:00
|
|
|
})
|
|
|
|
.sort((a, b) => b.createdTime - a.createdTime)
|
2022-01-16 03:09:15 +00:00
|
|
|
|
2022-02-02 04:58:38 +00:00
|
|
|
function exportCsv() {
|
2022-08-07 23:12:17 +00:00
|
|
|
const lines = [['Email', 'Name', 'Balance', 'Profit']].concat(
|
|
|
|
fullUsers.map((u) => [
|
|
|
|
u.email ?? '',
|
|
|
|
u.name,
|
|
|
|
Math.round(u.balance).toString(),
|
|
|
|
Math.round(u.profitCached.allTime).toString(),
|
|
|
|
])
|
|
|
|
)
|
|
|
|
const csv = lines.map((line) => line.join(', ')).join('\n')
|
2022-02-02 04:58:38 +00:00
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
const a = document.createElement('a')
|
|
|
|
a.href = url
|
|
|
|
a.download = 'manifold-users.csv'
|
|
|
|
a.click()
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
}
|
|
|
|
|
2022-01-16 03:09:15 +00:00
|
|
|
return (
|
2022-02-02 04:58:38 +00:00
|
|
|
<>
|
|
|
|
<Grid
|
2022-06-16 19:13:08 +00:00
|
|
|
data={fullUsers}
|
2022-02-02 04:58:38 +00:00
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
id: 'avatarUrl',
|
|
|
|
name: 'Avatar',
|
|
|
|
formatter: (cell) => html(avatarHtml(cell as string)),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'username',
|
|
|
|
name: 'Username',
|
|
|
|
formatter: (cell) =>
|
2022-05-22 08:36:05 +00:00
|
|
|
html(`<a
|
2022-01-16 03:09:15 +00:00
|
|
|
class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
|
|
|
href="/${cell}">@${cell}</a>`),
|
2022-02-02 04:58:38 +00:00
|
|
|
},
|
2022-07-04 20:25:44 +00:00
|
|
|
{
|
|
|
|
id: 'name',
|
|
|
|
name: 'Name',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(`<span class="whitespace-nowrap">${cell}</span>`),
|
|
|
|
},
|
2022-06-16 19:13:08 +00:00
|
|
|
{
|
|
|
|
id: 'email',
|
|
|
|
name: 'Email',
|
|
|
|
},
|
2022-02-02 04:58:38 +00:00
|
|
|
{
|
|
|
|
id: 'createdTime',
|
2022-07-04 20:25:44 +00:00
|
|
|
name: 'Created',
|
2022-02-02 04:58:38 +00:00
|
|
|
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),
|
|
|
|
},
|
2022-08-07 23:12:17 +00:00
|
|
|
{
|
|
|
|
id: 'profit',
|
|
|
|
name: 'profit',
|
|
|
|
formatter: (cell) => (cell as number).toFixed(0),
|
|
|
|
},
|
2022-02-02 04:58:38 +00:00
|
|
|
{
|
|
|
|
id: 'id',
|
|
|
|
name: 'ID',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(`<a
|
2022-01-16 03:09:15 +00:00
|
|
|
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>`),
|
2022-02-02 04:58:38 +00:00
|
|
|
},
|
|
|
|
]}
|
|
|
|
search={true}
|
|
|
|
sort={true}
|
|
|
|
pagination={{
|
|
|
|
enabled: true,
|
|
|
|
limit: 25,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<button className="btn" onClick={exportCsv}>
|
|
|
|
Export emails to CSV
|
|
|
|
</button>
|
|
|
|
</>
|
2022-01-16 03:09:15 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-18 22:10:40 +00:00
|
|
|
function ContractsTable() {
|
2022-05-26 21:41:24 +00:00
|
|
|
const contracts = useContracts() ?? []
|
|
|
|
|
2022-01-18 22:10:40 +00:00
|
|
|
// Sort users by createdTime descending, by default
|
2022-05-26 21:41:24 +00:00
|
|
|
const displayContracts = contracts
|
|
|
|
.sort((a, b) => b.createdTime - a.createdTime)
|
|
|
|
.map((contract) => {
|
|
|
|
// Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
|
|
|
|
const questionLink = r(
|
|
|
|
<div className="w-60">
|
|
|
|
<a
|
|
|
|
className="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
|
|
|
href={contractPath(contract)}
|
|
|
|
>
|
|
|
|
{contract.question}
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
return { questionLink, ...contract }
|
|
|
|
})
|
2022-01-18 22:10:40 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Grid
|
2022-05-26 21:41:24 +00:00
|
|
|
data={displayContracts}
|
2022-01-18 22:10:40 +00:00
|
|
|
columns={[
|
|
|
|
{
|
|
|
|
id: 'creatorUsername',
|
|
|
|
name: 'Username',
|
|
|
|
formatter: (cell) =>
|
2022-05-22 08:36:05 +00:00
|
|
|
html(`<a
|
2022-01-18 22:10:40 +00:00
|
|
|
class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
|
|
|
target="_blank"
|
|
|
|
href="/${cell}">@${cell}</a>`),
|
|
|
|
},
|
|
|
|
{
|
2022-02-14 00:09:30 +00:00
|
|
|
id: 'questionLink',
|
2022-01-18 22:10:40 +00:00
|
|
|
name: 'Question',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'volume24Hours',
|
2022-01-20 06:55:10 +00:00
|
|
|
name: '24h vol',
|
2022-01-18 22:10:40 +00:00
|
|
|
formatter: (cell) => (cell as number).toFixed(0),
|
|
|
|
},
|
2022-01-20 06:55:10 +00:00
|
|
|
{
|
|
|
|
id: 'createdTime',
|
|
|
|
name: 'Created time',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(
|
|
|
|
`<span class="whitespace-nowrap">${dayjs(cell as number).format(
|
|
|
|
'MMM D, h:mma'
|
|
|
|
)}</span>`
|
|
|
|
),
|
|
|
|
},
|
2022-01-18 22:10:40 +00:00
|
|
|
{
|
|
|
|
id: 'closeTime',
|
|
|
|
name: 'Close time',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(
|
|
|
|
`<span class="whitespace-nowrap">${dayjs(cell as number).format(
|
|
|
|
'MMM D, h:mma'
|
|
|
|
)}</span>`
|
|
|
|
),
|
2022-01-20 06:55:10 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'resolvedTime',
|
|
|
|
name: 'Resolved time',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(
|
|
|
|
`<span class="whitespace-nowrap">${dayjs(cell as number).format(
|
|
|
|
'MMM D, h:mma'
|
|
|
|
)}</span>`
|
|
|
|
),
|
2022-01-18 22:10:40 +00:00
|
|
|
},
|
2022-01-18 22:29:49 +00:00
|
|
|
{
|
|
|
|
id: 'visibility',
|
|
|
|
name: 'Visibility',
|
|
|
|
formatter: (cell) => cell,
|
|
|
|
},
|
2022-01-18 22:10:40 +00:00
|
|
|
{
|
|
|
|
id: 'id',
|
|
|
|
name: 'ID',
|
|
|
|
formatter: (cell) =>
|
|
|
|
html(`<a
|
|
|
|
class="hover:underline hover:decoration-indigo-400 hover:decoration-2"
|
|
|
|
target="_blank"
|
|
|
|
href="https://console.firebase.google.com/project/mantic-markets/firestore/data/~2Fcontracts~2F${cell}">${cell}</a>`),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
search={true}
|
|
|
|
sort={true}
|
|
|
|
pagination={{
|
|
|
|
enabled: true,
|
|
|
|
limit: 25,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-01-16 03:09:15 +00:00
|
|
|
export default function Admin() {
|
2022-02-09 18:58:33 +00:00
|
|
|
return useAdmin() ? (
|
2022-03-31 05:35:20 +00:00
|
|
|
<Page>
|
2022-01-16 03:09:15 +00:00
|
|
|
<UsersTable />
|
2022-01-18 22:10:40 +00:00
|
|
|
<ContractsTable />
|
2022-01-16 03:09:15 +00:00
|
|
|
</Page>
|
|
|
|
) : (
|
|
|
|
<Custom404 />
|
|
|
|
)
|
|
|
|
}
|