client/server: linting and fix next building

This commit is contained in:
Max Leiter 2022-04-21 22:01:59 -07:00
parent bceeb5cee8
commit a52e9a1c62
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
17 changed files with 368 additions and 295 deletions

View file

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

View file

@ -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: ""
} }
@ -109,14 +109,14 @@ const PostTable = () => {
dataIndex: "", dataIndex: "",
key: "actions", key: "actions",
width: 50, width: 50,
render(post: Post) { render() {
return ( return (
<ActionDropdown <ActionDropdown
title="Actions" title="Actions"
actions={[ actions={[
{ {
title: "Delete", title: "Delete",
onClick: () => deletePost(post.id) onClick: () => deletePost()
} }
]} ]}
/> />
@ -128,7 +128,11 @@ const PostTable = () => {
return ( return (
<SettingsGroup title="Posts"> <SettingsGroup title="Posts">
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>} {!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{posts && <Fieldset.Subtitle><h5>{posts.length} posts</h5></Fieldset.Subtitle>} {posts && (
<Fieldset.Subtitle>
<h5>{posts.length} posts</h5>
</Fieldset.Subtitle>
)}
{posts && <Table columns={tableColumns} data={tablePosts} />} {posts && <Table columns={tableColumns} data={tablePosts} />}
</SettingsGroup> </SettingsGroup>
) )

View file

@ -131,7 +131,8 @@ const UserTable = () => {
actions={[ actions={[
{ {
title: user.role === "admin" ? "Change role" : "Make admin", title: user.role === "admin" ? "Change role" : "Make admin",
onClick: () => toggleRole(user.id, user.role === "admin" ? "user" : "admin") onClick: () =>
toggleRole(user.id, user.role === "admin" ? "user" : "admin")
}, },
{ {
title: "Delete", title: "Delete",
@ -147,7 +148,11 @@ const UserTable = () => {
return ( return (
<SettingsGroup title="Users"> <SettingsGroup title="Users">
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>} {!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{users && <Fieldset.Subtitle><h5>{users.length} users</h5></Fieldset.Subtitle>} {users && (
<Fieldset.Subtitle>
<h5>{users.length} users</h5>
</Fieldset.Subtitle>
)}
{users && <Table columns={usernameColumns} data={tableUsers} />} {users && <Table columns={usernameColumns} data={tableUsers} />}
</SettingsGroup> </SettingsGroup>
) )

View file

@ -89,10 +89,10 @@ const Header = () => {
href: "/mine" href: "/mine"
}, },
{ {
name: 'settings', name: "settings",
icon: <SettingsIcon />, icon: <SettingsIcon />,
value: 'settings', value: "settings",
href: '/settings' href: "/settings"
}, },
{ {
name: "sign out", name: "sign out",

View file

@ -1,9 +1,4 @@
import { import { Button, useToasts, ButtonDropdown, Input } from "@geist-ui/core"
Button,
useToasts,
ButtonDropdown,
Input,
} from "@geist-ui/core"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import generateUUID from "@lib/generate-uuid" import generateUUID from "@lib/generate-uuid"

View file

@ -97,10 +97,7 @@ const ListItem = ({
{post.files?.map((file: File) => { {post.files?.map((file: File) => {
return ( return (
<div key={file.id}> <div key={file.id}>
<Link <Link color href={`/post/${post.id}#${file.title}`}>
color
href={`/post/${post.id}#${file.title}`}
>
{file.title || "Untitled file"} {file.title || "Untitled file"}
</Link> </Link>
</div> </div>

View file

@ -85,9 +85,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
} }
const viewParentClick = () => { const viewParentClick = () => {
router.push( router.push(`/post/${post.parent!.id}`)
`/post/${post.parent!.id}`
)
} }
if (isLoading) { if (isLoading) {
@ -123,11 +121,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
Edit a Copy Edit a Copy
</Button> </Button>
{post.parent && ( {post.parent && (
<Button <Button auto icon={<Parent />} onClick={viewParentClick}>
auto
icon={<Parent />}
onClick={viewParentClick}
>
View Parent View Parent
</Button> </Button>
)} )}

View file

@ -1,24 +1,21 @@
import { Fieldset, Text, Divider } from "@geist-ui/core" import { Fieldset, Text, Divider } from "@geist-ui/core"
import styles from './settings-group.module.css' import styles from "./settings-group.module.css"
type Props = { type Props = {
title: string, title: string
children: React.ReactNode | React.ReactNode[], children: React.ReactNode | React.ReactNode[]
} }
const SettingsGroup = ({ const SettingsGroup = ({ title, children }: Props) => {
title, return (
children, <Fieldset>
}: Props) => { <Fieldset.Content>
return <Fieldset> <Text h4>{title}</Text>
<Fieldset.Content> </Fieldset.Content>
<Text h4>{title}</Text> <Divider />
</Fieldset.Content> <Fieldset.Content className={styles.content}>{children}</Fieldset.Content>
<Divider /> </Fieldset>
<Fieldset.Content className={styles.content}> )
{children}
</Fieldset.Content>
</Fieldset>
} }
export default SettingsGroup export default SettingsGroup

View file

@ -3,20 +3,24 @@ import Profile from "./sections/profile"
import SettingsGroup from "../settings-group" import SettingsGroup from "../settings-group"
const SettingsPage = () => { const SettingsPage = () => {
return (<div style={{ return (
display: 'flex', <div
flexDirection: 'column', style={{
gap: 'var(--gap)', display: "flex",
marginBottom: 'var(--gap)', flexDirection: "column",
}}> gap: "var(--gap)",
<h1>Settings</h1> marginBottom: "var(--gap)"
<SettingsGroup title="Profile"> }}
<Profile /> >
</SettingsGroup> <h1>Settings</h1>
<SettingsGroup title="Password"> <SettingsGroup title="Profile">
<Password /> <Profile />
</SettingsGroup> </SettingsGroup>
</div>) <SettingsGroup title="Password">
<Password />
</SettingsGroup>
</div>
)
} }
export default SettingsPage export default SettingsPage

View file

@ -3,95 +3,132 @@ import Cookies from "js-cookie"
import { useState } from "react" import { useState } from "react"
const Password = () => { const Password = () => {
const [password, setPassword] = useState<string>('') const [password, setPassword] = useState<string>("")
const [newPassword, setNewPassword] = useState<string>('') const [newPassword, setNewPassword] = useState<string>("")
const [confirmPassword, setConfirmPassword] = useState<string>('') const [confirmPassword, setConfirmPassword] = useState<string>("")
const { setToast } = useToasts() const { setToast } = useToasts()
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value) setPassword(e.target.value)
} }
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(e.target.value) setNewPassword(e.target.value)
} }
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleConfirmPasswordChange = (
setConfirmPassword(e.target.value) e: React.ChangeEvent<HTMLInputElement>
} ) => {
setConfirmPassword(e.target.value)
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if (!password || !newPassword || !confirmPassword) { if (!password || !newPassword || !confirmPassword) {
setToast({ setToast({
text: "Please fill out all fields", text: "Please fill out all fields",
type: "error", type: "error"
}) })
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setToast({ setToast({
text: "New password and confirm password do not match", text: "New password and confirm password do not match",
type: "error", type: "error"
}) })
} }
const res = await fetch("/server-api/auth/change-password", { const res = await fetch("/server-api/auth/change-password", {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${Cookies.get("drift-token")}`, Authorization: `Bearer ${Cookies.get("drift-token")}`
}, },
body: JSON.stringify({ body: JSON.stringify({
oldPassword: password, oldPassword: password,
newPassword, newPassword
}), })
}) })
if (res.status === 200) {
setToast({
text: "Password updated successfully",
type: "success"
})
setPassword("")
setNewPassword("")
setConfirmPassword("")
} else {
const data = await res.json()
if (res.status === 200) { setToast({
setToast({ text: data.error ?? "Failed to update password",
text: "Password updated successfully", type: "error"
type: "success", })
}) }
setPassword('') }
setNewPassword('')
setConfirmPassword('')
} else {
const data = await res.json()
setToast({ return (
text: data.error ?? "Failed to update password", <form
type: "error", style={{
}) display: "flex",
} flexDirection: "column",
} gap: "var(--gap)",
maxWidth: "300px"
return ( }}
<form onSubmit={onSubmit}
style={{ >
display: "flex", <div>
flexDirection: "column", <label htmlFor="current-password">Current password</label>
gap: "var(--gap)", <Input
maxWidth: "300px", onChange={handlePasswordChange}
}} minLength={6}
onSubmit={onSubmit} maxLength={128}
> value={password}
<div> id="current-password"
<label htmlFor="current-password">Current password</label> htmlType="password"
<Input onChange={handlePasswordChange} minLength={6} maxLength={128} value={password} id="current-password" htmlType="password" required autoComplete="current-password" placeholder="Current Password" width={"100%"} /> required
</div> autoComplete="current-password"
<div> placeholder="Current Password"
<label htmlFor="new-password">New password</label> width={"100%"}
<Input onChange={handleNewPasswordChange} minLength={6} maxLength={128} value={newPassword} id="new-password" htmlType="password" required autoComplete="new-password" placeholder="New Password" width={"100%"} /> />
</div> </div>
<div> <div>
<label htmlFor="confirm-password">Confirm password</label> <label htmlFor="new-password">New password</label>
<Input onChange={handleConfirmPasswordChange} minLength={6} maxLength={128} value={confirmPassword} id="confirm-password" htmlType="password" required autoComplete="confirm-password" placeholder="Confirm Password" width={"100%"} /> <Input
</div> onChange={handleNewPasswordChange}
<Button htmlType="submit" auto>Change Password</Button> minLength={6}
</form>) maxLength={128}
value={newPassword}
id="new-password"
htmlType="password"
required
autoComplete="new-password"
placeholder="New Password"
width={"100%"}
/>
</div>
<div>
<label htmlFor="confirm-password">Confirm password</label>
<Input
onChange={handleConfirmPasswordChange}
minLength={6}
maxLength={128}
value={confirmPassword}
id="confirm-password"
htmlType="password"
required
autoComplete="confirm-password"
placeholder="Confirm Password"
width={"100%"}
/>
</div>
<Button htmlType="submit" auto>
Change Password
</Button>
</form>
)
} }
export default Password export default Password

View file

@ -4,97 +4,122 @@ import Cookies from "js-cookie"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
const Profile = () => { const Profile = () => {
const user = useUserData() const user = useUserData()
const [name, setName] = useState<string>() const [name, setName] = useState<string>()
const [email, setEmail] = useState<string>() const [email, setEmail] = useState<string>()
const [bio, setBio] = useState<string>() const [bio, setBio] = useState<string>()
useEffect(() => { useEffect(() => {
console.log(user) console.log(user)
if (user?.displayName) setName(user.displayName) if (user?.displayName) setName(user.displayName)
if (user?.email) setEmail(user.email) if (user?.email) setEmail(user.email)
if (user?.bio) setBio(user.bio) if (user?.bio) setBio(user.bio)
}, [user]) }, [user])
const { setToast } = useToasts() const { setToast } = useToasts()
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value) setName(e.target.value)
} }
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value) setEmail(e.target.value)
} }
const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setBio(e.target.value) setBio(e.target.value)
} }
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if (!name && !email && !bio) { if (!name && !email && !bio) {
setToast({ setToast({
text: "Please fill out at least one field", text: "Please fill out at least one field",
type: "error", type: "error"
}) })
return return
} }
const data = { const data = {
displayName: name, displayName: name,
email, email,
bio, bio
} }
const res = await fetch("/server-api/user/profile", { const res = await fetch("/server-api/user/profile", {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${Cookies.get("drift-token")}`, Authorization: `Bearer ${Cookies.get("drift-token")}`
}, },
body: JSON.stringify(data), body: JSON.stringify(data)
}) })
if (res.status === 200) { if (res.status === 200) {
setToast({ setToast({
text: "Profile updated", text: "Profile updated",
type: "success", type: "success"
}) })
} else { } else {
setToast({ setToast({
text: "Something went wrong updating your profile", text: "Something went wrong updating your profile",
type: "error", type: "error"
}) })
} }
} }
return (<> return (
<Note type="warning" marginBottom={"var(--gap)"}> <>
This information will be publicly available on your profile <Note type="warning" marginBottom={"var(--gap)"}>
</Note> This information will be publicly available on your profile
<form </Note>
style={{ <form
display: "flex", style={{
flexDirection: "column", display: "flex",
gap: "var(--gap)", flexDirection: "column",
maxWidth: "300px", gap: "var(--gap)",
}} maxWidth: "300px"
onSubmit={onSubmit} }}
> onSubmit={onSubmit}
<div> >
<label htmlFor="displayName">Display name</label> <div>
<Input id="displayName" width={"100%"} placeholder="my name" value={name || ''} onChange={handleNameChange} /> <label htmlFor="displayName">Display name</label>
</div> <Input
<div> id="displayName"
<label htmlFor="email">Email</label> width={"100%"}
<Input id="email" htmlType="email" width={"100%"} placeholder="my@email.io" value={email || ''} onChange={handleEmailChange} /> placeholder="my name"
</div> value={name || ""}
<div> onChange={handleNameChange}
<label htmlFor="bio">Biography (max 250 characters)</label> />
<Textarea id="bio" width="100%" maxLength={250} placeholder="I enjoy..." value={bio || ''} onChange={handleBioChange} /> </div>
</div> <div>
<Button htmlType="submit" auto>Submit</Button> <label htmlFor="email">Email</label>
</form></>) <Input
id="email"
htmlType="email"
width={"100%"}
placeholder="my@email.io"
value={email || ""}
onChange={handleEmailChange}
/>
</div>
<div>
<label htmlFor="bio">Biography (max 250 characters)</label>
<Textarea
id="bio"
width="100%"
maxLength={250}
placeholder="I enjoy..."
value={bio || ""}
onChange={handleBioChange}
/>
</div>
<Button htmlType="submit" auto>
Submit
</Button>
</form>
</>
)
} }
export default Profile export default Profile

View file

@ -3,14 +3,16 @@ import PageSeo from "@components/page-seo"
import HomeComponent from "@components/home" import HomeComponent from "@components/home"
import { Page, Text } from "@geist-ui/core" import { Page, Text } from "@geist-ui/core"
import type { GetStaticProps } from "next" import type { GetStaticProps } from "next"
import { InferGetStaticPropsType } from 'next' import { InferGetStaticPropsType } from "next"
type Props = { type Props =
introContent: string | {
introTitle: string introContent: string
rendered: string introTitle: string
} | { rendered: string
error: boolean }
} | {
error: boolean
}
export const getStaticProps: GetStaticProps = async () => { export const getStaticProps: GetStaticProps = async () => {
try { try {
@ -32,21 +34,26 @@ export const getStaticProps: GetStaticProps = async () => {
// Next.js will attempt to re-generate the page: // Next.js will attempt to re-generate the page:
// - When a request comes in // - When a request comes in
// - At most every 60 seconds // - At most every 60 seconds
revalidate: 60, // In seconds revalidate: 60 // In seconds
} }
} catch (err) { } catch (err) {
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page // If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
return { return {
props: { props: {
error: true, error: true
}, },
revalidate: 10, // In seconds revalidate: 10 // In seconds
} }
} }
} }
// TODO: fix props type // TODO: fix props type
const Home = ({ rendered, introContent, introTitle, error }: InferGetStaticPropsType<typeof getStaticProps>) => { const Home = ({
rendered,
introContent,
introTitle,
error
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return ( return (
<Page className={styles.wrapper}> <Page className={styles.wrapper}>
<PageSeo /> <PageSeo />

View file

@ -69,7 +69,6 @@ export const getServerSideProps: GetServerSideProps = async ({
} }
} }
return { return {
props: { props: {
post: json, post: json,

View file

@ -1,15 +1,27 @@
import { Button, Divider, Text, Fieldset, Input, Page, Note, Textarea } from "@geist-ui/core" import {
Button,
Divider,
Text,
Fieldset,
Input,
Page,
Note,
Textarea
} from "@geist-ui/core"
import PageSeo from "@components/page-seo" import PageSeo from "@components/page-seo"
import styles from "@styles/Home.module.css" import styles from "@styles/Home.module.css"
import SettingsPage from "@components/settings" import SettingsPage from "@components/settings"
const Settings = () => ( const Settings = () => (
<Page width={"100%"}> <Page width={"100%"}>
<PageSeo title="Drift - Settings" /> <PageSeo title="Drift - Settings" />
<Page.Content className={styles.main} style={{ gap: 'var(--gap)', display: 'flex', flexDirection: 'column' }}> <Page.Content
<SettingsPage /> className={styles.main}
</Page.Content> style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }}
</Page> >
<SettingsPage />
</Page.Content>
</Page>
) )
export default Settings export default Settings

View file

@ -3,24 +3,24 @@ import { DataTypes } from "sequelize"
import type { Migration } from "../database" import type { Migration } from "../database"
export const up: Migration = async ({ context: queryInterface }) => export const up: Migration = async ({ context: queryInterface }) =>
Promise.all([ Promise.all([
queryInterface.addColumn("users", "email", { queryInterface.addColumn("users", "email", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
}), }),
queryInterface.addColumn("users", "displayName", { queryInterface.addColumn("users", "displayName", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true
}), }),
queryInterface.addColumn("users", "bio", { queryInterface.addColumn("users", "bio", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true
}), })
]) ])
export const down: Migration = async ({ context: queryInterface }) => export const down: Migration = async ({ context: queryInterface }) =>
Promise.all([ Promise.all([
queryInterface.removeColumn("users", "email"), queryInterface.removeColumn("users", "email"),
queryInterface.removeColumn("users", "displayName"), queryInterface.removeColumn("users", "displayName"),
queryInterface.removeColumn("users", "bio"), queryInterface.removeColumn("users", "bio")
]) ])

View file

@ -195,7 +195,8 @@ auth.post("/signout", secretKey, async (req, res, next) => {
} }
}) })
auth.put("/change-password", auth.put(
"/change-password",
jwt, jwt,
celebrate({ celebrate({
body: { body: {

View file

@ -30,13 +30,14 @@ user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
} }
}) })
user.put("/profile", user.put(
"/profile",
jwt, jwt,
celebrate({ celebrate({
body: { body: {
displayName: Joi.string().optional().allow(""), displayName: Joi.string().optional().allow(""),
bio: Joi.string().optional().allow(""), bio: Joi.string().optional().allow(""),
email: Joi.string().optional().email().allow(""), email: Joi.string().optional().email().allow("")
} }
}), }),
async (req: UserJwtRequest, res, next) => { async (req: UserJwtRequest, res, next) => {