user bios mostly working

This commit is contained in:
keyan 2021-09-24 16:28:21 -05:00
parent 027ba6a048
commit a339516a54
7 changed files with 147 additions and 90 deletions

View File

@ -2,21 +2,6 @@ import { AuthenticationError, UserInputError } from 'apollo-server-errors'
import { createMentions, getItem, SELECT } from './item' import { createMentions, getItem, SELECT } from './item'
import serialize from './serial' import serialize from './serial'
export const createBio = async (parent, { bio }, { me, models }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
`@${me.name}'s bio`, bio, Number(me.id)))
await createMentions(item, models)
item.comments = []
return item
}
export default { export default {
Query: { Query: {
me: async (parent, args, { models, me }) => me: async (parent, args, { models, me }) =>
@ -50,7 +35,31 @@ export default {
throw error throw error
} }
}, },
createBio: createBio upsertBio: async (parent, { bio }, { me, models }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
const user = await models.user.findUnique({ where: { id: me.id } })
let item
if (user.bioId) {
item = await models.item.update({
where: { id: Number(user.bioId) },
data: {
text: bio
}
})
} else {
([item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
`@${me.name}'s bio`, bio, Number(me.id))))
}
await createMentions(item, models)
return await models.user.findUnique({ where: { id: me.id } })
}
}, },
User: { User: {

View File

@ -10,11 +10,12 @@ export default gql`
extend type Mutation { extend type Mutation {
setName(name: String!): Boolean setName(name: String!): Boolean
createBio(bio: String!): Item! upsertBio(bio: String!): User!
} }
type User { type User {
id: ID! id: ID!
createdAt: String!
name: String name: String
nitems: Int! nitems: Int!
ncomments: Int! ncomments: Int!

View File

@ -41,7 +41,7 @@ function Parent ({ item, rootText }) {
export default function Comment ({ export default function Comment ({
item, children, replyOpen, includeParent, item, children, replyOpen, includeParent,
rootText, noComments rootText, noComments, noReply
}) { }) {
const [edit, setEdit] = useState() const [edit, setEdit] = useState()
const [collapse, setCollapse] = useState(false) const [collapse, setCollapse] = useState(false)
@ -130,9 +130,10 @@ export default function Comment ({
</div> </div>
</div> </div>
<div className={`${styles.children}`}> <div className={`${styles.children}`}>
{!noReply &&
<Reply <Reply
parentId={item.id} replyOpen={replyOpen} parentId={item.id} replyOpen={replyOpen}
/> />}
{children} {children}
<div className={`${styles.comments} ml-sm-1 ml-md-3`}> <div className={`${styles.comments} ml-sm-1 ml-md-3`}>
{item.comments && !noComments {item.comments && !noComments

View File

@ -1,18 +1,18 @@
import Item, { ItemSkeleton } from './item' import Item from './item'
import Reply, { ReplySkeleton } from './reply' import Reply from './reply'
import Comment from './comment' import Comment from './comment'
import Text from './text' import Text from './text'
import Comments, { CommentsSkeleton } from './comments' import Comments from './comments'
import { COMMENTS } from '../fragments/comments' import { COMMENTS } from '../fragments/comments'
import { ITEM_FIELDS } from '../fragments/items' import { ITEM_FIELDS } from '../fragments/items'
import { gql, useQuery } from '@apollo/client' import { gql, useQuery } from '@apollo/client'
import styles from '../styles/item.module.css' import styles from '../styles/item.module.css'
import { NOFOLLOW_LIMIT } from '../lib/constants' import { NOFOLLOW_LIMIT } from '../lib/constants'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Link from 'next/link'
import { useMe } from './me' import { useMe } from './me'
import { Button } from 'react-bootstrap'
function BioItem ({ item }) { function BioItem ({ item, handleClick }) {
const me = useMe() const me = useMe()
if (!item.text) { if (!item.text) {
return null return null
@ -22,9 +22,12 @@ function BioItem ({ item }) {
<> <>
<ItemText item={item} /> <ItemText item={item} />
{me?.name === item.user.name && {me?.name === item.user.name &&
<Link href={`/items/${item.id}/edit`} passHref> <Button
<a className='text-right'>edit bio</a> onClick={handleClick}
</Link>} size='md' variant='link'
className='text-right'
>edit bio
</Button>}
<Reply parentId={item.id} /> <Reply parentId={item.id} />
</> </>
) )
@ -43,12 +46,12 @@ function ItemText ({ item }) {
return <Text nofollow={item.sats + item.boost < NOFOLLOW_LIMIT}>{item.text}</Text> return <Text nofollow={item.sats + item.boost < NOFOLLOW_LIMIT}>{item.text}</Text>
} }
export default function ItemFull ({ item: qItem, bio }) { export default function ItemFull ({ item, bio, ...props }) {
const query = gql` const query = gql`
${ITEM_FIELDS} ${ITEM_FIELDS}
${COMMENTS} ${COMMENTS}
{ {
item(id: ${qItem.id}) { item(id: ${item.id}) {
...ItemFields ...ItemFields
text text
comments { comments {
@ -61,34 +64,27 @@ export default function ItemFull ({ item: qItem, bio }) {
const { error, data } = useQuery(query, { const { error, data } = useQuery(query, {
fetchPolicy: router.query.cache ? 'cache-first' : undefined fetchPolicy: router.query.cache ? 'cache-first' : undefined
}) })
if (error) return <div>Failed to load!</div> if (error) {
return <div>Failed to load!</div>
if (!data) {
return (
<div>
<ItemSkeleton>
<ReplySkeleton />
</ItemSkeleton>
<div className={styles.comments}>
<CommentsSkeleton />
</div>
</div>
)
} }
const { item } = data // XXX replace item with cache version
if (data) {
({ item } = data)
}
return ( return (
<> <>
{item.parentId {item.parentId
? <Comment item={item} replyOpen includeParent noComments /> ? <Comment item={item} replyOpen includeParent noComments {...props} />
: (bio : (bio
? <BioItem item={item} /> ? <BioItem item={item} {...props} />
: <TopLevelItem item={item} /> : <TopLevelItem item={item} {...props} />
)} )}
{item.comments &&
<div className={styles.comments}> <div className={styles.comments}>
<Comments comments={item.comments} /> <Comments comments={item.comments} />
</div> </div>}
</> </>
) )
} }

View File

@ -59,7 +59,6 @@ export default function UserHeader ({ user }) {
initial={{ initial={{
name: user.name name: user.name
}} }}
className='d-flex align-items-center'
validateImmediately validateImmediately
onSubmit={async ({ name }) => { onSubmit={async ({ name }) => {
if (name === user.name) { if (name === user.name) {
@ -88,6 +87,7 @@ export default function UserHeader ({ user }) {
setEditting(false) setEditting(false)
}} }}
> >
<div className='d-flex align-items-center'>
<Input <Input
prepend=<InputGroup.Text>@</InputGroup.Text> prepend=<InputGroup.Text>@</InputGroup.Text>
name='name' name='name'
@ -96,6 +96,7 @@ export default function UserHeader ({ user }) {
showValid showValid
/> />
<SubmitButton variant='link' onClick={() => setEditting(true)}>save</SubmitButton> <SubmitButton variant='link' onClick={() => setEditting(true)}>save</SubmitButton>
</div>
</Form> </Form>
) )
: ( : (

18
fragments/users.js Normal file
View File

@ -0,0 +1,18 @@
import { gql } from '@apollo/client'
import { ITEM_FIELDS } from './items'
export const USER_FIELDS = gql`
${ITEM_FIELDS}
fragment UserFields on User {
id
createdAt
name
nitems
ncomments
stacked
sats
bio {
...ItemFields
text
}
}`

View File

@ -1,37 +1,29 @@
import Layout from '../components/layout' import Layout from '../components/layout'
import { gql, useMutation } from '@apollo/client' import { gql, useMutation, useQuery } from '@apollo/client'
import ApolloClient from '../api/client' import ApolloClient from '../api/client'
import UserHeader from '../components/user-header' import UserHeader from '../components/user-header'
import Seo from '../components/seo' import Seo from '../components/seo'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import styles from '../styles/user.module.css' import styles from '../styles/user.module.css'
import { useState } from 'react' import { useState } from 'react'
import { useSession } from 'next-auth/client'
import { ITEM_FIELDS } from '../fragments/items'
import ItemFull from '../components/item-full' import ItemFull from '../components/item-full'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Form, MarkdownInput, SubmitButton } from '../components/form' import { Form, MarkdownInput, SubmitButton } from '../components/form'
import ActionTooltip from '../components/action-tooltip' import ActionTooltip from '../components/action-tooltip'
import TextareaAutosize from 'react-textarea-autosize' import TextareaAutosize from 'react-textarea-autosize'
import { useMe } from '../components/me' import { useMe } from '../components/me'
import { USER_FIELDS } from '../fragments/users'
import { useRouter } from 'next/router'
import { ITEM_FIELDS } from '../fragments/items'
export async function getServerSideProps ({ req, params }) { export async function getServerSideProps ({ req, params }) {
const { error, data: { user } } = await (await ApolloClient(req)).query({ const { error, data: { user } } = await (await ApolloClient(req)).query({
query: query:
gql` gql`
${ITEM_FIELDS} ${USER_FIELDS}
{ {
user(name: "${params.username}") { user(name: "${params.username}") {
id ...UserFields
createdAt
name
nitems
ncomments
stacked
sats
bio {
...ItemFields
}
} }
}` }`
}) })
@ -53,20 +45,25 @@ const BioSchema = Yup.object({
bio: Yup.string().required('required').trim() bio: Yup.string().required('required').trim()
}) })
export function BioForm () { export function BioForm ({ handleSuccess, bio }) {
const [createBio] = useMutation( const [upsertBio] = useMutation(
gql` gql`
mutation createBio($bio: String!) { ${ITEM_FIELDS}
createBio(bio: $bio) { mutation upsertBio($bio: String!) {
upsertBio(bio: $bio) {
id id
bio {
...ItemFields
text
}
} }
}`, { }`, {
update (cache, { data: { createBio } }) { update (cache, { data: { upsertBio } }) {
cache.modify({ cache.modify({
id: `User:${createBio.userId}`, id: `User:${upsertBio.id}`,
fields: { fields: {
bio () { bio () {
return createBio return upsertBio.bio
} }
} }
}) })
@ -78,14 +75,15 @@ export function BioForm () {
<div className={styles.createFormContainer}> <div className={styles.createFormContainer}>
<Form <Form
initial={{ initial={{
bio: '' bio: bio?.text || ''
}} }}
schema={BioSchema} schema={BioSchema}
onSubmit={async values => { onSubmit={async values => {
const { error } = await createBio({ variables: values }) const { error } = await upsertBio({ variables: values })
if (error) { if (error) {
throw new Error({ message: error.toString() }) throw new Error({ message: error.toString() })
} }
handleSuccess && handleSuccess()
}} }}
> >
<MarkdownInput <MarkdownInput
@ -94,7 +92,7 @@ export function BioForm () {
minRows={4} minRows={4}
/> />
<ActionTooltip> <ActionTooltip>
<SubmitButton variant='secondary' className='mt-3'>create</SubmitButton> <SubmitButton variant='secondary' className='mt-3'>{bio?.text ? 'save' : 'create'}</SubmitButton>
</ActionTooltip> </ActionTooltip>
</Form> </Form>
</div> </div>
@ -103,22 +101,55 @@ export function BioForm () {
export default function User ({ user }) { export default function User ({ user }) {
const [create, setCreate] = useState(false) const [create, setCreate] = useState(false)
const [session] = useSession() const [edit, setEdit] = useState(false)
const me = useMe() const me = useMe()
const query = gql`
${USER_FIELDS}
{
user(name: "${user.name}") {
...UserFields
}
}`
const router = useRouter()
const { error, data } = useQuery(query, {
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) {
return <div>Failed to load!</div>
}
// XXX replace item with cache version
if (data) {
({ user } = data)
}
const mine = me?.name === user.name
return ( return (
<Layout noSeo containClassName={styles.contain}> <Layout noSeo containClassName={styles.contain}>
<Seo user={user} /> <Seo user={user} />
<UserHeader user={user} /> <UserHeader user={user} />
{user.bio {user.bio
? <ItemFull item={user.bio} bio /> ? (edit
: (me?.name === user.name && ? (
<div className={styles.create}>
<BioForm bio={user.bio} handleSuccess={() => setEdit(false)} />
</div>)
: <ItemFull item={user.bio} bio handleClick={setEdit} />
)
: (mine &&
<div className={styles.create}> <div className={styles.create}>
{create {create
? <BioForm /> ? <BioForm handleSuccess={() => setCreate(false)} />
: ( : (
session?.user?.name === user.name && mine &&
<Button onClick={setCreate} size='md' variant='secondary'>create bio</Button> <Button
onClick={setCreate}
size='md' variant='secondary'
>create bio
</Button>
)} )}
</div>)} </div>)}
</Layout> </Layout>