diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 2bb8f0d2..506f3405 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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 ` diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 642069a1..a02c3e74 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -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 } ` diff --git a/components/avatar.js b/components/avatar.js new file mode 100644 index 00000000..d9fd394a --- /dev/null +++ b/components/avatar.js @@ -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 ( + <> + setEditProps(null)} + > +
setEditProps(null)}>X
+ + + + setScale(parseFloat(e.target.value))} + min={1} max={2} step='0.05' + defaultValue={scale} custom + /> + + + +
+ +
+ {uploading + ? + : } +
} + onError={e => { + console.log(e) + setUploading(false) + }} + onSelect={(file, upload) => { + setEditProps({ file, upload }) + }} + onSuccess={async key => { + onSuccess && onSuccess(key) + setUploading(false) + }} + onStarted={() => { + setUploading(true) + }} + /> + + ) +} diff --git a/components/item-full.js b/components/item-full.js index cd402bff..fb945e38 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -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' diff --git a/components/item-job.js b/components/item-job.js new file mode 100644 index 00000000..4f3fd0ea --- /dev/null +++ b/components/item-job.js @@ -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 + ? ( +
+ {rank} +
) + :
} +
+ + + + + +
+
+ + + {item.searchTitle + ? + : ( + <>{item.title})} + + +
+
+ {item.status === 'NOSATS' && + <> + expired + {item.company && \ } + } + {item.company && + <> + {item.company} + } + {(item.location || item.remote) && + <> + \ + {`${item.location || ''}${item.location && item.remote ? ' or ' : ''}${item.remote ? 'Remote' : ''}`} + } + + \ + + + @{item.user.name} + + + + {timeSince(new Date(item.createdAt))} + + + {item.mine && + <> + + \ + + + edit + + + {item.status !== 'ACTIVE' && {item.status}} + } +
+
+ {toc && } +
+ {children && ( +
+
+ + {isEmail &&
{item.url}
} +
+ {children} +
+ )} + + ) +} diff --git a/components/item.js b/components/item.js index a77383a2..7a1a7344 100644 --- a/components/item.js +++ b/components/item.js @@ -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 {match} }) } -export function ItemJob ({ item, toc, rank, children }) { - const isEmail = Yup.string().email().isValidSync(item.url) - - return ( - <> - {rank - ? ( -
- {rank} -
) - :
} -
- -
- -
- {item.status !== 'NOSATS' - ? {formatSats(item.maxBid)} sats per min - : expired} - \ - - {item.ncomments} comments - - \ - - - @{item.user.name} - - - - {timeSince(new Date(item.createdAt))} - - - {item.mine && - <> - \ - - - edit - - - {item.status !== 'ACTIVE' && {item.status}} - } -
-
- {toc && } -
- {children && ( -
- {children} -
- )} - - ) -} - function FwdUser ({ user }) { return (
diff --git a/components/item.module.css b/components/item.module.css index 92828e9f..2f6e3c75 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -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; diff --git a/components/items.js b/components/items.js index a8aaef7b..4f4c5002 100644 --- a/components/items.js +++ b/components/items.js @@ -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' diff --git a/components/job-form.js b/components/job-form.js index e94c4572..122c2d35 100644 --- a/components/job-form.js +++ b/components/job-form.js @@ -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 }) { } })} > +
+ +
+ + +
+
+ diff --git a/components/user-header.js b/components/user-header.js index 9aec579b..525b0fb9 100644 --- a/components/user-header.js +++ b/components/user-header.js @@ -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 = () =>
{isMe ? `${user.sats} sats \\ ` : ''}{user.stacked} stacked
@@ -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 && } + {isMe && + { + const { error } = await setPhoto({ variables: { photoId } }) + if (error) { + console.log(error) + } + }} + />}
{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 ( - <> - setEditProps(null)} - > -
setEditProps(null)}>X
- - - - setScale(parseFloat(e.target.value))} - min={1} max={2} step='0.05' - defaultValue={scale} custom - /> - - - -
- -
- {uploading - ? - : } -
} - 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) - }} - /> - - ) -} diff --git a/fragments/items.js b/fragments/items.js index 23459c22..e131881a 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -31,6 +31,7 @@ export const ITEM_FIELDS = gql` baseCost } status + uploadId mine root { id diff --git a/fragments/subs.js b/fragments/subs.js index cea8ec61..fc6025d0 100644 --- a/fragments/subs.js +++ b/fragments/subs.js @@ -29,6 +29,11 @@ export const SUB_ITEMS = gql` cursor items { ...ItemFields + position + }, + pins { + ...ItemFields + position } } } diff --git a/prisma/migrations/20220720211644_item_uploads/migration.sql b/prisma/migrations/20220720211644_item_uploads/migration.sql new file mode 100644 index 00000000..c4b17159 --- /dev/null +++ b/prisma/migrations/20220720211644_item_uploads/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bca8d0c4..32980bb9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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]) diff --git a/public/jobs-default.png b/public/jobs-default.png new file mode 100644 index 00000000..89f6d793 Binary files /dev/null and b/public/jobs-default.png differ diff --git a/svgs/mail-open-fill.svg b/svgs/mail-open-fill.svg new file mode 100644 index 00000000..cd75d9cb --- /dev/null +++ b/svgs/mail-open-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/mail-open-line.svg b/svgs/mail-open-line.svg new file mode 100644 index 00000000..64b33032 --- /dev/null +++ b/svgs/mail-open-line.svg @@ -0,0 +1 @@ + \ No newline at end of file