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 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[]>()
@ -44,7 +44,9 @@ const PostTable = () => {
[posts] [posts]
) )
// const deletePost = async (id: number) => { const deletePost = async (/* id: string */) => {
return alert("Not implemented")
// const confirm = window.confirm("Are you sure you want to delete this post?") // const confirm = window.confirm("Are you sure you want to delete this post?")
// if (!confirm) return // if (!confirm) return
// const res = await adminFetcher(`/posts/${id}`, { // const res = await adminFetcher(`/posts/${id}`, {
@ -58,13 +60,18 @@ const PostTable = () => {
// text: "Post deleted", // text: "Post deleted",
// type: "success" // type: "success"
// }) // })
// setPosts((posts) => {
// const newPosts = posts?.filter((post) => post.id !== id)
// return newPosts
// })
// } else { // } else {
// setToast({ // setToast({
// text: json.error || "Something went wrong", // text: json.error || "Something went wrong",
// type: "error" // 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>
) )
} }

View file

@ -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
}) })
setUsers(newUsers) return 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>
) )
} }

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 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

View file

@ -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={{

View file

@ -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 {