client/server: clean-up admin page, re-implement user deletion/role toggling

This commit is contained in:
Max Leiter 2022-04-19 23:36:56 -07:00
parent 3d2bec0d5e
commit be6de7c796
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
8 changed files with 145 additions and 114 deletions

View file

@ -0,0 +1,43 @@
import { Popover, Button } from "@geist-ui/core"
import { MoreVertical } from "@geist-ui/icons"
type Action = {
title: string
onClick: () => void
}
const ActionDropdown = ({
title = "Actions",
actions,
showTitle = false,
}: {
title?: string,
showTitle?: boolean,
actions: Action[]
}) => {
return (
<Popover
title={title}
content={
<>
{showTitle && <Popover.Item title>
{title}
</Popover.Item>}
{actions.map(action => (
<Popover.Item
onClick={action.onClick}
key={action.title}
>
{action.title}
</Popover.Item>
))}
</>
}
hideArrow
>
<Button iconRight={<MoreVertical />} auto></Button>
</Popover>
)
}
export default ActionDropdown

View file

@ -1,11 +1,11 @@
import { Button, Fieldset, Link, Popover, useToasts } from "@geist-ui/core"
import MoreVertical from "@geist-ui/icons/moreVertical"
import SettingsGroup from "@components/settings-group"
import { Fieldset, useToasts } from "@geist-ui/core"
import byteToMB from "@lib/byte-to-mb"
import { Post } from "@lib/types"
import Cookies from "js-cookie"
import Table from "rc-table"
import { useEffect, useMemo, useState } from "react"
import { adminFetcher } from "."
import Table from "rc-table"
import byteToMB from "@lib/byte-to-mb"
import ActionDropdown from "./action-dropdown"
const PostTable = () => {
const [posts, setPosts] = useState<Post[]>()
@ -35,8 +35,8 @@ const PostTable = () => {
visibility: post.visibility,
size: post.files
? byteToMB(
post.files.reduce((acc, file) => acc + file.html.length, 0)
)
post.files.reduce((acc, file) => acc + file.html.length, 0)
)
: 0,
actions: ""
}
@ -44,27 +44,34 @@ const PostTable = () => {
[posts]
)
// const deletePost = async (id: number) => {
// const confirm = window.confirm("Are you sure you want to delete this post?")
// if (!confirm) return
// const res = await adminFetcher(`/posts/${id}`, {
// method: "DELETE",
// })
const deletePost = async (/* id: string */) => {
return alert("Not implemented")
// const json = await res.json()
// const confirm = window.confirm("Are you sure you want to delete this post?")
// if (!confirm) return
// const res = await adminFetcher(`/posts/${id}`, {
// method: "DELETE",
// })
// if (res.status === 200) {
// setToast({
// text: "Post deleted",
// type: "success"
// })
// } else {
// setToast({
// text: json.error || "Something went wrong",
// type: "error"
// })
// }
// }
// const json = await res.json()
// if (res.status === 200) {
// setToast({
// text: "Post deleted",
// type: "success"
// })
// setPosts((posts) => {
// const newPosts = posts?.filter((post) => post.id !== id)
// return newPosts
// })
// } else {
// setToast({
// text: json.error || "Something went wrong",
// type: "error"
// })
// }
}
const tableColumns = [
{
@ -102,37 +109,28 @@ const PostTable = () => {
dataIndex: "",
key: "actions",
width: 50,
render() {
render(post: Post) {
return (
<Popover
content={
<div
style={{
width: 100,
display: "flex",
flexDirection: "column",
alignItems: "center"
}}
>
{/* <Link href="#" onClick={() => deletePost(post.id)}>Delete post</Link> */}
</div>
}
hideArrow
>
<Button iconRight={<MoreVertical />} auto></Button>
</Popover>
<ActionDropdown
title="Actions"
actions={[
{
title: "Delete",
onClick: () => deletePost(post.id)
}
]}
/>
)
}
}
]
return (
<Fieldset>
<Fieldset.Title>Posts</Fieldset.Title>
{posts && <Fieldset.Subtitle>{posts.length} posts</Fieldset.Subtitle>}
<SettingsGroup title="Posts">
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{posts && <Fieldset.Subtitle><h5>{posts.length} posts</h5></Fieldset.Subtitle>}
{posts && <Table columns={tableColumns} data={tablePosts} />}
</Fieldset>
</SettingsGroup>
)
}

View file

@ -1,10 +1,10 @@
import { Button, Fieldset, Link, Popover, useToasts } from "@geist-ui/core"
import MoreVertical from "@geist-ui/icons/moreVertical"
import { Fieldset, useToasts } from "@geist-ui/core"
import { User } from "@lib/types"
import Cookies from "js-cookie"
import { useEffect, useMemo, useState } from "react"
import { adminFetcher } from "."
import Table from "rc-table"
import SettingsGroup from "@components/settings-group"
import ActionDropdown from "./action-dropdown"
const UserTable = () => {
const [users, setUsers] = useState<User[]>()
@ -19,7 +19,7 @@ const UserTable = () => {
fetchUsers()
}, [])
const toggleRole = async (id: number, role: "admin" | "user") => {
const toggleRole = async (id: string, role: "admin" | "user") => {
const res = await adminFetcher("/users/toggle-role", {
method: "POST",
body: { id, role }
@ -33,17 +33,18 @@ const UserTable = () => {
type: "success"
})
// TODO: swr should handle updating this
const newUsers = users?.map((user) => {
if (user.id === id.toString()) {
return {
...user,
role: role === "admin" ? "user" : ("admin" as User["role"])
setUsers((users) => {
const newUsers = users?.map((user) => {
if (user.id === id) {
return {
...user,
role
}
}
}
return user
return user
})
return newUsers
})
setUsers(newUsers)
} else {
setToast({
text: json.error || "Something went wrong",
@ -52,7 +53,7 @@ const UserTable = () => {
}
}
const deleteUser = async (id: number) => {
const deleteUser = async (id: string) => {
const confirm = window.confirm("Are you sure you want to delete this user?")
if (!confirm) return
const res = await adminFetcher(`/users/${id}`, {
@ -123,42 +124,32 @@ const UserTable = () => {
dataIndex: "",
key: "actions",
width: 50,
render(user: any) {
render(user: User) {
return (
<Popover
content={
<div
style={{
width: 100,
display: "flex",
flexDirection: "column",
alignItems: "center"
}}
>
<Link href="#" onClick={() => toggleRole(user.id, user.role)}>
{user.role === "admin" ? "Change role" : "Make admin"}
</Link>
<Link href="#" onClick={() => deleteUser(user.id)}>
Delete user
</Link>
</div>
}
hideArrow
>
<Button iconRight={<MoreVertical />} auto></Button>
</Popover>
<ActionDropdown
title="Actions"
actions={[
{
title: user.role === "admin" ? "Change role" : "Make admin",
onClick: () => toggleRole(user.id, user.role === "admin" ? "user" : "admin")
},
{
title: "Delete",
onClick: () => deleteUser(user.id)
}
]}
/>
)
}
}
]
return (
<Fieldset>
<Fieldset.Title>Users</Fieldset.Title>
{users && <Fieldset.Subtitle>{users.length} users</Fieldset.Subtitle>}
<SettingsGroup title="Users">
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{users && <Fieldset.Subtitle><h5>{users.length} users</h5></Fieldset.Subtitle>}
{users && <Table columns={usernameColumns} data={tableUsers} />}
</Fieldset>
</SettingsGroup>
)
}

View file

@ -1,12 +1,12 @@
import { Button, Link, Text, Popover } from "@geist-ui/core"
import FileIcon from "@geist-ui/icons/fileText"
import CodeIcon from "@geist-ui/icons/fileFunction"
import styles from "./dropdown.module.css"
import { useCallback, useEffect, useRef, useState } from "react"
import { codeFileExtensions } from "@lib/constants"
import ChevronDown from "@geist-ui/icons/chevronDown"
import ShiftBy from "@components/shift-by"
import { Button, Popover } from "@geist-ui/core"
import ChevronDown from "@geist-ui/icons/chevronDown"
import CodeIcon from "@geist-ui/icons/fileFunction"
import FileIcon from "@geist-ui/icons/fileText"
import { codeFileExtensions } from "@lib/constants"
import type { File } from "@lib/types"
import { useCallback, useEffect, useState } from "react"
import styles from "./dropdown.module.css"
type Item = File & {
icon: JSX.Element

View file

@ -1,7 +1,6 @@
import { Fieldset, Text, Divider, Note, Input, Textarea, Button } from "@geist-ui/core"
import Password from "./sections/password"
import Profile from "./sections/profile"
import SettingsGroup from "./settings-group"
import SettingsGroup from "../settings-group"
const SettingsPage = () => {
return (<div style={{

View file

@ -94,23 +94,23 @@ admin.delete("/users/:id", async (req, res, next) => {
}
})
// admin.delete("/posts/:id", async (req, res, next) => {
// try {
// const post = await Post.findByPk(req.params.id)
// if (!post) {
// return res.status(404).json({
// error: "Post not found"
// })
// }
// await post.destroy()
admin.delete("/posts/:id", async (req, res, next) => {
try {
const post = await Post.findByPk(req.params.id)
if (!post) {
return res.status(404).json({
error: "Post not found"
})
}
await post.destroy()
// res.json({
// success: true
// })
// } catch (e) {
// next(e)
// }
// })
res.json({
success: true
})
} catch (e) {
next(e)
}
})
admin.get("/posts", async (req, res, next) => {
try {