client/server: clean-up admin page, re-implement user deletion/role toggling
This commit is contained in:
parent
3d2bec0d5e
commit
be6de7c796
8 changed files with 145 additions and 114 deletions
43
client/components/admin/action-dropdown/index.tsx
Normal file
43
client/components/admin/action-dropdown/index.tsx
Normal 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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue