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 })
|
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) {
|
||||||
|
@ -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!
|
||||||
|
93
components/discussion-form.js
Normal file
93
components/discussion-form.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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
107
components/link-form.js
Normal 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
44
pages/items/[id]/edit.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
118
pages/post.js
118
pages/post.js
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user