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

@ -9,10 +9,10 @@ type Action = {
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 (
@ -20,14 +20,9 @@ const ActionDropdown = ({
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 => (
<Popover.Item
onClick={action.onClick}
key={action.title}
>
{action.title} {action.title}
</Popover.Item> </Popover.Item>
))} ))}

View file

@ -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) => {
return <Fieldset>
<Fieldset.Content> <Fieldset.Content>
<Text h4>{title}</Text> <Text h4>{title}</Text>
</Fieldset.Content> </Fieldset.Content>
<Divider /> <Divider />
<Fieldset.Content className={styles.content}> <Fieldset.Content className={styles.content}>{children}</Fieldset.Content>
{children}
</Fieldset.Content>
</Fieldset> </Fieldset>
)
} }
export default SettingsGroup export default SettingsGroup

View file

@ -3,12 +3,15 @@ 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)",
marginBottom: "var(--gap)"
}}
>
<h1>Settings</h1> <h1>Settings</h1>
<SettingsGroup title="Profile"> <SettingsGroup title="Profile">
<Profile /> <Profile />
@ -16,7 +19,8 @@ const SettingsPage = () => {
<SettingsGroup title="Password"> <SettingsGroup title="Password">
<Password /> <Password />
</SettingsGroup> </SettingsGroup>
</div>) </div>
)
} }
export default SettingsPage export default SettingsPage

View file

@ -3,9 +3,9 @@ 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()
@ -17,7 +17,9 @@ const Password = () => {
setNewPassword(e.target.value) setNewPassword(e.target.value)
} }
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setConfirmPassword(e.target.value) setConfirmPassword(e.target.value)
} }
@ -26,14 +28,14 @@ const Password = () => {
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"
}) })
} }
@ -41,29 +43,28 @@ const 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) { if (res.status === 200) {
setToast({ setToast({
text: "Password updated successfully", text: "Password updated successfully",
type: "success", type: "success"
}) })
setPassword('') setPassword("")
setNewPassword('') setNewPassword("")
setConfirmPassword('') setConfirmPassword("")
} else { } else {
const data = await res.json() const data = await res.json()
setToast({ setToast({
text: data.error ?? "Failed to update password", text: data.error ?? "Failed to update password",
type: "error", type: "error"
}) })
} }
} }
@ -74,24 +75,60 @@ const Password = () => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "var(--gap)", gap: "var(--gap)",
maxWidth: "300px", maxWidth: "300px"
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<div> <div>
<label htmlFor="current-password">Current password</label> <label htmlFor="current-password">Current password</label>
<Input onChange={handlePasswordChange} minLength={6} maxLength={128} value={password} id="current-password" htmlType="password" required autoComplete="current-password" placeholder="Current Password" width={"100%"} /> <Input
onChange={handlePasswordChange}
minLength={6}
maxLength={128}
value={password}
id="current-password"
htmlType="password"
required
autoComplete="current-password"
placeholder="Current Password"
width={"100%"}
/>
</div> </div>
<div> <div>
<label htmlFor="new-password">New password</label> <label htmlFor="new-password">New password</label>
<Input onChange={handleNewPasswordChange} minLength={6} maxLength={128} value={newPassword} id="new-password" htmlType="password" required autoComplete="new-password" placeholder="New Password" 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="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%"} /> <Input
onChange={handleConfirmPasswordChange}
minLength={6}
maxLength={128}
value={confirmPassword}
id="confirm-password"
htmlType="password"
required
autoComplete="confirm-password"
placeholder="Confirm Password"
width={"100%"}
/>
</div> </div>
<Button htmlType="submit" auto>Change Password</Button> <Button htmlType="submit" auto>
</form>) Change Password
</Button>
</form>
)
} }
export default Password export default Password

View file

@ -35,7 +35,7 @@ const Profile = () => {
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
} }
@ -43,32 +43,33 @@ const Profile = () => {
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)"}> <Note type="warning" marginBottom={"var(--gap)"}>
This information will be publicly available on your profile This information will be publicly available on your profile
</Note> </Note>
@ -77,24 +78,48 @@ const Profile = () => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "var(--gap)", gap: "var(--gap)",
maxWidth: "300px", maxWidth: "300px"
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<div> <div>
<label htmlFor="displayName">Display name</label> <label htmlFor="displayName">Display name</label>
<Input id="displayName" width={"100%"} placeholder="my name" value={name || ''} onChange={handleNameChange} /> <Input
id="displayName"
width={"100%"}
placeholder="my name"
value={name || ""}
onChange={handleNameChange}
/>
</div> </div>
<div> <div>
<label htmlFor="email">Email</label> <label htmlFor="email">Email</label>
<Input id="email" htmlType="email" width={"100%"} placeholder="my@email.io" value={email || ''} onChange={handleEmailChange} /> <Input
id="email"
htmlType="email"
width={"100%"}
placeholder="my@email.io"
value={email || ""}
onChange={handleEmailChange}
/>
</div> </div>
<div> <div>
<label htmlFor="bio">Biography (max 250 characters)</label> <label htmlFor="bio">Biography (max 250 characters)</label>
<Textarea id="bio" width="100%" maxLength={250} placeholder="I enjoy..." value={bio || ''} onChange={handleBioChange} /> <Textarea
id="bio"
width="100%"
maxLength={250}
placeholder="I enjoy..."
value={bio || ""}
onChange={handleBioChange}
/>
</div> </div>
<Button htmlType="submit" auto>Submit</Button> <Button htmlType="submit" auto>
</form></>) 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 introContent: string
introTitle: string introTitle: string
rendered: 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,4 +1,13 @@
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"
@ -6,7 +15,10 @@ 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
className={styles.main}
style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }}
>
<SettingsPage /> <SettingsPage />
</Page.Content> </Page.Content>
</Page> </Page>

View file

@ -10,17 +10,17 @@ export const up: Migration = async ({ context: queryInterface }) =>
}), }),
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) => {