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 SettingsGroup from "@components/settings-group"
|
||||||
import MoreVertical from "@geist-ui/icons/moreVertical"
|
import { Fieldset, useToasts } from "@geist-ui/core"
|
||||||
|
import byteToMB from "@lib/byte-to-mb"
|
||||||
import { Post } from "@lib/types"
|
import { Post } from "@lib/types"
|
||||||
import Cookies from "js-cookie"
|
import Table from "rc-table"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { adminFetcher } from "."
|
import { adminFetcher } from "."
|
||||||
import Table from "rc-table"
|
import ActionDropdown from "./action-dropdown"
|
||||||
import byteToMB from "@lib/byte-to-mb"
|
|
||||||
|
|
||||||
const PostTable = () => {
|
const PostTable = () => {
|
||||||
const [posts, setPosts] = useState<Post[]>()
|
const [posts, setPosts] = useState<Post[]>()
|
||||||
|
@ -35,8 +35,8 @@ const PostTable = () => {
|
||||||
visibility: post.visibility,
|
visibility: post.visibility,
|
||||||
size: post.files
|
size: post.files
|
||||||
? byteToMB(
|
? byteToMB(
|
||||||
post.files.reduce((acc, file) => acc + file.html.length, 0)
|
post.files.reduce((acc, file) => acc + file.html.length, 0)
|
||||||
)
|
)
|
||||||
: 0,
|
: 0,
|
||||||
actions: ""
|
actions: ""
|
||||||
}
|
}
|
||||||
|
@ -44,27 +44,34 @@ const PostTable = () => {
|
||||||
[posts]
|
[posts]
|
||||||
)
|
)
|
||||||
|
|
||||||
// const deletePost = async (id: number) => {
|
const deletePost = async (/* id: string */) => {
|
||||||
// const confirm = window.confirm("Are you sure you want to delete this post?")
|
return alert("Not implemented")
|
||||||
// if (!confirm) return
|
|
||||||
// const res = await adminFetcher(`/posts/${id}`, {
|
|
||||||
// method: "DELETE",
|
|
||||||
// })
|
|
||||||
|
|
||||||
// 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) {
|
// const json = await res.json()
|
||||||
// setToast({
|
|
||||||
// text: "Post deleted",
|
// if (res.status === 200) {
|
||||||
// type: "success"
|
// setToast({
|
||||||
// })
|
// text: "Post deleted",
|
||||||
// } else {
|
// type: "success"
|
||||||
// setToast({
|
// })
|
||||||
// text: json.error || "Something went wrong",
|
|
||||||
// type: "error"
|
// setPosts((posts) => {
|
||||||
// })
|
// const newPosts = posts?.filter((post) => post.id !== id)
|
||||||
// }
|
// return newPosts
|
||||||
// }
|
// })
|
||||||
|
// } else {
|
||||||
|
// setToast({
|
||||||
|
// text: json.error || "Something went wrong",
|
||||||
|
// type: "error"
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
|
@ -102,37 +109,28 @@ const PostTable = () => {
|
||||||
dataIndex: "",
|
dataIndex: "",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
width: 50,
|
width: 50,
|
||||||
render() {
|
render(post: Post) {
|
||||||
return (
|
return (
|
||||||
<Popover
|
<ActionDropdown
|
||||||
content={
|
title="Actions"
|
||||||
<div
|
actions={[
|
||||||
style={{
|
{
|
||||||
width: 100,
|
title: "Delete",
|
||||||
display: "flex",
|
onClick: () => deletePost(post.id)
|
||||||
flexDirection: "column",
|
}
|
||||||
alignItems: "center"
|
]}
|
||||||
}}
|
/>
|
||||||
>
|
|
||||||
{/* <Link href="#" onClick={() => deletePost(post.id)}>Delete post</Link> */}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
hideArrow
|
|
||||||
>
|
|
||||||
<Button iconRight={<MoreVertical />} auto></Button>
|
|
||||||
</Popover>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<SettingsGroup title="Posts">
|
||||||
<Fieldset.Title>Posts</Fieldset.Title>
|
|
||||||
{posts && <Fieldset.Subtitle>{posts.length} posts</Fieldset.Subtitle>}
|
|
||||||
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
||||||
|
{posts && <Fieldset.Subtitle><h5>{posts.length} posts</h5></Fieldset.Subtitle>}
|
||||||
{posts && <Table columns={tableColumns} data={tablePosts} />}
|
{posts && <Table columns={tableColumns} data={tablePosts} />}
|
||||||
</Fieldset>
|
</SettingsGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Button, Fieldset, Link, Popover, useToasts } from "@geist-ui/core"
|
import { Fieldset, useToasts } from "@geist-ui/core"
|
||||||
import MoreVertical from "@geist-ui/icons/moreVertical"
|
|
||||||
import { User } from "@lib/types"
|
import { User } from "@lib/types"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { adminFetcher } from "."
|
import { adminFetcher } from "."
|
||||||
import Table from "rc-table"
|
import Table from "rc-table"
|
||||||
|
import SettingsGroup from "@components/settings-group"
|
||||||
|
import ActionDropdown from "./action-dropdown"
|
||||||
|
|
||||||
const UserTable = () => {
|
const UserTable = () => {
|
||||||
const [users, setUsers] = useState<User[]>()
|
const [users, setUsers] = useState<User[]>()
|
||||||
|
@ -19,7 +19,7 @@ const UserTable = () => {
|
||||||
fetchUsers()
|
fetchUsers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleRole = async (id: number, role: "admin" | "user") => {
|
const toggleRole = async (id: string, role: "admin" | "user") => {
|
||||||
const res = await adminFetcher("/users/toggle-role", {
|
const res = await adminFetcher("/users/toggle-role", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { id, role }
|
body: { id, role }
|
||||||
|
@ -33,17 +33,18 @@ const UserTable = () => {
|
||||||
type: "success"
|
type: "success"
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: swr should handle updating this
|
setUsers((users) => {
|
||||||
const newUsers = users?.map((user) => {
|
const newUsers = users?.map((user) => {
|
||||||
if (user.id === id.toString()) {
|
if (user.id === id) {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
role: role === "admin" ? "user" : ("admin" as User["role"])
|
role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return user
|
||||||
return user
|
})
|
||||||
|
return newUsers
|
||||||
})
|
})
|
||||||
setUsers(newUsers)
|
|
||||||
} else {
|
} else {
|
||||||
setToast({
|
setToast({
|
||||||
text: json.error || "Something went wrong",
|
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?")
|
const confirm = window.confirm("Are you sure you want to delete this user?")
|
||||||
if (!confirm) return
|
if (!confirm) return
|
||||||
const res = await adminFetcher(`/users/${id}`, {
|
const res = await adminFetcher(`/users/${id}`, {
|
||||||
|
@ -123,42 +124,32 @@ const UserTable = () => {
|
||||||
dataIndex: "",
|
dataIndex: "",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
width: 50,
|
width: 50,
|
||||||
render(user: any) {
|
render(user: User) {
|
||||||
return (
|
return (
|
||||||
<Popover
|
<ActionDropdown
|
||||||
content={
|
title="Actions"
|
||||||
<div
|
actions={[
|
||||||
style={{
|
{
|
||||||
width: 100,
|
title: user.role === "admin" ? "Change role" : "Make admin",
|
||||||
display: "flex",
|
onClick: () => toggleRole(user.id, user.role === "admin" ? "user" : "admin")
|
||||||
flexDirection: "column",
|
},
|
||||||
alignItems: "center"
|
{
|
||||||
}}
|
title: "Delete",
|
||||||
>
|
onClick: () => deleteUser(user.id)
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fieldset>
|
<SettingsGroup title="Users">
|
||||||
<Fieldset.Title>Users</Fieldset.Title>
|
|
||||||
{users && <Fieldset.Subtitle>{users.length} users</Fieldset.Subtitle>}
|
|
||||||
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
||||||
|
{users && <Fieldset.Subtitle><h5>{users.length} users</h5></Fieldset.Subtitle>}
|
||||||
{users && <Table columns={usernameColumns} data={tableUsers} />}
|
{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 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 type { File } from "@lib/types"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import styles from "./dropdown.module.css"
|
||||||
|
|
||||||
type Item = File & {
|
type Item = File & {
|
||||||
icon: JSX.Element
|
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 Password from "./sections/password"
|
||||||
import Profile from "./sections/profile"
|
import Profile from "./sections/profile"
|
||||||
import SettingsGroup from "./settings-group"
|
import SettingsGroup from "../settings-group"
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const SettingsPage = () => {
|
||||||
return (<div style={{
|
return (<div style={{
|
||||||
|
|
|
@ -94,23 +94,23 @@ admin.delete("/users/:id", async (req, res, next) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// admin.delete("/posts/:id", async (req, res, next) => {
|
admin.delete("/posts/:id", async (req, res, next) => {
|
||||||
// try {
|
try {
|
||||||
// const post = await Post.findByPk(req.params.id)
|
const post = await Post.findByPk(req.params.id)
|
||||||
// if (!post) {
|
if (!post) {
|
||||||
// return res.status(404).json({
|
return res.status(404).json({
|
||||||
// error: "Post not found"
|
error: "Post not found"
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
// await post.destroy()
|
await post.destroy()
|
||||||
|
|
||||||
// res.json({
|
res.json({
|
||||||
// success: true
|
success: true
|
||||||
// })
|
})
|
||||||
// } catch (e) {
|
} catch (e) {
|
||||||
// next(e)
|
next(e)
|
||||||
// }
|
}
|
||||||
// })
|
})
|
||||||
|
|
||||||
admin.get("/posts", async (req, res, next) => {
|
admin.get("/posts", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|
Loading…
Reference in a new issue