job board enhancements
This commit is contained in:
parent
70cbdd057a
commit
cb313429d5
|
@ -135,13 +135,24 @@ export default {
|
||||||
// we pull from their wallet
|
// we pull from their wallet
|
||||||
// TODO: need to filter out by payment status
|
// TODO: need to filter out by payment status
|
||||||
items = await models.$queryRaw(`
|
items = await models.$queryRaw(`
|
||||||
${SELECT}
|
SELECT *
|
||||||
FROM "Item"
|
FROM (
|
||||||
WHERE "parentId" IS NULL AND created_at <= $1
|
(${SELECT}
|
||||||
AND "pinId" IS NULL
|
FROM "Item"
|
||||||
${subClause(3)}
|
WHERE "parentId" IS NULL AND created_at <= $1
|
||||||
AND status <> 'STOPPED'
|
AND "pinId" IS NULL
|
||||||
ORDER BY (CASE WHEN status = 'ACTIVE' THEN "maxBid" ELSE 0 END) DESC, created_at ASC
|
${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
|
OFFSET $2
|
||||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
|
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub)
|
||||||
break
|
break
|
||||||
|
@ -447,7 +458,10 @@ export default {
|
||||||
return await createItem(parent, data, { me, models })
|
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) {
|
if (!me) {
|
||||||
throw new AuthenticationError('you must be logged in to create job')
|
throw new AuthenticationError('you must be logged in to create job')
|
||||||
}
|
}
|
||||||
|
@ -483,7 +497,8 @@ export default {
|
||||||
url,
|
url,
|
||||||
maxBid,
|
maxBid,
|
||||||
subName: sub,
|
subName: sub,
|
||||||
userId: me.id
|
userId: me.id,
|
||||||
|
uploadId: logo
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
|
@ -837,7 +852,7 @@ export const SELECT =
|
||||||
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
|
`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".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
|
||||||
"Item".company, "Item".location, "Item".remote,
|
"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) {
|
function newTimedOrderByWeightedSats (num) {
|
||||||
return `
|
return `
|
||||||
|
|
|
@ -21,7 +21,8 @@ export default gql`
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
|
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
|
||||||
upsertDiscussion(id: ID, title: String!, text: 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!
|
createComment(text: String!, parentId: ID!): Item!
|
||||||
updateComment(id: ID!, text: String!): Item!
|
updateComment(id: ID!, text: String!): Item!
|
||||||
act(id: ID!, sats: Int): ItemActResult!
|
act(id: ID!, sats: Int): ItemActResult!
|
||||||
|
@ -71,5 +72,6 @@ export default gql`
|
||||||
remote: Boolean
|
remote: Boolean
|
||||||
sub: Sub
|
sub: Sub
|
||||||
status: String
|
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 Reply from './reply'
|
||||||
import Comment from './comment'
|
import Comment from './comment'
|
||||||
import Text from './text'
|
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 { NOFOLLOW_LIMIT } from '../lib/constants'
|
||||||
import Pin from '../svgs/pushpin-fill.svg'
|
import Pin from '../svgs/pushpin-fill.svg'
|
||||||
import reactStringReplace from 'react-string-replace'
|
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'
|
import Toc from './table-of-contents'
|
||||||
|
|
||||||
function SearchTitle ({ title }) {
|
export function SearchTitle ({ title }) {
|
||||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||||
return <mark key={`mark-${match}`}>{match}</mark>
|
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 }) {
|
function FwdUser ({ user }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.other}>
|
<div className={styles.other}>
|
||||||
|
|
|
@ -41,13 +41,20 @@ a.link:visited {
|
||||||
.other {
|
.other {
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: var(--theme-grey);
|
color: var(--theme-grey);
|
||||||
margin-bottom: .15rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
margin-bottom: .45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item .companyImage {
|
||||||
|
border-radius: 100%;
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-left: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemDead {
|
.itemDead {
|
||||||
|
@ -62,10 +69,17 @@ a.link:visited {
|
||||||
.hunk {
|
.hunk {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: .3rem;
|
|
||||||
line-height: 1.06rem;
|
line-height: 1.06rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .itemJob .hunk {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemJob .rank {
|
||||||
|
align-self: center;
|
||||||
|
} */
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useQuery } from '@apollo/client'
|
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 styles from './items.module.css'
|
||||||
import { ITEMS } from '../fragments/items'
|
import { ITEMS } from '../fragments/items'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
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 * as Yup from 'yup'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
@ -10,6 +10,7 @@ import { useLazyQuery, gql, useMutation } from '@apollo/client'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePrice } from './price'
|
import { usePrice } from './price'
|
||||||
|
import Avatar from './avatar'
|
||||||
|
|
||||||
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
Yup.addMethod(Yup.string, 'or', function (schemas, msg) {
|
||||||
return this.test({
|
return this.test({
|
||||||
|
@ -47,6 +48,7 @@ export default function JobForm ({ item, sub }) {
|
||||||
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || sub.baseCost))
|
const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || sub.baseCost))
|
||||||
|
const [logoId, setLogoId] = useState(item?.uploadId)
|
||||||
const [getAuctionPosition, { data }] = useLazyQuery(gql`
|
const [getAuctionPosition, { data }] = useLazyQuery(gql`
|
||||||
query AuctionPosition($id: ID, $bid: Int!) {
|
query AuctionPosition($id: ID, $bid: Int!) {
|
||||||
auctionPosition(sub: "${sub.name}", id: $id, bid: $bid)
|
auctionPosition(sub: "${sub.name}", id: $id, bid: $bid)
|
||||||
|
@ -54,10 +56,10 @@ export default function JobForm ({ item, sub }) {
|
||||||
{ fetchPolicy: 'network-only' })
|
{ fetchPolicy: 'network-only' })
|
||||||
const [upsertJob] = useMutation(gql`
|
const [upsertJob] = useMutation(gql`
|
||||||
mutation upsertJob($id: ID, $title: String!, $company: String!, $location: String,
|
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,
|
upsertJob(sub: "${sub.name}", id: $id, title: $title, company: $company,
|
||||||
location: $location, remote: $remote, text: $text,
|
location: $location, remote: $remote, text: $text,
|
||||||
url: $url, maxBid: $maxBid, status: $status) {
|
url: $url, maxBid: $maxBid, status: $status, logo: $logo) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -122,6 +124,7 @@ export default function JobForm ({ item, sub }) {
|
||||||
sub: sub.name,
|
sub: sub.name,
|
||||||
maxBid: Number(maxBid),
|
maxBid: Number(maxBid),
|
||||||
status,
|
status,
|
||||||
|
logo: Number(logoId),
|
||||||
...values
|
...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
|
<Input
|
||||||
label='job title'
|
label='job title'
|
||||||
name='title'
|
name='title'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import Comment, { CommentSkeleton } from './comment'
|
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 { NOTIFICATIONS } from '../fragments/notifications'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function Toc ({ text }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown alignRight>
|
<Dropdown alignRight className='d-flex align-items-center'>
|
||||||
<Dropdown.Toggle as={CustomToggle} id='dropdown-custom-components'>
|
<Dropdown.Toggle as={CustomToggle} id='dropdown-custom-components'>
|
||||||
<TocIcon className='mx-2 fill-grey theme' />
|
<TocIcon className='mx-2 fill-grey theme' />
|
||||||
</Dropdown.Toggle>
|
</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 Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import Nav from 'react-bootstrap/Nav'
|
import Nav from 'react-bootstrap/Nav'
|
||||||
import { useRef, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Form, Input, SubmitButton } from './form'
|
import { Form, Input, SubmitButton } from './form'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
|
@ -13,10 +13,7 @@ import QRCode from 'qrcode.react'
|
||||||
import LightningIcon from '../svgs/bolt.svg'
|
import LightningIcon from '../svgs/bolt.svg'
|
||||||
import ModalButton from './modal-button'
|
import ModalButton from './modal-button'
|
||||||
import { encodeLNUrl } from '../lib/lnurl'
|
import { encodeLNUrl } from '../lib/lnurl'
|
||||||
import Upload from './upload'
|
import Avatar from './avatar'
|
||||||
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 }) {
|
export default function UserHeader ({ user }) {
|
||||||
const [editting, setEditting] = useState(false)
|
const [editting, setEditting] = useState(false)
|
||||||
|
@ -25,6 +22,24 @@ export default function UserHeader ({ user }) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const [setName] = useMutation(NAME_MUTATION)
|
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 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>
|
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'
|
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}
|
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>
|
||||||
<div className='ml-0 ml-sm-1 mt-3 mt-sm-0 justify-content-center align-self-sm-center'>
|
<div className='ml-0 ml-sm-1 mt-3 mt-sm-0 justify-content-center align-self-sm-center'>
|
||||||
{editting
|
{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
|
baseCost
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
|
uploadId
|
||||||
mine
|
mine
|
||||||
root {
|
root {
|
||||||
id
|
id
|
||||||
|
|
|
@ -29,6 +29,11 @@ export const SUB_ITEMS = gql`
|
||||||
cursor
|
cursor
|
||||||
items {
|
items {
|
||||||
...ItemFields
|
...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?
|
width Int?
|
||||||
height Int?
|
height Int?
|
||||||
item Item? @relation(fields: [itemId], references: [id])
|
item Item? @relation(fields: [itemId], references: [id])
|
||||||
itemId Int?
|
itemId Int? @unique
|
||||||
user User @relation(name: "Uploads", fields: [userId], references: [id])
|
user User @relation(name: "Uploads", fields: [userId], references: [id])
|
||||||
userId Int
|
userId Int
|
||||||
|
|
||||||
|
@ -161,6 +161,8 @@ model Item {
|
||||||
pinId Int?
|
pinId Int?
|
||||||
weightedVotes Float @default(0)
|
weightedVotes Float @default(0)
|
||||||
boost Int @default(0)
|
boost Int @default(0)
|
||||||
|
uploadId Int?
|
||||||
|
upload Upload?
|
||||||
|
|
||||||
// if sub is null, this is the main sub
|
// if sub is null, this is the main sub
|
||||||
sub Sub? @relation(fields: [subName], references: [name])
|
sub Sub? @relation(fields: [subName], references: [name])
|
||||||
|
@ -178,8 +180,7 @@ model Item {
|
||||||
longitude Float?
|
longitude Float?
|
||||||
remote Boolean?
|
remote Boolean?
|
||||||
|
|
||||||
User User[]
|
User User[]
|
||||||
Upload Upload[]
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([parentId])
|
@@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