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 | ||||
|   } | ||||
| ` | ||||
|  | ||||
							
								
								
									
										74
									
								
								components/avatar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								components/avatar.js
									
									
									
									
									
										Normal file
									
								
							| @ -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' | ||||
|  | ||||
							
								
								
									
										96
									
								
								components/item-job.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								components/item-job.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
							
								
								
									
										11
									
								
								prisma/migrations/20220720211644_item_uploads/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								prisma/migrations/20220720211644_item_uploads/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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]) | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								public/jobs-default.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/jobs-default.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.1 KiB | 
							
								
								
									
										1
									
								
								svgs/mail-open-fill.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								svgs/mail-open-fill.svg
									
									
									
									
									
										Normal 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="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 | 
							
								
								
									
										1
									
								
								svgs/mail-open-line.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								svgs/mail-open-line.svg
									
									
									
									
									
										Normal 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="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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user