profile photos

This commit is contained in:
keyan 2022-05-16 15:51:22 -05:00
parent 9abc41b7b2
commit 29fb37b763
11 changed files with 386 additions and 112 deletions

View File

@ -42,7 +42,7 @@ export default {
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })
const res = await new Promise((resolve, reject) => {
s3.createPresignedPost({
Bucket: process.env.AWS_UPLOAD_BUCKET,
Bucket: process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET,
Fields: {
key: String(upload.id)
},

View File

@ -116,6 +116,18 @@ export default {
return true
},
setPhoto: async (parent, { photoId }, { me, models }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
await models.user.update({
where: { id: me.id },
data: { photoId: Number(photoId) }
})
return Number(photoId)
},
upsertBio: async (parent, { bio }, { me, models }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')

View File

@ -30,6 +30,7 @@ export default gql`
setSettings(tipDefault: Int!, noteItemSats: Boolean!, noteEarning: Boolean!,
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!): Boolean
setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
}
@ -48,6 +49,7 @@ export default gql`
tipDefault: Int!
bio: Item
bioId: Int
photoId: Int
sats: Int!
upvotePopover: Boolean!
tipPopover: Boolean!

View File

@ -1,16 +1,8 @@
import { useRef, useState } from 'react'
import { useRef } from 'react'
import { gql, useMutation } from '@apollo/client'
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
import AddImage from '../svgs/image-add-fill.svg'
import Moon from '../svgs/moon-fill.svg'
import { Image as BImage } from 'react-bootstrap'
// what we want is that they supply a component that we turn into an upload button
// we need to return an error if the upload fails
// we need to report that the upload started
// we return the image id on success
export default function Upload ({ as: Component, onStarted, onError, onSuccess }) {
export default function Upload ({ as: Component, onSelect, onStarted, onError, onSuccess }) {
const [getSignedPOST] = useMutation(
gql`
mutation getSignedPOST($type: String!, $size: Int!, $width: Int!, $height: Int!) {
@ -21,98 +13,77 @@ export default function Upload ({ as: Component, onStarted, onError, onSuccess }
}`)
const ref = useRef()
const upload = file => {
onStarted && onStarted()
const img = new Image()
img.src = window.URL.createObjectURL(file)
img.onload = async () => {
let data
try {
({ data } = await getSignedPOST({
variables: {
type: file.type,
size: file.size,
width: img.width,
height: img.height
}
}))
} catch (e) {
onError && onError(e.toString())
return
}
const form = new FormData()
Object.keys(data.getSignedPOST.fields).forEach(key =>
form.append(key, data.getSignedPOST.fields[key]))
form.append('Content-Type', file.type)
form.append('acl', 'public-read')
form.append('file', file)
const res = await fetch(data.getSignedPOST.url, {
method: 'POST',
body: form
})
if (!res.ok) {
onError && onError(res.statusText)
return
}
onSuccess && onSuccess(data.getSignedPOST.fields.key)
}
}
return (
<>
<input
ref={ref}
type='file'
className='d-none'
accept={UPLOAD_TYPES_ALLOW.join(', ')}
onChange={(e) => {
if (e.target.files.length === 0) {
return
}
onStarted && onStarted()
const file = e.target.files[0]
if (UPLOAD_TYPES_ALLOW.indexOf(file.type) === -1) {
onError && onError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
return
}
const img = new Image()
img.src = window.URL.createObjectURL(file)
img.onload = async () => {
let data
try {
({ data } = await getSignedPOST({
variables: {
type: file.type,
size: file.size,
width: img.width,
height: img.height
}
}))
} catch (e) {
onError && onError(e.toString())
return
}
const form = new FormData()
Object.keys(data.getSignedPOST.fields).forEach(key =>
form.append(key, data.getSignedPOST.fields[key]))
form.append('Content-Type', file.type)
form.append('acl', 'public-read')
form.append('file', file)
const res = await fetch(data.getSignedPOST.url, {
method: 'POST',
body: form
})
if (!res.ok) {
onError && onError(res.statusText)
return
}
onSuccess && onSuccess(data.getSignedPOST.fields.key)
if (onSelect) {
onSelect(file, upload)
} else {
upload(file)
}
e.target.value = null
}}
/>
<Component onClick={() => ref.current?.click()} />
</>
)
}
export function UploadExample () {
const [error, setError] = useState()
const [key, setKey] = useState()
const [uploading, setUploading] = useState()
const Component = uploading
? ({ onClick }) => <Moon className='fill-grey spin' onClick={onClick} />
: ({ onClick }) => <AddImage className='fill-grey' onClick={onClick} />
return (
<>
<Upload
onError={e => {
setUploading(false)
setError(e)
}}
onSuccess={key => {
setUploading(false)
setKey(key)
}}
onStarted={() => {
setError(false)
setUploading(true)
}}
as={Component}
/>
<div>
{key && <BImage src={`https://sn-mtest.s3.amazonaws.com/${key}`} width='100%' />}
{error && <div>{error}</div>}
</div>
</>
)
}

View File

@ -1,20 +1,22 @@
import { Button } from 'react-bootstrap'
import { Button, InputGroup, Image, Modal, Form as BootstrapForm } from 'react-bootstrap'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Nav from 'react-bootstrap/Nav'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { Form, Input, SubmitButton } from './form'
import InputGroup from 'react-bootstrap/InputGroup'
import * as Yup from 'yup'
import { gql, useApolloClient, useMutation } from '@apollo/client'
import styles from './user-header.module.css'
import { useMe } from './me'
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
// import Image from 'next/image'
import QRCode from 'qrcode.react'
import LightningIcon from '../svgs/bolt.svg'
import ModalButton from './modal-button'
import { encodeLNUrl } from '../lib/lnurl'
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'
export default function UserHeader ({ user }) {
const [editting, setEditting] = useState(false)
@ -24,7 +26,7 @@ export default function UserHeader ({ user }) {
const [setName] = useMutation(NAME_MUTATION)
const isMe = me?.name === user.name
const Satistics = () => <div className={`mb-2 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
const Satistics = () => <div className={`mb-2 ml-0 ml-sm-1 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
const UserSchema = Yup.object({
name: Yup.string()
@ -46,12 +48,15 @@ export default function UserHeader ({ user }) {
return (
<>
<div className='d-flex align-items-center mt-2 flex-wrap'>
{/* <Image
src='/dorian400.jpg' width='135' height='135' layout='fixed'
className={styles.userimg}
/> */}
<div>
<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'>
{editting
? (
<Form
@ -105,13 +110,13 @@ export default function UserHeader ({ user }) {
<div className='d-flex align-items-center mb-2'>
<div className={styles.username}>@{user.name}</div>
{isMe &&
<Button className='py-0' variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
</div>
)}
<Satistics user={user} />
<ModalButton
clicker={
<Button className='font-weight-bold'>
<Button className='font-weight-bold ml-0 ml-sm-2'>
<LightningIcon
width={20}
height={20}
@ -156,3 +161,92 @@ export default function UserHeader ({ user }) {
</>
)
}
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)
}}
/>
</>
)
}

View File

@ -14,6 +14,7 @@ export const ME = gql`
hasNewNotes
tipDefault
bioId
photoId
hasInvites
upvotePopover
tipPopover
@ -39,6 +40,7 @@ export const ME_SSR = gql`
freeComments
tipDefault
bioId
photoId
upvotePopover
tipPopover
noteItemSats
@ -76,6 +78,7 @@ export const USER_FIELDS = gql`
ncomments
stacked
sats
photoId
bio {
...ItemFields
text

212
package-lock.json generated
View File

@ -81,27 +81,84 @@
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.14.5"
}
},
"@babel/compat-data": {
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz",
"integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw=="
},
"@babel/generator": {
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz",
"integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==",
"dev": true,
"requires": {
"@babel/types": "^7.15.4",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/helper-compilation-targets": {
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz",
"integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==",
"requires": {
"@babel/compat-data": "^7.17.10",
"@babel/helper-validator-option": "^7.16.7",
"browserslist": "^4.20.2",
"semver": "^6.3.0"
},
"dependencies": {
"browserslist": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
"integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==",
"requires": {
"caniuse-lite": "^1.0.30001332",
"electron-to-chromium": "^1.4.118",
"escalade": "^3.1.1",
"node-releases": "^2.0.3",
"picocolors": "^1.0.0"
}
},
"caniuse-lite": {
"version": "1.0.30001341",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz",
"integrity": "sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA=="
},
"electron-to-chromium": {
"version": "1.4.137",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz",
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA=="
},
"node-releases": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz",
"integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ=="
}
}
},
"@babel/helper-define-polyfill-provider": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz",
"integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==",
"requires": {
"@babel/helper-compilation-targets": "^7.13.0",
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/traverse": "^7.13.0",
"debug": "^4.1.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2",
"semver": "^6.1.2"
}
},
"@babel/helper-function-name": {
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz",
"integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.15.4",
"@babel/template": "^7.15.4",
@ -112,7 +169,6 @@
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz",
"integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==",
"dev": true,
"requires": {
"@babel/types": "^7.15.4"
}
@ -121,11 +177,34 @@
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz",
"integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==",
"dev": true,
"requires": {
"@babel/types": "^7.15.4"
}
},
"@babel/helper-module-imports": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz",
"integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==",
"requires": {
"@babel/types": "^7.16.7"
},
"dependencies": {
"@babel/helper-validator-identifier": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
"integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw=="
},
"@babel/types": {
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz",
"integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
}
}
}
},
"@babel/helper-plugin-utils": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz",
@ -135,7 +214,6 @@
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz",
"integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==",
"dev": true,
"requires": {
"@babel/types": "^7.15.4"
}
@ -145,6 +223,11 @@
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
"integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w=="
},
"@babel/helper-validator-option": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz",
"integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ=="
},
"@babel/highlight": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
@ -168,6 +251,26 @@
"@babel/helper-plugin-utils": "^7.14.5"
}
},
"@babel/plugin-transform-runtime": {
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz",
"integrity": "sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==",
"requires": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/helper-plugin-utils": "^7.16.7",
"babel-plugin-polyfill-corejs2": "^0.3.0",
"babel-plugin-polyfill-corejs3": "^0.5.0",
"babel-plugin-polyfill-regenerator": "^0.3.0",
"semver": "^6.3.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.16.7",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz",
"integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA=="
}
}
},
"@babel/runtime": {
"version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz",
@ -180,7 +283,6 @@
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz",
"integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.14.5",
"@babel/parser": "^7.15.4",
@ -191,7 +293,6 @@
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz",
"integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.14.5",
"@babel/generator": "^7.15.4",
@ -208,7 +309,6 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz",
"integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.15.7",
"to-fast-properties": "^2.0.0"
@ -1536,6 +1636,33 @@
"svgo": "^2.0.3"
}
},
"babel-plugin-polyfill-corejs2": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz",
"integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==",
"requires": {
"@babel/compat-data": "^7.13.11",
"@babel/helper-define-polyfill-provider": "^0.3.1",
"semver": "^6.1.1"
}
},
"babel-plugin-polyfill-corejs3": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz",
"integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==",
"requires": {
"@babel/helper-define-polyfill-provider": "^0.3.1",
"core-js-compat": "^3.21.0"
}
},
"babel-plugin-polyfill-regenerator": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz",
"integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==",
"requires": {
"@babel/helper-define-polyfill-provider": "^0.3.1"
}
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@ -2345,6 +2472,49 @@
"integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==",
"dev": true
},
"core-js-compat": {
"version": "3.22.5",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.5.tgz",
"integrity": "sha512-rEF75n3QtInrYICvJjrAgV03HwKiYvtKHdPtaba1KucG+cNZ4NJnH9isqt979e67KZlhpbCOTwnsvnIr+CVeOg==",
"requires": {
"browserslist": "^4.20.3",
"semver": "7.0.0"
},
"dependencies": {
"browserslist": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
"integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==",
"requires": {
"caniuse-lite": "^1.0.30001332",
"electron-to-chromium": "^1.4.118",
"escalade": "^3.1.1",
"node-releases": "^2.0.3",
"picocolors": "^1.0.0"
}
},
"caniuse-lite": {
"version": "1.0.30001341",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001341.tgz",
"integrity": "sha512-2SodVrFFtvGENGCv0ChVJIDQ0KPaS1cg7/qtfMaICgeMolDdo/Z2OD32F0Aq9yl6F4YFwGPBS5AaPqNYiW4PoA=="
},
"electron-to-chromium": {
"version": "1.4.137",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz",
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA=="
},
"node-releases": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz",
"integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ=="
},
"semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A=="
}
}
},
"core-js-pure": {
"version": "3.18.1",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.18.1.tgz",
@ -3689,8 +3859,7 @@
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"got": {
"version": "6.7.1",
@ -4425,8 +4594,7 @@
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"json-parse-better-errors": {
"version": "1.0.2",
@ -6635,6 +6803,11 @@
"split2": "^4.1.0"
}
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@ -7236,6 +7409,16 @@
"object-assign": "^4.1.1"
}
},
"react-avatar-editor": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz",
"integrity": "sha512-0xw63MbRRQdDy7YI1IXU9+7tTFxYEFLV8CABvryYOGjZmXRTH2/UA0mafe57ns62uaEFX181kA4XlGlxCaeXKA==",
"requires": {
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/runtime": "^7.12.5",
"prop-types": "^15.7.2"
}
},
"react-bootstrap": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.4.tgz",
@ -7902,8 +8085,7 @@
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"space-separated-tokens": {
"version": "1.1.5",

View File

@ -39,6 +39,7 @@
"prisma": "^2.25.0",
"qrcode.react": "^1.0.1",
"react": "^17.0.1",
"react-avatar-editor": "^13.0.0",
"react-bootstrap": "^1.5.2",
"react-countdown": "^2.3.2",
"react-dom": "^17.0.2",

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "photoId" INTEGER;
-- AddForeignKey
ALTER TABLE "users" ADD FOREIGN KEY ("photoId") REFERENCES "Upload"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -40,6 +40,8 @@ model User {
trust Float @default(0)
lastSeenAt DateTime?
lastCheckedJobs DateTime?
photoId Int?
photo Upload? @relation(fields: [photoId], references: [id])
upvotePopover Boolean @default(false)
tipPopover Boolean @default(false)
@ -54,7 +56,7 @@ model User {
noteJobIndicator Boolean @default(true)
Earn Earn[]
Upload Upload[]
Upload Upload[] @relation(name: "Uploads")
@@index([createdAt])
@@index([inviteId])
@@map(name: "users")
@ -70,9 +72,10 @@ model Upload {
height Int?
item Item? @relation(fields: [itemId], references: [id])
itemId Int?
user User @relation(fields: [userId], references: [id])
user User @relation(name: "Uploads", fields: [userId], references: [id])
userId Int
User User[]
@@index([createdAt])
@@index([itemId])
@@index([userId])

1
svgs/image-edit-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M20 3c.552 0 1 .448 1 1v1.757l-2 2V5H5v8.1l4-4 4.328 4.329-1.327 1.327-.006 4.239 4.246.006 1.33-1.33L18.899 19H19v-2.758l2-2V20c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h16zm1.778 4.808l1.414 1.414L15.414 17l-1.416-.002.002-1.412 7.778-7.778zM15.5 7c.828 0 1.5.672 1.5 1.5s-.672 1.5-1.5 1.5S14 9.328 14 8.5 14.672 7 15.5 7z"/></svg>

After

Width:  |  Height:  |  Size: 478 B