8f590425dc
* 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>
96 lines
3.5 KiB
JavaScript
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>
|
|
)}
|
|
</>
|
|
)
|
|
}
|