job board enhancements
This commit is contained in:
parent
70cbdd057a
commit
cb313429d5
|
@ -135,13 +135,24 @@ export default {
|
|||
// we pull from their wallet
|
||||
// TODO: need to filter out by payment status
|
||||
items = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
AND status <> 'STOPPED'
|
||||
ORDER BY (CASE WHEN status = 'ACTIVE' THEN "maxBid" ELSE 0 END) DESC, created_at ASC
|
||||
SELECT *
|
||||
FROM (
|
||||
(${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
AND status = 'ACTIVE'
|
||||
ORDER BY "maxBid" DESC, created_at ASC)
|
||||
UNION ALL
|
||||
(${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
AND status = 'NOSATS'
|
||||
ORDER BY created_at DESC)
|
||||
) a
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
|
||||
break
|
||||
|
@ -447,7 +458,10 @@ export default {
|
|||
return await createItem(parent, data, { me, models })
|
||||
}
|
||||
},
|
||||
upsertJob: async (parent, { id, sub, title, company, location, remote, text, url, maxBid, status }, { me, models }) => {
|
||||
upsertJob: async (parent, {
|
||||
id, sub, title, company, location, remote,
|
||||
text, url, maxBid, status, logo
|
||||
}, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in to create job')
|
||||
}
|
||||
|
@ -483,7 +497,8 @@ export default {
|
|||
url,
|
||||
maxBid,
|
||||
subName: sub,
|
||||
userId: me.id
|
||||
userId: me.id,
|
||||
uploadId: logo
|
||||
}
|
||||
|
||||
if (id) {
|
||||
|
@ -837,7 +852,7 @@ export const SELECT =
|
|||
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
|
||||
"Item".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
|
||||
"Item".company, "Item".location, "Item".remote,
|
||||
"Item"."subName", "Item".status, ltree2text("Item"."path") AS "path"`
|
||||
"Item"."subName", "Item".status, "Item"."uploadId", ltree2text("Item"."path") AS "path"`
|
||||
|
||||
function newTimedOrderByWeightedSats (num) {
|
||||
return `
|
||||
|
|
|
@ -21,7 +21,8 @@ export default gql`
|
|||
extend type Mutation {
|
||||
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
|
||||
upsertDiscussion(id: ID, title: String!, text: String, boost: Int, forward: String): Item!
|
||||
upsertJob(id: ID, sub: ID!, title: String!, company: String!, location: String, remote: Boolean, text: String!, url: String!, maxBid: Int!, status: String): Item!
|
||||
upsertJob(id: ID, sub: ID!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
act(id: ID!, sats: Int): ItemActResult!
|
||||
|
@ -71,5 +72,6 @@ export default gql`
|
|||
remote: Boolean
|
||||
sub: Sub
|
||||
status: String
|
||||
uploadId: Int
|
||||
}
|
||||
`
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import AvatarEditor from 'react-avatar-editor'
|
||||
import { Button, Modal, Form as BootstrapForm } from 'react-bootstrap'
|
||||
import Upload from './upload'
|
||||
import EditImage from '../svgs/image-edit-fill.svg'
|
||||
import Moon from '../svgs/moon-fill.svg'
|
||||
|
||||
export default function Avatar ({ onSuccess }) {
|
||||
const [uploading, setUploading] = useState()
|
||||
const [editProps, setEditProps] = useState()
|
||||
const ref = useRef()
|
||||
const [scale, setScale] = useState(1)
|
||||
|
||||
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 => {
|
||||
onSuccess && onSuccess(key)
|
||||
setUploading(false)
|
||||
}}
|
||||
onStarted={() => {
|
||||
setUploading(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import Item, { ItemJob } from './item'
|
||||
import Item from './item'
|
||||
import ItemJob from './item-job'
|
||||
import Reply from './reply'
|
||||
import Comment from './comment'
|
||||
import Text from './text'
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import * as Yup from 'yup'
|
||||
import Toc from './table-of-contents'
|
||||
import { Button, Image } from 'react-bootstrap'
|
||||
import { SearchTitle } from './item'
|
||||
import styles from './item.module.css'
|
||||
import Link from 'next/link'
|
||||
import { timeSince } from '../lib/time'
|
||||
import EmailIcon from '../svgs/mail-open-line.svg'
|
||||
|
||||
export default function ItemJob ({ item, toc, rank, children }) {
|
||||
const isEmail = Yup.string().email().isValidSync(item.url)
|
||||
|
||||
return (
|
||||
<>
|
||||
{rank
|
||||
? (
|
||||
<div className={`${styles.rank} align-self-center`}>
|
||||
{rank}
|
||||
</div>)
|
||||
: <div />}
|
||||
<div className={`${styles.item} ${item.status === 'NOSATS' && !item.mine ? styles.itemDead : ''}`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a>
|
||||
<Image
|
||||
src={item.uploadId ? `https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/${item.uploadId}` : '/jobs-default.png'} width='42' height='42' className={styles.companyImage}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<div className={`${styles.hunk} align-self-center mb-0`}>
|
||||
<div className={`${styles.main} flex-wrap d-inline`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a className={`${styles.title} text-reset mr-2`}>
|
||||
{item.searchTitle
|
||||
? <SearchTitle title={item.searchTitle} />
|
||||
: (
|
||||
<>{item.title}</>)}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`${styles.other}`}>
|
||||
{item.status === 'NOSATS' &&
|
||||
<>
|
||||
<span>expired</span>
|
||||
{item.company && <span> \ </span>}
|
||||
</>}
|
||||
{item.company &&
|
||||
<>
|
||||
{item.company}
|
||||
</>}
|
||||
{(item.location || item.remote) &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
{`${item.location || ''}${item.location && item.remote ? ' or ' : ''}${item.remote ? 'Remote' : ''}`}
|
||||
</>}
|
||||
<wbr />
|
||||
<span> \ </span>
|
||||
<span>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
|
||||
</Link>
|
||||
</span>
|
||||
{item.mine &&
|
||||
<>
|
||||
<wbr />
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.id}/edit`} passHref>
|
||||
<a className='text-reset'>
|
||||
edit
|
||||
</a>
|
||||
</Link>
|
||||
{item.status !== 'ACTIVE' && <span className='font-weight-bold text-danger'> {item.status}</span>}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
{toc && <Toc text={item.text} />}
|
||||
</div>
|
||||
{children && (
|
||||
<div className={`${styles.children}`} style={{ marginLeft: 'calc(42px + .8rem)' }}>
|
||||
<div className='mb-3 d-flex'>
|
||||
<Button
|
||||
target='_blank' href={isEmail ? `mailto:${item.url}?subject=${encodeURIComponent(item.title)} via Stacker News` : item.url}
|
||||
>
|
||||
apply {isEmail && <EmailIcon className='ml-1' />}
|
||||
</Button>
|
||||
{isEmail && <div className='ml-3 align-self-center text-muted font-weight-bold'>{item.url}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -7,100 +7,14 @@ import Countdown from './countdown'
|
|||
import { NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import Pin from '../svgs/pushpin-fill.svg'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import { formatSats } from '../lib/format'
|
||||
import * as Yup from 'yup'
|
||||
import Briefcase from '../svgs/briefcase-4-fill.svg'
|
||||
import Toc from './table-of-contents'
|
||||
|
||||
function SearchTitle ({ title }) {
|
||||
export function SearchTitle ({ title }) {
|
||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||
return <mark key={`mark-${match}`}>{match}</mark>
|
||||
})
|
||||
}
|
||||
|
||||
export function ItemJob ({ item, toc, rank, children }) {
|
||||
const isEmail = Yup.string().email().isValidSync(item.url)
|
||||
|
||||
return (
|
||||
<>
|
||||
{rank
|
||||
? (
|
||||
<div className={styles.rank}>
|
||||
{rank}
|
||||
</div>)
|
||||
: <div />}
|
||||
<div className={`${styles.item} ${item.status === 'NOSATS' && !item.mine ? styles.itemDead : ''}`}>
|
||||
<Briefcase width={24} height={24} className={styles.case} />
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap d-inline`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a className={`${styles.title} text-reset mr-2`}>
|
||||
{item.searchTitle
|
||||
? <SearchTitle title={item.searchTitle} />
|
||||
: (
|
||||
<>{item.title}
|
||||
{item.company &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
{item.company}
|
||||
</>}
|
||||
{(item.location || item.remote) &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
{`${item.location || ''}${item.location && item.remote ? ' or ' : ''}${item.remote ? 'Remote' : ''}`}
|
||||
</>}
|
||||
</>)}
|
||||
</a>
|
||||
</Link>
|
||||
{/* eslint-disable-next-line */}
|
||||
<a
|
||||
className={`${styles.link}`}
|
||||
target='_blank' href={(isEmail ? 'mailto:' : '') + item.url}
|
||||
>
|
||||
apply
|
||||
</a>
|
||||
</div>
|
||||
<div className={`${styles.other}`}>
|
||||
{item.status !== 'NOSATS'
|
||||
? <span>{formatSats(item.maxBid)} sats per min</span>
|
||||
: <span>expired</span>}
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a className='text-reset'>{item.ncomments} comments</a>
|
||||
</Link>
|
||||
<span> \ </span>
|
||||
<span>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
|
||||
</Link>
|
||||
</span>
|
||||
{item.mine &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
<Link href={`/items/${item.id}/edit`} passHref>
|
||||
<a className='text-reset'>
|
||||
edit
|
||||
</a>
|
||||
</Link>
|
||||
{item.status !== 'ACTIVE' && <span className='font-weight-bold text-danger'> {item.status}</span>}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
{toc && <Toc text={item.text} />}
|
||||
</div>
|
||||
{children && (
|
||||
<div className={`${styles.children}`}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FwdUser ({ user }) {
|
||||
return (
|
||||
<div className={styles.other}>
|
||||
|
|
|
@ -41,13 +41,20 @@ a.link:visited {
|
|||
.other {
|
||||
font-size: 80%;
|
||||
color: var(--theme-grey);
|
||||
margin-bottom: .15rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
margin-bottom: .45rem;
|
||||
}
|
||||
|
||||
.item .companyImage {
|
||||
border-radius: 100%;
|
||||
align-self: center;
|
||||
margin-right: 0.5rem;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
.itemDead {
|
||||
|
@ -62,10 +69,17 @@ a.link:visited {
|
|||
.hunk {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: .3rem;
|
||||
line-height: 1.06rem;
|
||||
}
|
||||
|
||||
/* .itemJob .hunk {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.itemJob .rank {
|
||||
align-self: center;
|
||||
} */
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useQuery } from '@apollo/client'
|
||||
import Item, { ItemJob, ItemSkeleton } from './item'
|
||||
import Item, { ItemSkeleton } from './item'
|
||||
import ItemJob from './item-job'
|
||||
import styles from './items.module.css'
|
||||
import { ITEMS } from '../fragments/items'
|
||||
import MoreFooter from './more-footer'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { InputGroup, Form as BForm, Col } from 'react-bootstrap'
|
||||
import { InputGroup, Form as BForm, Col, Image } from 'react-bootstrap'
|
||||
import * as Yup from 'yup'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Info from './info'
|
||||
|
@ -10,6 +10,7 @@ import { useLazyQuery, gql, useMutation } from '@apollo/client'
|
|||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { usePrice } from './price'
|
||||
import Avatar from './avatar'
|
||||
|
||||
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
||||
return this.test({
|
||||
|
@ -47,6 +48,7 @@ export default function JobForm ({ item, sub }) {
|
|||
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
||||
const router = useRouter()
|
||||
const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || sub.baseCost))
|
||||
const [logoId, setLogoId] = useState(item?.uploadId)
|
||||
const [getAuctionPosition, { data }] = useLazyQuery(gql`
|
||||
query AuctionPosition($id: ID, $bid: Int!) {
|
||||
auctionPosition(sub: "${sub.name}", id: $id, bid: $bid)
|
||||
|
@ -54,10 +56,10 @@ export default function JobForm ({ item, sub }) {
|
|||
{ fetchPolicy: 'network-only' })
|
||||
const [upsertJob] = useMutation(gql`
|
||||
mutation upsertJob($id: ID, $title: String!, $company: String!, $location: String,
|
||||
$remote: Boolean, $text: String!, $url: String!, $maxBid: Int!, $status: String) {
|
||||
$remote: Boolean, $text: String!, $url: String!, $maxBid: Int!, $status: String, $logo: Int) {
|
||||
upsertJob(sub: "${sub.name}", id: $id, title: $title, company: $company,
|
||||
location: $location, remote: $remote, text: $text,
|
||||
url: $url, maxBid: $maxBid, status: $status) {
|
||||
url: $url, maxBid: $maxBid, status: $status, logo: $logo) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
|
@ -122,6 +124,7 @@ export default function JobForm ({ item, sub }) {
|
|||
sub: sub.name,
|
||||
maxBid: Number(maxBid),
|
||||
status,
|
||||
logo: Number(logoId),
|
||||
...values
|
||||
}
|
||||
})
|
||||
|
@ -136,6 +139,15 @@ export default function JobForm ({ item, sub }) {
|
|||
}
|
||||
})}
|
||||
>
|
||||
<div className='form-group'>
|
||||
<label className='form-label'>logo</label>
|
||||
<div className='position-relative' style={{ width: 'fit-content' }}>
|
||||
<Image
|
||||
src={logoId ? `https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/${logoId}` : '/jobs-default.png'} width='135' height='135' roundedCircle
|
||||
/>
|
||||
<Avatar onSuccess={setLogoId} />
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label='job title'
|
||||
name='title'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useQuery } from '@apollo/client'
|
||||
import Comment, { CommentSkeleton } from './comment'
|
||||
import Item, { ItemJob } from './item'
|
||||
import Item from './item'
|
||||
import ItemJob from './item-job'
|
||||
import { NOTIFICATIONS } from '../fragments/notifications'
|
||||
import { useRouter } from 'next/router'
|
||||
import MoreFooter from './more-footer'
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function Toc ({ text }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dropdown alignRight>
|
||||
<Dropdown alignRight className='d-flex align-items-center'>
|
||||
<Dropdown.Toggle as={CustomToggle} id='dropdown-custom-components'>
|
||||
<TocIcon className='mx-2 fill-grey theme' />
|
||||
</Dropdown.Toggle>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, InputGroup, Image, Modal, Form as BootstrapForm } from 'react-bootstrap'
|
||||
import { Button, InputGroup, Image } from 'react-bootstrap'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import Nav from 'react-bootstrap/Nav'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Form, Input, SubmitButton } from './form'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
|
@ -13,10 +13,7 @@ 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'
|
||||
import Avatar from './avatar'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const [editting, setEditting] = useState(false)
|
||||
|
@ -25,6 +22,24 @@ export default function UserHeader ({ user }) {
|
|||
const client = useApolloClient()
|
||||
const [setName] = useMutation(NAME_MUTATION)
|
||||
|
||||
const [setPhoto] = useMutation(
|
||||
gql`
|
||||
mutation setPhoto($photoId: ID!) {
|
||||
setPhoto(photoId: $photoId)
|
||||
}`, {
|
||||
update (cache, { data: { setPhoto } }) {
|
||||
cache.modify({
|
||||
id: `User:${user.id}`,
|
||||
fields: {
|
||||
photoId () {
|
||||
return setPhoto
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const isMe = me?.name === user.name
|
||||
const Satistics = () => <div className={`mb-2 ml-0 ml-sm-1 ${styles.username} text-success`}>{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked</div>
|
||||
|
||||
|
@ -54,7 +69,14 @@ export default function UserHeader ({ user }) {
|
|||
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} />}
|
||||
{isMe &&
|
||||
<Avatar onSuccess={async photoId => {
|
||||
const { error } = await setPhoto({ variables: { photoId } })
|
||||
if (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
<div className='ml-0 ml-sm-1 mt-3 mt-sm-0 justify-content-center align-self-sm-center'>
|
||||
{editting
|
||||
|
@ -161,92 +183,3 @@ 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)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const ITEM_FIELDS = gql`
|
|||
baseCost
|
||||
}
|
||||
status
|
||||
uploadId
|
||||
mine
|
||||
root {
|
||||
id
|
||||
|
|
|
@ -29,6 +29,11 @@ export const SUB_ITEMS = gql`
|
|||
cursor
|
||||
items {
|
||||
...ItemFields
|
||||
position
|
||||
},
|
||||
pins {
|
||||
...ItemFields
|
||||
position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[itemId]` on the table `Upload` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Item" ADD COLUMN "uploadId" INTEGER;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Upload.itemId_unique" ON "Upload"("itemId");
|
|
@ -72,7 +72,7 @@ model Upload {
|
|||
width Int?
|
||||
height Int?
|
||||
item Item? @relation(fields: [itemId], references: [id])
|
||||
itemId Int?
|
||||
itemId Int? @unique
|
||||
user User @relation(name: "Uploads", fields: [userId], references: [id])
|
||||
userId Int
|
||||
|
||||
|
@ -161,6 +161,8 @@ model Item {
|
|||
pinId Int?
|
||||
weightedVotes Float @default(0)
|
||||
boost Int @default(0)
|
||||
uploadId Int?
|
||||
upload Upload?
|
||||
|
||||
// if sub is null, this is the main sub
|
||||
sub Sub? @relation(fields: [subName], references: [name])
|
||||
|
@ -178,8 +180,7 @@ model Item {
|
|||
longitude Float?
|
||||
remote Boolean?
|
||||
|
||||
User User[]
|
||||
Upload Upload[]
|
||||
User User[]
|
||||
@@index([createdAt])
|
||||
@@index([userId])
|
||||
@@index([parentId])
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
|
@ -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="M2.243 6.854L11.49 1.31a1 1 0 0 1 1.029 0l9.238 5.545a.5.5 0 0 1 .243.429V20a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.283a.5.5 0 0 1 .243-.429zm16.103 1.39l-6.285 5.439-6.414-5.445-1.294 1.524 7.72 6.555 7.581-6.56-1.308-1.513z"/></svg>
|
After Width: | Height: | Size: 356 B |
|
@ -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="M2.243 6.854L11.49 1.31a1 1 0 0 1 1.029 0l9.238 5.545a.5.5 0 0 1 .243.429V20a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.283a.5.5 0 0 1 .243-.429zM4 8.133V19h16V8.132l-7.996-4.8L4 8.132zm8.06 5.565l5.296-4.463 1.288 1.53-6.57 5.537-6.71-5.53 1.272-1.544 5.424 4.47z"/></svg>
|
After Width: | Height: | Size: 391 B |
Loading…
Reference in New Issue