edit posts - links and discussions

This commit is contained in:
keyan 2021-08-11 15:13:10 -05:00
parent fd79b92b9b
commit a48cd33db3
7 changed files with 326 additions and 118 deletions

View File

@ -146,13 +146,65 @@ export default {
return await createItem(parent, { title, url: ensureProtocol(url) }, { me, models }) return await createItem(parent, { title, url: ensureProtocol(url) }, { me, models })
}, },
createDiscussion: async (parent, { title, text }, { me, models }) => { updateLink: async (parent, { id, title, url }, { me, models }) => {
if (!id) {
throw new UserInputError('link must have id', { argumentName: 'id' })
}
if (!title) { if (!title) {
throw new UserInputError('link must have title', { argumentName: 'title' }) throw new UserInputError('link must have title', { argumentName: 'title' })
} }
if (!url) {
throw new UserInputError('link must have url', { argumentName: 'url' })
}
// update iff this item belongs to me
const item = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(item.userId) !== Number(me.id)) {
throw new AuthenticationError('item does not belong to you')
}
if (Date.now() > new Date(item.createdAt).getTime() + 10 * 60000) {
throw new UserInputError('item can no longer be editted')
}
return await models.item.update({
where: { id: Number(id) },
data: { title, url: ensureProtocol(url) }
})
},
createDiscussion: async (parent, { title, text }, { me, models }) => {
if (!title) {
throw new UserInputError('discussion must have title', { argumentName: 'title' })
}
return await createItem(parent, { title, text }, { me, models }) return await createItem(parent, { title, text }, { me, models })
}, },
updateDiscussion: async (parent, { id, title, text }, { me, models }) => {
if (!id) {
throw new UserInputError('discussion must have id', { argumentName: 'id' })
}
if (!title) {
throw new UserInputError('discussion must have title', { argumentName: 'title' })
}
// update iff this item belongs to me
const item = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(item.userId) !== Number(me.id)) {
throw new AuthenticationError('item does not belong to you')
}
if (Date.now() > new Date(item.createdAt).getTime() + 10 * 60000) {
throw new UserInputError('item can no longer be editted')
}
return await models.item.update({
where: { id: Number(id) },
data: { title, text }
})
},
createComment: async (parent, { text, parentId }, { me, models }) => { createComment: async (parent, { text, parentId }, { me, models }) => {
if (!text) { if (!text) {
throw new UserInputError('comment must have text', { argumentName: 'text' }) throw new UserInputError('comment must have text', { argumentName: 'text' })
@ -176,7 +228,7 @@ export default {
// update iff this comment belongs to me // update iff this comment belongs to me
const comment = await models.item.findUnique({ where: { id: Number(id) } }) const comment = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(comment.userId) !== Number(me.id)) { if (Number(comment.userId) !== Number(me.id)) {
throw new AuthenticationError('comment must belong to you') throw new AuthenticationError('comment does not belong to you')
} }
if (Date.now() > new Date(comment.createdAt).getTime() + 10 * 60000) { if (Date.now() > new Date(comment.createdAt).getTime() + 10 * 60000) {

View File

@ -11,7 +11,9 @@ export default gql`
extend type Mutation { extend type Mutation {
createLink(title: String!, url: String): Item! createLink(title: String!, url: String): Item!
updateLink(id: ID!, title: String!, url: String): Item!
createDiscussion(title: String!, text: String): Item! createDiscussion(title: String!, text: String): Item!
updateDiscussion(id: ID!, title: String!, text: String): Item!
createComment(text: String!, parentId: ID!): Item! createComment(text: String!, parentId: ID!): Item!
updateComment(id: ID!, text: String!): Item! updateComment(id: ID!, text: String!): Item!
vote(id: ID!, sats: Int): Int! vote(id: ID!, sats: Int): Int!

View File

@ -0,0 +1,93 @@
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
import { useRouter } from 'next/router'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import ActionTooltip from '../components/action-tooltip'
import TextareaAutosize from 'react-textarea-autosize'
import Countdown from 'react-countdown'
export const DiscussionSchema = Yup.object({
title: Yup.string().required('required').trim()
})
export function DiscussionForm ({ item, editThreshold }) {
const router = useRouter()
const [createDiscussion] = useMutation(
gql`
mutation createDiscussion($title: String!, $text: String) {
createDiscussion(title: $title, text: $text) {
id
}
}`
)
const [updateDiscussion] = useMutation(
gql`
mutation updateDiscussion($id: ID!, $title: String!, $text: String!) {
updateDiscussion(id: $id, title: $title, text: $text) {
id
}
}`, {
update (cache, { data: { updateDiscussion } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
title () {
return updateDiscussion.title
},
text () {
return updateDiscussion.text
}
}
})
}
}
)
return (
<Form
initial={{
title: item?.title || '',
text: item?.text || ''
}}
schema={DiscussionSchema}
onSubmit={async (values) => {
let id, error
if (item) {
({ data: { updateDiscussion: { id } }, error } = await updateDiscussion({ variables: { ...values, id: item.id } }))
} else {
({ data: { createDiscussion: { id } }, error } = await createDiscussion({ variables: values }))
}
if (error) {
throw new Error({ message: error.toString() })
}
router.push(`/items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<MarkdownInput
label={<>text <small className='text-muted ml-2'>optional</small></>}
name='text'
as={TextareaAutosize}
minRows={4}
hint={editThreshold
? (
<span className='text-muted font-weight-bold'>
<Countdown
date={editThreshold}
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
/>
</span>
)
: null}
/>
<ActionTooltip>
<SubmitButton variant='secondary' className='mt-2'>{item ? 'save' : 'post'}</SubmitButton>
</ActionTooltip>
</Form>
)
}

View File

@ -2,8 +2,16 @@ import Link from 'next/link'
import styles from './item.module.css' import styles from './item.module.css'
import { timeSince } from '../lib/time' import { timeSince } from '../lib/time'
import UpVote from './upvote' import UpVote from './upvote'
import { useMe } from './me'
import { useState } from 'react'
import Countdown from 'react-countdown'
export default function Item ({ item, rank, children }) { export default function Item ({ item, rank, children }) {
const me = useMe()
const mine = me?.id === item.user.id
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
const [canEdit, setCanEdit] =
useState(mine && (Date.now() < editThreshold))
return ( return (
<> <>
{rank {rank
@ -43,6 +51,22 @@ export default function Item ({ item, rank, children }) {
<span> </span> <span> </span>
<span>{timeSince(new Date(item.createdAt))}</span> <span>{timeSince(new Date(item.createdAt))}</span>
</span> </span>
{canEdit &&
<>
<span> \ </span>
<Link href={`/items/${item.id}/edit`} passHref>
<a className='text-reset'>
edit
<Countdown
date={editThreshold}
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
onComplete={() => {
setCanEdit(false)
}}
/>
</a>
</Link>
</>}
</div> </div>
</div> </div>
</div> </div>

107
components/link-form.js Normal file
View File

@ -0,0 +1,107 @@
import { Form, Input, SubmitButton } from '../components/form'
import { useRouter } from 'next/router'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import { ensureProtocol } from '../lib/url'
import ActionTooltip from '../components/action-tooltip'
import Countdown from 'react-countdown'
export const LinkSchema = Yup.object({
title: Yup.string().required('required').trim(),
url: Yup.string().test({
name: 'url',
test: (value) => {
try {
value = ensureProtocol(value)
const valid = new URL(value)
return Boolean(valid)
} catch {
return false
}
},
message: 'invalid url'
}).required('required')
})
export function LinkForm ({ item, editThreshold }) {
const router = useRouter()
const [createLink] = useMutation(
gql`
mutation createLink($title: String!, $url: String!) {
createLink(title: $title, url: $url) {
id
}
}`
)
const [updateLink] = useMutation(
gql`
mutation updateLink($id: ID!, $title: String!, $url: String!) {
updateLink(id: $id, title: $title, url: $url) {
id
title
url
}
}`, {
update (cache, { data: { updateLink } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
title () {
return updateLink.title
},
url () {
return updateLink.url
}
}
})
}
}
)
return (
<Form
initial={{
title: item?.title || '',
url: item?.url || ''
}}
schema={LinkSchema}
onSubmit={async (values) => {
let id, error
if (item) {
({ data: { updateLink: { id } }, error } = await updateLink({ variables: { ...values, id: item.id } }))
} else {
({ data: { createLink: { id } }, error } = await createLink({ variables: values }))
}
if (error) {
throw new Error({ message: error.toString() })
}
router.push(`/items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<Input
label='url'
name='url'
required
hint={editThreshold
? (
<span className='text-muted font-weight-bold'>
<Countdown
date={editThreshold}
renderer={props => <span> {props.formatted.minutes}:{props.formatted.seconds}</span>}
/>
</span>
)
: null}
/>
<ActionTooltip>
<SubmitButton variant='secondary' className='mt-2'>{item ? 'save' : 'post'}</SubmitButton>
</ActionTooltip>
</Form>
)
}

44
pages/items/[id]/edit.js Normal file
View File

@ -0,0 +1,44 @@
import { ITEM_FIELDS } from '../../../fragments/items'
import { gql } from '@apollo/client'
import ApolloClient from '../../../api/client'
import { DiscussionForm } from '../../../components/discussion-form'
import { LinkForm } from '../../../components/link-form'
import LayoutCenter from '../../../components/layout-center'
export async function getServerSideProps ({ req, params: { id } }) {
const { error, data: { item } } = await (await ApolloClient(req)).query({
query:
gql`
${ITEM_FIELDS}
{
item(id: ${id}) {
...ItemFields
text
}
}`
})
if (!item || error) {
return {
notFound: true
}
}
return {
props: {
item
}
}
}
export default function PostEdit ({ item }) {
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
return (
<LayoutCenter>
{item.url
? <LinkForm item={item} editThreshold={editThreshold} />
: <DiscussionForm item={item} editThreshold={editThreshold} />}
</LayoutCenter>
)
}

View File

@ -1,124 +1,10 @@
import Button from 'react-bootstrap/Button' import Button from 'react-bootstrap/Button'
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Link from 'next/link' import Link from 'next/link'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import LayoutCenter from '../components/layout-center' import LayoutCenter from '../components/layout-center'
import { ensureProtocol } from '../lib/url'
import { useMe } from '../components/me' import { useMe } from '../components/me'
import ActionTooltip from '../components/action-tooltip' import { DiscussionForm } from '../components/discussion-form'
import TextareaAutosize from 'react-textarea-autosize' import { LinkForm } from '../components/link-form'
export const DiscussionSchema = Yup.object({
title: Yup.string().required('required').trim()
})
export function DiscussionForm () {
const router = useRouter()
const [createDiscussion] = useMutation(
gql`
mutation createDiscussion($title: String!, $text: String) {
createDiscussion(title: $title, text: $text) {
id
}
}`
)
return (
<Form
initial={{
title: '',
text: ''
}}
schema={DiscussionSchema}
onSubmit={async (values) => {
const { data: { createDiscussion: { id } }, error } = await createDiscussion({ variables: values })
if (error) {
throw new Error({ message: error.toString() })
}
router.push(`items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<MarkdownInput
label={<>text <small className='text-muted ml-2'>optional</small></>}
name='text'
as={TextareaAutosize}
minRows={4}
/>
<ActionTooltip>
<SubmitButton variant='secondary' className='mt-2'>post</SubmitButton>
</ActionTooltip>
</Form>
)
}
export const LinkSchema = Yup.object({
title: Yup.string().required('required').trim(),
url: Yup.string().test({
name: 'url',
test: (value) => {
try {
value = ensureProtocol(value)
const valid = new URL(value)
return Boolean(valid)
} catch {
return false
}
},
message: 'invalid url'
}).required('required')
})
export function LinkForm () {
const router = useRouter()
const [createLink] = useMutation(
gql`
mutation createLink($title: String!, $url: String!) {
createLink(title: $title, url: $url) {
id
}
}`
)
return (
<Form
initial={{
title: '',
url: ''
}}
schema={LinkSchema}
onSubmit={async (values) => {
const { data: { createLink: { id } }, error } = await createLink({ variables: values })
if (error) {
throw new Error({ message: error.toString() })
}
router.push(`items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<Input
label='url'
name='url'
required
/>
<ActionTooltip>
<SubmitButton variant='secondary' className='mt-2'>post</SubmitButton>
</ActionTooltip>
</Form>
)
}
export function PostForm () { export function PostForm () {
const router = useRouter() const router = useRouter()