stacker.news/components/user-header.js

253 lines
8.3 KiB
JavaScript
Raw Normal View History

2022-05-16 20:51:22 +00:00
import { Button, InputGroup, Image, Modal, Form as BootstrapForm } from 'react-bootstrap'
2021-04-22 22:14:32 +00:00
import Link from 'next/link'
import { useRouter } from 'next/router'
import Nav from 'react-bootstrap/Nav'
2022-05-16 20:51:22 +00:00
import { useRef, useState } from 'react'
2021-05-21 22:32:21 +00:00
import { Form, Input, SubmitButton } from './form'
import * as Yup from 'yup'
2021-05-22 00:09:11 +00:00
import { gql, useApolloClient, useMutation } from '@apollo/client'
2021-05-25 00:08:56 +00:00
import styles from './user-header.module.css'
import { useMe } from './me'
2022-04-19 18:32:39 +00:00
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
2022-04-27 19:04:00 +00:00
import QRCode from 'qrcode.react'
import LightningIcon from '../svgs/bolt.svg'
import ModalButton from './modal-button'
import { encodeLNUrl } from '../lib/lnurl'
2022-05-16 20:51:22 +00:00
import Upload from './upload'
import EditImage from '../svgs/image-edit-fill.svg'
import Moon from '../svgs/moon-fill.svg'
import AvatarEditor from 'react-avatar-editor'
2021-05-22 00:09:11 +00:00
2021-04-22 22:14:32 +00:00
export default function UserHeader ({ user }) {
2021-05-21 22:32:21 +00:00
const [editting, setEditting] = useState(false)
const me = useMe()
2021-04-22 22:14:32 +00:00
const router = useRouter()
2021-05-21 22:32:21 +00:00
const client = useApolloClient()
2021-05-22 00:09:11 +00:00
const [setName] = useMutation(NAME_MUTATION)
2021-05-21 22:32:21 +00:00
2021-12-15 16:50:11 +00:00
const isMe = me?.name === user.name
2022-05-16 20:51:22 +00:00
const Satistics = () => <div className={`mb-2 ml-0 ml-sm-1 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
2021-05-21 22:32:21 +00:00
const UserSchema = Yup.object({
name: Yup.string()
.required('required')
.matches(/^[\w_]+$/, 'only letters, numbers, and _')
.max(32, 'too long')
.test({
name: 'name',
test: async name => {
if (!name || !name.length) return false
2021-09-30 15:46:58 +00:00
const { data } = await client.query({ query: NAME_QUERY, variables: { name }, fetchPolicy: 'network-only' })
2021-05-21 22:32:21 +00:00
return data.nameAvailable
},
message: 'taken'
})
})
const lnurlp = encodeLNUrl(new URL(`https://stacker.news/.well-known/lnurlp/${user.name}`))
2021-04-22 22:14:32 +00:00
return (
<>
2022-05-16 20:51:22 +00:00
<div className='d-flex mt-2 flex-wrap flex-column flex-sm-row'>
<div className='position-relative' style={{ width: 'fit-content' }}>
<Image
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/${user.photoId}` : '/dorian400.jpg'} width='135' height='135'
className={styles.userimg}
/>
{isMe && <PhotoEditor userId={me.id} />}
</div>
<div className='ml-0 ml-sm-1 mt-3 mt-sm-0 justify-content-center align-self-sm-center'>
2022-04-27 19:04:00 +00:00
{editting
? (
<Form
schema={UserSchema}
initial={{
name: user.name
}}
validateImmediately
onSubmit={async ({ name }) => {
if (name === user.name) {
setEditting(false)
return
}
const { error } = await setName({ variables: { name } })
if (error) {
throw new Error({ message: error.toString() })
}
router.replace({
pathname: router.pathname,
query: { ...router.query, name }
})
2021-09-07 13:25:19 +00:00
2022-04-27 19:04:00 +00:00
client.writeFragment({
id: `User:${user.id}`,
fragment: gql`
2021-09-07 13:25:19 +00:00
fragment CurUser on User {
name
}
`,
2022-04-27 19:04:00 +00:00
data: {
name
}
})
2021-09-07 13:25:19 +00:00
2022-04-27 19:04:00 +00:00
setEditting(false)
}}
>
<div className='d-flex align-items-center mb-2'>
2022-04-27 19:04:00 +00:00
<Input
prepend=<InputGroup.Text>@</InputGroup.Text>
name='name'
autoFocus
groupClassName={styles.usernameForm}
showValid
/>
<SubmitButton variant='link' onClick={() => setEditting(true)}>save</SubmitButton>
</div>
</Form>
)
: (
<div className='d-flex align-items-center mb-2'>
2022-04-27 19:04:00 +00:00
<div className={styles.username}>@{user.name}</div>
{isMe &&
2022-05-16 20:51:22 +00:00
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
2021-09-24 21:28:21 +00:00
</div>
2022-04-27 19:04:00 +00:00
)}
<Satistics user={user} />
<ModalButton
clicker={
2022-05-16 20:51:22 +00:00
<Button className='font-weight-bold ml-0 ml-sm-2'>
<LightningIcon
width={20}
height={20}
className='mr-1'
/>{user.name}@stacker.news
</Button>
}
>
<a className='d-flex m-auto p-3' style={{ background: 'white', width: 'fit-content' }} href={`lightning:${lnurlp}`}>
<QRCode className='d-flex m-auto' value={lnurlp} renderAs='svg' size={300} />
</a>
<div className='text-center font-weight-bold text-muted mt-3'>click or scan</div>
</ModalButton>
2022-04-27 19:04:00 +00:00
</div>
2021-09-23 17:42:00 +00:00
</div>
2021-04-22 22:14:32 +00:00
<Nav
2021-09-23 17:42:00 +00:00
className={styles.nav}
2021-12-16 17:27:12 +00:00
activeKey={router.asPath.split('?')[0]}
2021-04-22 22:14:32 +00:00
>
<Nav.Item>
<Link href={'/' + user.name} passHref>
2021-09-23 17:42:00 +00:00
<Nav.Link>bio</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href={'/' + user.name + '/posts'} passHref>
2021-04-22 22:14:32 +00:00
<Nav.Link>{user.nitems} posts</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href={'/' + user.name + '/comments'} passHref>
<Nav.Link>{user.ncomments} comments</Nav.Link>
</Link>
</Nav.Item>
2021-12-15 16:50:11 +00:00
{isMe &&
<Nav.Item>
2021-12-16 20:02:17 +00:00
<Link href='/satistics?inc=invoice,withdrawal' passHref>
2021-12-16 17:27:12 +00:00
<Nav.Link eventKey='/satistics'>satistics</Nav.Link>
2021-12-15 16:50:11 +00:00
</Link>
</Nav.Item>}
2021-04-22 22:14:32 +00:00
</Nav>
</>
)
}
2022-05-16 20:51:22 +00:00
function PhotoEditor ({ userId }) {
const [uploading, setUploading] = useState()
const [editProps, setEditProps] = useState()
const ref = useRef()
const [scale, setScale] = useState(1)
const [setPhoto] = useMutation(
gql`
mutation setPhoto($photoId: ID!) {
setPhoto(photoId: $photoId)
}`, {
update (cache, { data: { setPhoto } }) {
cache.modify({
id: `User:${userId}`,
fields: {
photoId () {
return setPhoto
}
}
})
}
}
)
return (
<>
<Modal
show={!!editProps}
onHide={() => setEditProps(null)}
>
<div className='modal-close' onClick={() => setEditProps(null)}>X</div>
<Modal.Body className='text-right mt-1 p-4'>
<AvatarEditor
ref={ref} width={200} height={200}
image={editProps?.file}
scale={scale}
style={{
width: '100%',
height: 'auto'
}}
/>
<BootstrapForm.Group controlId='formBasicRange'>
<BootstrapForm.Control
type='range' onChange={e => setScale(parseFloat(e.target.value))}
min={1} max={2} step='0.05'
defaultValue={scale} custom
/>
</BootstrapForm.Group>
<Button onClick={() => {
ref.current.getImageScaledToCanvas().toBlob(blob => {
if (blob) {
editProps.upload(blob)
setEditProps(null)
}
}, 'image/jpeg')
}}
>save
</Button>
</Modal.Body>
</Modal>
<Upload
as={({ onClick }) =>
<div className='position-absolute p-1 bg-dark pointer' onClick={onClick} style={{ bottom: '0', right: '0' }}>
{uploading
? <Moon className='fill-white spin' />
: <EditImage className='fill-white' />}
</div>}
onError={e => {
console.log(e)
setUploading(false)
}}
onSelect={(file, upload) => {
setEditProps({ file, upload })
}}
onSuccess={async key => {
const { error } = await setPhoto({ variables: { photoId: key } })
if (error) {
console.log(error)
}
setUploading(false)
}}
onStarted={() => {
setUploading(true)
}}
/>
</>
)
}