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 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 {
Query: {
me: async (parent, args, { models, me }) =>
@ -50,7 +35,31 @@ export default {
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: {

View File

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

View File

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

View File

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

View File

@ -59,7 +59,6 @@ export default function UserHeader ({ user }) {
initial={{
name: user.name
}}
className='d-flex align-items-center'
validateImmediately
onSubmit={async ({ name }) => {
if (name === user.name) {
@ -88,14 +87,16 @@ export default function UserHeader ({ user }) {
setEditting(false)
}}
>
<Input
prepend=<InputGroup.Text>@</InputGroup.Text>
name='name'
autoFocus
groupClassName={`mb-0 ${styles.username}`}
showValid
/>
<SubmitButton variant='link' onClick={() => setEditting(true)}>save</SubmitButton>
<div className='d-flex align-items-center'>
<Input
prepend=<InputGroup.Text>@</InputGroup.Text>
name='name'
autoFocus
groupClassName={`mb-0 ${styles.username}`}
showValid
/>
<SubmitButton variant='link' onClick={() => setEditting(true)}>save</SubmitButton>
</div>
</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 { gql, useMutation } from '@apollo/client'
import { gql, useMutation, useQuery } from '@apollo/client'
import ApolloClient from '../api/client'
import UserHeader from '../components/user-header'
import Seo from '../components/seo'
import { Button } from 'react-bootstrap'
import styles from '../styles/user.module.css'
import { useState } from 'react'
import { useSession } from 'next-auth/client'
import { ITEM_FIELDS } from '../fragments/items'
import ItemFull from '../components/item-full'
import * as Yup from 'yup'
import { Form, MarkdownInput, SubmitButton } from '../components/form'
import ActionTooltip from '../components/action-tooltip'
import TextareaAutosize from 'react-textarea-autosize'
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 }) {
const { error, data: { user } } = await (await ApolloClient(req)).query({
query:
gql`
${ITEM_FIELDS}
${USER_FIELDS}
{
user(name: "${params.username}") {
id
createdAt
name
nitems
ncomments
stacked
sats
bio {
...ItemFields
}
...UserFields
}
}`
})
@ -53,20 +45,25 @@ const BioSchema = Yup.object({
bio: Yup.string().required('required').trim()
})
export function BioForm () {
const [createBio] = useMutation(
export function BioForm ({ handleSuccess, bio }) {
const [upsertBio] = useMutation(
gql`
mutation createBio($bio: String!) {
createBio(bio: $bio) {
${ITEM_FIELDS}
mutation upsertBio($bio: String!) {
upsertBio(bio: $bio) {
id
bio {
...ItemFields
text
}
}
}`, {
update (cache, { data: { createBio } }) {
update (cache, { data: { upsertBio } }) {
cache.modify({
id: `User:${createBio.userId}`,
id: `User:${upsertBio.id}`,
fields: {
bio () {
return createBio
return upsertBio.bio
}
}
})
@ -78,14 +75,15 @@ export function BioForm () {
<div className={styles.createFormContainer}>
<Form
initial={{
bio: ''
bio: bio?.text || ''
}}
schema={BioSchema}
onSubmit={async values => {
const { error } = await createBio({ variables: values })
const { error } = await upsertBio({ variables: values })
if (error) {
throw new Error({ message: error.toString() })
}
handleSuccess && handleSuccess()
}}
>
<MarkdownInput
@ -94,7 +92,7 @@ export function BioForm () {
minRows={4}
/>
<ActionTooltip>
<SubmitButton variant='secondary' className='mt-3'>create</SubmitButton>
<SubmitButton variant='secondary' className='mt-3'>{bio?.text ? 'save' : 'create'}</SubmitButton>
</ActionTooltip>
</Form>
</div>
@ -103,22 +101,55 @@ export function BioForm () {
export default function User ({ user }) {
const [create, setCreate] = useState(false)
const [session] = useSession()
const [edit, setEdit] = useState(false)
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 (
<Layout noSeo containClassName={styles.contain}>
<Seo user={user} />
<UserHeader user={user} />
{user.bio
? <ItemFull item={user.bio} bio />
: (me?.name === user.name &&
? (edit
? (
<div className={styles.create}>
<BioForm bio={user.bio} handleSuccess={() => setEdit(false)} />
</div>)
: <ItemFull item={user.bio} bio handleClick={setEdit} />
)
: (mine &&
<div className={styles.create}>
{create
? <BioForm />
? <BioForm handleSuccess={() => setCreate(false)} />
: (
session?.user?.name === user.name &&
<Button onClick={setCreate} size='md' variant='secondary'>create bio</Button>
mine &&
<Button
onClick={setCreate}
size='md' variant='secondary'
>create bio
</Button>
)}
</div>)}
</Layout>