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"
type Action = {
title: string
onClick: () => void
title: string
onClick: () => void
}
const ActionDropdown = ({
title = "Actions",
actions,
showTitle = false,
title = "Actions",
actions,
showTitle = false
}: {
title?: string,
showTitle?: boolean,
actions: Action[]
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>
)
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
export default ActionDropdown

View file

@ -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: ""
}
@ -109,14 +109,14 @@ const PostTable = () => {
dataIndex: "",
key: "actions",
width: 50,
render(post: Post) {
render() {
return (
<ActionDropdown
title="Actions"
actions={[
{
title: "Delete",
onClick: () => deletePost(post.id)
onClick: () => deletePost()
}
]}
/>
@ -128,7 +128,11 @@ const PostTable = () => {
return (
<SettingsGroup title="Posts">
{!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} />}
</SettingsGroup>
)

View file

@ -131,7 +131,8 @@ const UserTable = () => {
actions={[
{
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",
@ -147,7 +148,11 @@ const UserTable = () => {
return (
<SettingsGroup title="Users">
{!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} />}
</SettingsGroup>
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,20 +3,24 @@ import Profile from "./sections/profile"
import SettingsGroup from "../settings-group"
const SettingsPage = () => {
return (<div style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--gap)',
marginBottom: 'var(--gap)',
}}>
<h1>Settings</h1>
<SettingsGroup title="Profile">
<Profile />
</SettingsGroup>
<SettingsGroup title="Password">
<Password />
</SettingsGroup>
</div>)
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
marginBottom: "var(--gap)"
}}
>
<h1>Settings</h1>
<SettingsGroup title="Profile">
<Profile />
</SettingsGroup>
<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"
const Password = () => {
const [password, setPassword] = useState<string>('')
const [newPassword, setNewPassword] = useState<string>('')
const [confirmPassword, setConfirmPassword] = useState<string>('')
const [password, setPassword] = useState<string>("")
const [newPassword, setNewPassword] = useState<string>("")
const [confirmPassword, setConfirmPassword] = useState<string>("")
const { setToast } = useToasts()
const { setToast } = useToasts()
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(e.target.value)
}
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(e.target.value)
}
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setConfirmPassword(e.target.value)
}
const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setConfirmPassword(e.target.value)
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!password || !newPassword || !confirmPassword) {
setToast({
text: "Please fill out all fields",
type: "error",
})
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!password || !newPassword || !confirmPassword) {
setToast({
text: "Please fill out all fields",
type: "error"
})
}
if (newPassword !== confirmPassword) {
setToast({
text: "New password and confirm password do not match",
type: "error",
})
}
if (newPassword !== confirmPassword) {
setToast({
text: "New password and confirm password do not match",
type: "error"
})
}
const res = await fetch("/server-api/auth/change-password", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Cookies.get("drift-token")}`,
},
body: JSON.stringify({
oldPassword: password,
newPassword,
}),
})
const res = await fetch("/server-api/auth/change-password", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
},
body: JSON.stringify({
oldPassword: password,
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({
text: "Password updated successfully",
type: "success",
})
setPassword('')
setNewPassword('')
setConfirmPassword('')
} else {
const data = await res.json()
setToast({
text: data.error ?? "Failed to update password",
type: "error"
})
}
}
setToast({
text: data.error ?? "Failed to update password",
type: "error",
})
}
}
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
maxWidth: "300px",
}}
onSubmit={onSubmit}
>
<div>
<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%"} />
</div>
<div>
<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%"} />
</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>)
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
maxWidth: "300px"
}}
onSubmit={onSubmit}
>
<div>
<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%"}
/>
</div>
<div>
<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%"}
/>
</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"
const Profile = () => {
const user = useUserData()
const [name, setName] = useState<string>()
const [email, setEmail] = useState<string>()
const [bio, setBio] = useState<string>()
const user = useUserData()
const [name, setName] = useState<string>()
const [email, setEmail] = useState<string>()
const [bio, setBio] = useState<string>()
useEffect(() => {
console.log(user)
if (user?.displayName) setName(user.displayName)
if (user?.email) setEmail(user.email)
if (user?.bio) setBio(user.bio)
}, [user])
useEffect(() => {
console.log(user)
if (user?.displayName) setName(user.displayName)
if (user?.email) setEmail(user.email)
if (user?.bio) setBio(user.bio)
}, [user])
const { setToast } = useToasts()
const { setToast } = useToasts()
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
}
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value)
}
const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setBio(e.target.value)
}
const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setBio(e.target.value)
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!name && !email && !bio) {
setToast({
text: "Please fill out at least one field",
type: "error",
})
return
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!name && !email && !bio) {
setToast({
text: "Please fill out at least one field",
type: "error"
})
return
}
const data = {
displayName: name,
email,
bio,
}
const data = {
displayName: name,
email,
bio
}
const res = await fetch("/server-api/user/profile", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Cookies.get("drift-token")}`,
},
body: JSON.stringify(data),
})
const res = await fetch("/server-api/user/profile", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
},
body: JSON.stringify(data)
})
if (res.status === 200) {
setToast({
text: "Profile updated",
type: "success",
})
} else {
setToast({
text: "Something went wrong updating your profile",
type: "error",
})
}
}
if (res.status === 200) {
setToast({
text: "Profile updated",
type: "success"
})
} else {
setToast({
text: "Something went wrong updating your profile",
type: "error"
})
}
}
return (<>
<Note type="warning" marginBottom={"var(--gap)"}>
This information will be publicly available on your profile
</Note>
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
maxWidth: "300px",
}}
onSubmit={onSubmit}
>
<div>
<label htmlFor="displayName">Display name</label>
<Input id="displayName" width={"100%"} placeholder="my name" value={name || ''} onChange={handleNameChange} />
</div>
<div>
<label htmlFor="email">Email</label>
<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></>)
return (
<>
<Note type="warning" marginBottom={"var(--gap)"}>
This information will be publicly available on your profile
</Note>
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
maxWidth: "300px"
}}
onSubmit={onSubmit}
>
<div>
<label htmlFor="displayName">Display name</label>
<Input
id="displayName"
width={"100%"}
placeholder="my name"
value={name || ""}
onChange={handleNameChange}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<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 { Page, Text } from "@geist-ui/core"
import type { GetStaticProps } from "next"
import { InferGetStaticPropsType } from 'next'
type Props = {
introContent: string
introTitle: string
rendered: string
} | {
error: boolean
}
import { InferGetStaticPropsType } from "next"
type Props =
| {
introContent: string
introTitle: string
rendered: string
}
| {
error: boolean
}
export const getStaticProps: GetStaticProps = async () => {
try {
@ -32,21 +34,26 @@ export const getStaticProps: GetStaticProps = async () => {
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most every 60 seconds
revalidate: 60, // In seconds
revalidate: 60 // In seconds
}
} catch (err) {
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
return {
props: {
error: true,
error: true
},
revalidate: 10, // In seconds
revalidate: 10 // In seconds
}
}
}
// TODO: fix props type
const Home = ({ rendered, introContent, introTitle, error }: InferGetStaticPropsType<typeof getStaticProps>) => {
const Home = ({
rendered,
introContent,
introTitle,
error
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<Page className={styles.wrapper}>
<PageSeo />

View file

@ -69,7 +69,6 @@ export const getServerSideProps: GetServerSideProps = async ({
}
}
return {
props: {
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 styles from "@styles/Home.module.css"
import SettingsPage from "@components/settings"
const Settings = () => (
<Page width={"100%"}>
<PageSeo title="Drift - Settings" />
<Page.Content className={styles.main} style={{ gap: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
<SettingsPage />
</Page.Content>
</Page>
<Page width={"100%"}>
<PageSeo title="Drift - Settings" />
<Page.Content
className={styles.main}
style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }}
>
<SettingsPage />
</Page.Content>
</Page>
)
export default Settings

View file

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

View file

@ -30,13 +30,14 @@ user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
}
})
user.put("/profile",
user.put(
"/profile",
jwt,
celebrate({
body: {
displayName: 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) => {