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>
113 lines
4.0 KiB
JavaScript
113 lines
4.0 KiB
JavaScript
import Link from 'next/link'
|
|
import Image from 'react-bootstrap/Image'
|
|
import { abbrNum, numWithUnits } from '../lib/format'
|
|
import styles from './item.module.css'
|
|
import userStyles from './user-header.module.css'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { useQuery } from '@apollo/client'
|
|
import MoreFooter from './more-footer'
|
|
import { useData } from './use-data'
|
|
import Hat from './hat'
|
|
|
|
// all of this nonsense is to show the stat we are sorting by first
|
|
const Stacked = ({ user }) => (<span>{abbrNum(user.stacked)} stacked</span>)
|
|
const Spent = ({ user }) => (<span>{abbrNum(user.spent)} spent</span>)
|
|
const Posts = ({ user }) => (
|
|
<Link href={`/${user.name}/posts`} className='text-reset'>
|
|
{numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })}
|
|
</Link>)
|
|
const Comments = ({ user }) => (
|
|
<Link href={`/${user.name}/comments`} className='text-reset'>
|
|
{numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })}
|
|
</Link>)
|
|
const Referrals = ({ user }) => (<span>{numWithUnits(user.referrals, { unitSingular: 'referral', unitPlural: 'referrals' })}</span>)
|
|
const Seperator = () => (<span> \ </span>)
|
|
|
|
const STAT_POS = {
|
|
stacked: 0,
|
|
spent: 1,
|
|
posts: 2,
|
|
comments: 3,
|
|
referrals: 4
|
|
}
|
|
const STAT_COMPONENTS = [Stacked, Spent, Posts, Comments, Referrals]
|
|
|
|
function seperate (arr, seperator) {
|
|
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
|
|
}
|
|
|
|
export default function UserList ({ ssrData, query, variables, destructureData }) {
|
|
const { data, fetchMore } = useQuery(query, { variables })
|
|
const dat = useData(data, ssrData)
|
|
const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator))
|
|
|
|
useEffect(() => {
|
|
// shift the stat we are sorting by to the front
|
|
const comps = [...STAT_COMPONENTS]
|
|
setStatComps(seperate([...comps.splice(STAT_POS[variables?.by || 0], 1), ...comps], Seperator))
|
|
}, [variables?.by])
|
|
|
|
const { users, cursor } = useMemo(() => {
|
|
if (!dat) return {}
|
|
if (destructureData) {
|
|
return destructureData(dat)
|
|
} else {
|
|
return dat
|
|
}
|
|
}, [dat])
|
|
|
|
if (!dat) {
|
|
return <UsersSkeleton />
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{users?.map(user => (
|
|
<div className={`${styles.item} mb-2`} key={user.name}>
|
|
<Link href={`/${user.name}`}>
|
|
<Image
|
|
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
|
className={`${userStyles.userimg} me-2`}
|
|
/>
|
|
</Link>
|
|
<div className={styles.hunk}>
|
|
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
|
|
@{user.name}<Hat className='ms-1 fill-grey' height={14} width={14} user={user} />
|
|
</Link>
|
|
<div className={styles.other}>
|
|
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<MoreFooter cursor={cursor} count={users?.length} fetchMore={fetchMore} Skeleton={UsersSkeleton} noMoreText='NO MORE' />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function UsersSkeleton () {
|
|
const users = new Array(21).fill(null)
|
|
|
|
return (
|
|
<div>{users.map((_, i) => (
|
|
<div className={`${styles.item} ${styles.skeleton} mb-2`} key={i}>
|
|
<Image
|
|
src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/clouds.jpeg`}
|
|
width='32' height='32'
|
|
className={`${userStyles.userimg} clouds me-2`}
|
|
/>
|
|
<div className={styles.hunk}>
|
|
<div className={`${styles.name} clouds text-reset`} />
|
|
<div className={styles.other}>
|
|
<span className={`${styles.otherItem} clouds`} />
|
|
<span className={`${styles.otherItem} clouds`} />
|
|
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
|
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|