Allow admins to edit questions

This commit is contained in:
Austin Chen 2022-02-09 10:58:33 -08:00
parent b0a1da62d2
commit bcc011c1fd
4 changed files with 104 additions and 66 deletions

View File

@ -23,6 +23,7 @@ service cloud.firestore {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'tags', 'lowercaseTags']); .hasOnly(['description', 'tags', 'lowercaseTags']);
allow update: if isAdmin();
allow delete: if resource.data.creatorId == request.auth.uid; allow delete: if resource.data.creatorId == request.auth.uid;
} }

View File

@ -43,6 +43,7 @@ import { fromNow } from '../lib/util/time'
import BetRow from './bet-row' import BetRow from './bet-row'
import { parseTags } from '../../common/util/parse' import { parseTags } from '../../common/util/parse'
import { Avatar } from './avatar' import { Avatar } from './avatar'
import { useAdmin } from '../hooks/use-admin'
function FeedComment(props: { function FeedComment(props: {
activityItem: any activityItem: any
@ -154,21 +155,75 @@ function FeedBet(props: { activityItem: any }) {
) )
} }
function EditContract(props: {
text: string
onSave: (newText: string) => void
buttonText: string
}) {
const [text, setText] = useState(props.text)
const [editing, setEditing] = useState(false)
const onSave = (newText: string) => {
setEditing(false)
setText(props.text) // Reset to original text
props.onSave(newText)
}
return editing ? (
<div className="mt-4">
<Textarea
className="textarea h-24 textarea-bordered w-full mb-1"
rows={3}
value={text}
onChange={(e) => setText(e.target.value || '')}
autoFocus
onFocus={(e) =>
// Focus starts at end of text.
e.target.setSelectionRange(text.length, text.length)
}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.ctrlKey) {
onSave(text)
}
}}
/>
<Row className="gap-2">
<button
className="btn btn-neutral btn-outline btn-sm"
onClick={() => onSave(text)}
>
Save
</button>
<button
className="btn btn-error btn-outline btn-sm"
onClick={() => setEditing(false)}
>
Cancel
</button>
</Row>
</div>
) : (
<Row>
<button
className="btn btn-neutral btn-outline btn-sm mt-4"
onClick={() => setEditing(true)}
>
{props.buttonText}
</button>
</Row>
)
}
export function ContractDescription(props: { export function ContractDescription(props: {
contract: Contract contract: Contract
isCreator: boolean isCreator: boolean
}) { }) {
const { contract, isCreator } = props const { contract, isCreator } = props
const [editing, setEditing] = useState(false) const descriptionTimestamp = () => `${dayjs().format('MMM D, h:mma')}: `
const editStatement = () => `${dayjs().format('MMM D, h:mma')}: ` const isAdmin = useAdmin()
const [description, setDescription] = useState(editStatement())
// Append the new description (after a newline) // Append the new description (after a newline)
async function saveDescription(e: any) { async function saveDescription(newText: string) {
e.preventDefault() const newDescription = `${contract.description}\n\n${newText}`.trim()
setEditing(false)
const newDescription = `${contract.description}\n\n${description}`.trim()
const tags = parseTags( const tags = parseTags(
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` `${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
) )
@ -178,8 +233,6 @@ export function ContractDescription(props: {
tags, tags,
lowercaseTags, lowercaseTags,
}) })
setDescription(editStatement())
} }
if (!isCreator && !contract.description.trim()) return null if (!isCreator && !contract.description.trim()) return null
@ -188,53 +241,32 @@ export function ContractDescription(props: {
<div className="whitespace-pre-line break-words mt-2 text-gray-700"> <div className="whitespace-pre-line break-words mt-2 text-gray-700">
<Linkify text={contract.description} /> <Linkify text={contract.description} />
<br /> <br />
{isCreator && {isCreator && (
(editing ? ( <EditContract
<form className="mt-4"> // Note: Because descriptionTimestamp is called once, later edits use
<Textarea // a stale timestamp. Ideally this is a function that gets called when
className="textarea h-24 textarea-bordered w-full mb-1" // isEditing is set to true.
rows={3} text={descriptionTimestamp()}
value={description} onSave={saveDescription}
onChange={(e) => setDescription(e.target.value || '')} buttonText="Add to description"
autoFocus />
onFocus={(e) => )}
// Focus starts at end of description. {isAdmin && (
e.target.setSelectionRange( <EditContract
description.length, text={contract.question}
description.length onSave={(question) => updateContract(contract.id, { question })}
) buttonText="ADMIN: Edit question"
} />
onKeyDown={(e) => { )}
if (e.key === 'Enter' && e.ctrlKey) { {/* {isAdmin && (
saveDescription(e) <EditContract
} text={contract.createdTime.toString()}
}} onSave={(time) =>
/> updateContract(contract.id, { createdTime: Number(time) })
<Row className="gap-2"> }
<button buttonText="ADMIN: Edit createdTime"
className="btn btn-neutral btn-outline btn-sm" />
onClick={saveDescription} )} */}
>
Save
</button>
<button
className="btn btn-error btn-outline btn-sm"
onClick={() => setEditing(false)}
>
Cancel
</button>
</Row>
</form>
) : (
<Row>
<button
className="btn btn-neutral btn-outline btn-sm mt-4"
onClick={() => setEditing(true)}
>
Add to description
</button>
</Row>
))}
</div> </div>
) )
} }

12
web/hooks/use-admin.ts Normal file
View File

@ -0,0 +1,12 @@
import { useUser } from './use-user'
export const useAdmin = () => {
const user = useUser()
const adminIds = [
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // Manifold
]
return adminIds.includes(user?.id || '')
}

View File

@ -8,6 +8,7 @@ import { useUser } from '../hooks/use-user'
import Custom404 from './404' import Custom404 from './404'
import { useContracts } from '../hooks/use-contracts' import { useContracts } from '../hooks/use-contracts'
import _ from 'lodash' import _ from 'lodash'
import { useAdmin } from '../hooks/use-admin'
function avatarHtml(avatarUrl: string) { function avatarHtml(avatarUrl: string) {
return `<img return `<img
@ -190,15 +191,7 @@ function ContractsTable() {
} }
export default function Admin() { export default function Admin() {
const user = useUser() return useAdmin() ? (
const adminIds = [
'igi2zGXsfxYPgB0DJTXVJVmwCOr2', // Austin
'5LZ4LgYuySdL1huCWe7bti02ghx2', // James
'tlmGNz9kjXc2EteizMORes4qvWl2', // Stephen
'IPTOzEqrpkWmEzh6hwvAyY9PqFb2', // Manifold
]
const isAdmin = adminIds.includes(user?.id || '')
return isAdmin ? (
<Page wide> <Page wide>
<UsersTable /> <UsersTable />
<ContractsTable /> <ContractsTable />