stacker.news/components/item-job.js
ekzyis 8f590425dc
Image uploads (#576)
* Add icon to add images

* Open file explorer to select image

* Upload images to S3 on selection

* Show uploaded images below text input

* Link and remove image

* Fetch unsubmitted images from database

* Mark S3 images as submitted in imgproxy job

* Add margin-top

* Mark images as submitted on client after successful mutation

* Also delete objects in S3

* Allow items to have multiple uploads linked

* Overwrite old avatar

* Add fees for presigned URLs

* Use Github style upload

* removed upfront fees
* removed images provider since we no longer need to keep track of unsubmitted images on the client
* removed User.images resolver
* removed deleteImage mutation
* use Github style upload where it shows ![Uploading <filename>...]() first and then replaces that with ![<filename>](<url>) after successful upload

* Add Upload.paid boolean column

One item can have multiple images linked to it, but an image can also be used in multiple items (many-to-many relation).

Since we don't really care to which item an image is linked and vice versa, we just use a boolean column to mark if an image was already paid for.

This makes fee calculation easier since no JOINs are required.

* Add image fees during item creation/update

* we calculate image fees during item creation and update now
* function imageFees returns queries which deduct fees from user and mark images as paid + fees
* queries need to be run inside same transaction as item creation/update

* Allow anons to get presigned URLs

* Add comments regarding avatar upload

* Use megabytes in error message

* Remove unnecessary avatar check during image fees calculation

* Show image fees in frontend

* Also update image fees on blur

This makes sure that the images fees reflect the current state. For example, if an image was removed.

We could also add debounced requests.

* Show amount of unpaid images in receipt

* Fix fees in sats deducted from msats

* Fix algebraic order of fees

Spam fees must come immediately after the base fee since it multiplies the base fee.

* Fix image fees in edit receipt

* Fix stale fees shown

If we pay for an image and then want to edit the comment, the cache might return stale date; suggesting we didn't pay for the existing image yet.

* Add 0 base fee in edit receipt

* Remove 's' from 'image fees' in receipts

* Remove unnecessary async

* Remove 'Uploading <name>...' from text input on error

* Support upload of multiple files at once

* Add schedule to delete unused images

* Fix image fee display in receipts

* Use Drag and Drop API for image upload

* Remove dragOver style on drop

* Increase max upload size to 10MB to allow HQ camera pictures

* Fix free upload quota

* Fix stale image fees served

* Fix bad image fee return statements

* Fix multiplication with feesPerImage

* Fix NULL returned for size24h, sizeNow

* Remove unnecessary text field in query

* refactor: Unify <ImageUpload> and <Upload> component

* Add avatar cache busting using random query param

* Calculate image fee info in postgres function

* we now calculate image fee info in a postgres function which is much cleaner
* we use this function inside `create_item` and `update_item`: image fees are now deducted in the same transaction as creating/updating the item!
* reversed changes in `serializeInvoiceable`

* Fix line break in receipt

* Update upload limits

* Add comment about `e.target.value = null`

* Use debounce instead of onBlur to update image fees info

* Fix invoice amount

* Refactor avatar upload control flow

* Update image fees in onChange

* Fix rescheduling of other jobs

* also update schedule from every minute to every hour

* Add image fees in calling context

* keep item ids on uploads

* Fix incompatible onSubmit signature

* Revert "keep item ids on uploads"

This reverts commit 4688962abcd54fdc5850109372a7ad054cf9b2e4.

* many2many item uploads

* pretty subdomain for images

* handle upload conditions for profile images and job logos

---------

Co-authored-by: ekzyis <ek@ekzyis.com>
Co-authored-by: ekzyis <ek@stacker.news>
2023-11-06 14:53:33 -06:00

96 lines
3.5 KiB
JavaScript

import { string } from 'yup'
import Toc from './table-of-contents'
import Badge from 'react-bootstrap/Badge'
import Button from 'react-bootstrap/Button'
import Image from 'react-bootstrap/Image'
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'
import Share from './share'
import Hat from './hat'
export default function ItemJob ({ item, toc, rank, children }) {
const isEmail = string().email().isValidSync(item.url)
return (
<>
{rank
? (
<div className={`${styles.rank} pb-2 align-self-center`}>
{rank}
</div>)
: <div />}
<div className={styles.item}>
<Link href={`/items/${item.id}`}>
<Image
src={item.uploadId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${item.uploadId}` : '/jobs-default.png'} width='42' height='42' className={styles.companyImage}
/>
</Link>
<div className={`${styles.hunk} align-self-center mb-0`}>
<div className={`${styles.main} flex-wrap d-inline`}>
<Link href={`/items/${item.id}`} className={`${styles.title} text-reset me-2`}>
{item.searchTitle
? <SearchTitle title={item.searchTitle} />
: (
<>{item.title}</>)}
</Link>
</div>
<div className={styles.other}>
{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}`} className='d-inline-flex align-items-center'>
@{item.user.name}<Hat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
</Link>
<span> </span>
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
{timeSince(new Date(item.createdAt))}
</Link>
</span>
{item.mine &&
(
<>
<wbr />
<span> \ </span>
<Link href={`/items/${item.id}/edit`} className='text-reset'>
edit
</Link>
{item.status !== 'ACTIVE' && <span className='ms-1 fw-bold text-boost'> {item.status}</span>}
</>)}
{item.maxBid > 0 && item.status === 'ACTIVE' && <Badge className={`${styles.newComment} ms-1`}>PROMOTED</Badge>}
</div>
</div>
{toc &&
<>
<Share item={item} />
<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='ms-1' />}
</Button>
{isEmail && <div className='ms-3 align-self-center text-muted fw-bold'>{item.url}</div>}
</div>
{children}
</div>
)}
</>
)
}