edit posts - links and discussions
This commit is contained in:
parent
fd79b92b9b
commit
a48cd33db3
|
@ -146,13 +146,65 @@ export default {
|
|||
|
||||
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) {
|
||||
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 })
|
||||
},
|
||||
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 }) => {
|
||||
if (!text) {
|
||||
throw new UserInputError('comment must have text', { argumentName: 'text' })
|
||||
|
@ -176,7 +228,7 @@ export default {
|
|||
// update iff this comment belongs to me
|
||||
const comment = await models.item.findUnique({ where: { id: Number(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) {
|
||||
|
|
|
@ -11,7 +11,9 @@ export default gql`
|
|||
|
||||
extend type Mutation {
|
||||
createLink(title: String!, url: String): Item!
|
||||
updateLink(id: ID!, title: String!, url: String): Item!
|
||||
createDiscussion(title: String!, text: String): Item!
|
||||
updateDiscussion(id: ID!, title: String!, text: String): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
vote(id: ID!, sats: Int): Int!
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -2,8 +2,16 @@ import Link from 'next/link'
|
|||
import styles from './item.module.css'
|
||||
import { timeSince } from '../lib/time'
|
||||
import UpVote from './upvote'
|
||||
import { useMe } from './me'
|
||||
import { useState } from 'react'
|
||||
import Countdown from 'react-countdown'
|
||||
|
||||
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 (
|
||||
<>
|
||||
{rank
|
||||
|
@ -43,6 +51,22 @@ export default function Item ({ item, rank, children }) {
|
|||
<span> </span>
|
||||
<span>{timeSince(new Date(item.createdAt))}</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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
118
pages/post.js
118
pages/post.js
|
@ -1,124 +1,10 @@
|
|||
import Button from 'react-bootstrap/Button'
|
||||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import LayoutCenter from '../components/layout-center'
|
||||
import { ensureProtocol } from '../lib/url'
|
||||
import { useMe } from '../components/me'
|
||||
import ActionTooltip from '../components/action-tooltip'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
import { DiscussionForm } from '../components/discussion-form'
|
||||
import { LinkForm } from '../components/link-form'
|
||||
|
||||
export function PostForm () {
|
||||
const router = useRouter()
|
||||
|
|
Loading…
Reference in New Issue