This commit is contained in:
keyan 2023-02-01 08:44:35 -06:00
parent a6ce93c2bb
commit 072e60c954
25 changed files with 322 additions and 40 deletions

View File

@ -181,6 +181,15 @@ export default {
GROUP BY "userId", created_at`
)
}
if (meFull.noteCowboyHat) {
queries.push(
`SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
FROM "Streak"
WHERE "userId" = $1
AND updated_at <= $2`
)
}
}
// we do all this crazy subquery stuff to make 'reward' islands
@ -227,6 +236,17 @@ export default {
JobChanged: {
item: async (n, args, { models }) => getItem(n, { id: n.id }, { models })
},
Streak: {
days: async (n, args, { models }) => {
const res = await models.$queryRaw`
SELECT "endedAt" - "startedAt" AS days
FROM "Streak"
WHERE id = ${Number(n.id)} AND "endedAt" IS NOT NULL
`
return res.length ? res[0].days : null
}
},
Earn: {
sources: async (n, args, { me, models }) => {
const [sources] = await models.$queryRaw(`

View File

@ -304,6 +304,21 @@ export default {
}
}
if (user.noteCowboyHat) {
const streak = await models.streak.findFirst({
where: {
userId: me.id,
updatedAt: {
gt: lastChecked
}
}
})
if (streak) {
return true
}
}
return false
},
searchUsers: async (parent, { q, limit, similarity }, { models }) => {
@ -475,6 +490,15 @@ export default {
}
})
},
streak: async (user, args, { models }) => {
const res = await models.$queryRaw`
SELECT (now_utc() at time zone 'America/Chicago')::date - "startedAt" AS days
FROM "Streak"
WHERE "userId" = ${user.id} AND "endedAt" IS NULL
`
return res.length ? res[0].days : null
},
stacked: async (user, { when }, { models }) => {
if (user.stacked) {
return user.stacked

View File

@ -38,6 +38,12 @@ export default gql`
tips: Int!
}
type Streak {
sortTime: String!
days: Int
id: ID!
}
type Earn {
earnedSats: Int!
sortTime: String!
@ -56,6 +62,7 @@ export default gql`
union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | Referral
| Streak
type Notifications {
lastChecked: String

View File

@ -21,7 +21,7 @@ export default gql`
setName(name: String!): Boolean
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!,
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!]): User
setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User!
@ -58,6 +58,7 @@ export default gql`
bio: Item
bioId: Int
photoId: Int
streak: Int
sats: Int!
upvotePopover: Boolean!
tipPopover: Boolean!
@ -68,6 +69,7 @@ export default gql`
noteDeposits: Boolean!
noteInvites: Boolean!
noteJobIndicator: Boolean!
noteCowboyHat: Boolean!
hideInvoiceDesc: Boolean!
hideFromTopUsers: Boolean!
wildWestMode: Boolean!

View File

@ -23,6 +23,7 @@ import { Badge } from 'react-bootstrap'
import { abbrNum } from '../lib/format'
import Share from './share'
import { DeleteDropdown } from './delete'
import CowboyHat from './cowboy-hat'
function Parent ({ item, rootText }) {
const ParentFrag = () => (
@ -135,7 +136,10 @@ export default function Comment ({
</Link>
<span> \ </span>
<Link href={`/${item.user.name}`} passHref>
<a>@{item.user.name}<span className='text-boost font-weight-bold'>{op && ' OP'}</span></a>
<a className='d-inline-flex align-items-center'>
@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} />
{op && <span className='text-boost font-weight-bold ml-1'>OP</span>}
</a>
</Link>
<span> </span>
<Link href={`/items/${item.id}`} passHref>

36
components/cowboy-hat.js Normal file
View File

@ -0,0 +1,36 @@
import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'
import CowboyHatIcon from '../svgs/cowboy.svg'
export default function CowboyHat ({ streak, badge, className = 'ml-1', height = 16, width = 16 }) {
if (!streak) {
return null
}
return (
<HatTooltip overlayText={`${streak} days`}>
{badge
? (
<Badge variant='grey-medium' className='ml-2 d-inline-flex align-items-center'>
<CowboyHatIcon className={className} height={height} width={width} />
<span className='ml-1'>{streak}</span>
</Badge>)
: <CowboyHatIcon className={className} height={height} width={width} />}
</HatTooltip>
)
}
function HatTooltip ({ children, overlayText, placement }) {
return (
<OverlayTrigger
placement={placement || 'bottom'}
overlay={
<Tooltip>
{overlayText || '1 sat'}
</Tooltip>
}
trigger={['hover', 'focus']}
>
{children}
</OverlayTrigger>
)
}

View File

@ -15,6 +15,7 @@ import { abbrNum } from '../lib/format'
import NoteIcon from '../svgs/notification-4-fill.svg'
import { useQuery, gql } from '@apollo/client'
import LightningIcon from '../svgs/bolt.svg'
import CowboyHat from './cowboy-hat'
function WalletSummary ({ me }) {
if (!me) return null
@ -72,7 +73,9 @@ export default function Header ({ sub }) {
<NavDropdown
className={styles.dropdown} title={
<Link href={`/${me?.name}`} passHref>
<Nav.Link eventKey={me?.name} as='div' className='p-0' onClick={e => e.preventDefault()}>{`@${me?.name}`}</Nav.Link>
<Nav.Link eventKey={me?.name} as='div' className='p-0 d-flex align-items-center' onClick={e => e.preventDefault()}>
{`@${me?.name}`}<CowboyHat streak={me.streak} />
</Nav.Link>
</Link>
} alignRight
>

View File

@ -7,6 +7,7 @@ import Link from 'next/link'
import { timeSince } from '../lib/time'
import EmailIcon from '../svgs/mail-open-line.svg'
import Share from './share'
import CowboyHat from './cowboy-hat'
export default function ItemJob ({ item, toc, rank, children }) {
const isEmail = Yup.string().email().isValidSync(item.url)
@ -52,7 +53,9 @@ export default function ItemJob ({ item, toc, rank, children }) {
<span> \ </span>
<span>
<Link href={`/${item.user.name}`} passHref>
<a>@{item.user.name}</a>
<a className='d-inline-flex align-items-center'>
@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} />
</a>
</Link>
<span> </span>
<Link href={`/items/${item.id}`} passHref>

View File

@ -19,6 +19,7 @@ import Flag from '../svgs/flag-fill.svg'
import Share from './share'
import { abbrNum } from '../lib/format'
import { DeleteDropdown } from './delete'
import CowboyHat from './cowboy-hat'
export function SearchTitle ({ title }) {
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
@ -115,7 +116,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
<span> \ </span>
<span>
<Link href={`/${item.user.name}`} passHref>
<a>@{item.user.name}</a>
<a className='d-inline-flex align-items-center'>@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} /></a>
</Link>
<span> </span>
<Link href={`/items/${item.id}`} passHref>

View File

@ -12,6 +12,8 @@ import Link from 'next/link'
import Check from '../svgs/check-double-line.svg'
import HandCoin from '../svgs/hand-coin-fill.svg'
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
import CowboyHatIcon from '../svgs/cowboy.svg'
import BaldIcon from '../svgs/bald.svg'
// TODO: oh man, this is a mess ... each notification type should just be a component ...
function Notification ({ n }) {
@ -20,7 +22,7 @@ function Notification ({ n }) {
<div
className='clickToContext'
onClick={e => {
if (n.__typename === 'Earn' || n.__typename === 'Referral') {
if (n.__typename === 'Earn' || n.__typename === 'Referral' || n.__typename === 'Streak') {
return
}
@ -103,35 +105,76 @@ function Notification ({ n }) {
<Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
<small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
</div>)
: (
<>
{n.__typename === 'Votification' &&
<small className='font-weight-bold text-success ml-2'>
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
</small>}
{n.__typename === 'Mention' &&
<small className='font-weight-bold text-info ml-2'>
you were mentioned in
</small>}
{n.__typename === 'JobChanged' &&
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
{n.item.status === 'ACTIVE'
? 'your job is active again'
: (n.item.status === 'NOSATS'
? 'your job promotion ran out of sats'
: 'your job has been stopped')}
</small>}
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
{n.item.isJob
? <ItemJob item={n.item} />
: n.item.title
? <Item item={n.item} />
: (
<div className='pb-2'>
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
</div>)}
</div>
</>)}
: n.__typename === 'Streak'
? <Streak n={n} />
: (
<>
{n.__typename === 'Votification' &&
<small className='font-weight-bold text-success ml-2'>
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
</small>}
{n.__typename === 'Mention' &&
<small className='font-weight-bold text-info ml-2'>
you were mentioned in
</small>}
{n.__typename === 'JobChanged' &&
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
{n.item.status === 'ACTIVE'
? 'your job is active again'
: (n.item.status === 'NOSATS'
? 'your job promotion ran out of sats'
: 'your job has been stopped')}
</small>}
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
{n.item.isJob
? <ItemJob item={n.item} />
: n.item.title
? <Item item={n.item} />
: (
<div className='pb-2'>
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
</div>)}
</div>
</>)}
</div>
)
}
function Streak ({ n }) {
function blurb (n) {
const index = Number(n.id) % 6
const FOUND_BLURBS = [
'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.',
'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.',
"This is not just a hat, it's a matter of survival. Take care of this essential tool, and it will shield you from the scorching sun and the elements.",
"A cowboy hat isn't just a fashion statement. It's your last defense against the unforgiving elements of the Wild West. Hang onto it tight.",
"A good cowboy hat is worth its weight in gold, shielding you from the sun, wind, and dust of the western frontier. Don't lose it.",
'Your cowboy hat is the key to your survival in the wild west. Treat it with respect and it will protect you from the elements.'
]
const LOST_BLURBS = [
'your cowboy hat was taken by the wind storm that blew in from the west. No worries, a true cowboy always finds another hat.',
"you left your trusty cowboy hat in the saloon before leaving town. You'll need a replacement for the long journey west.",
'you lost your cowboy hat in a wild shoot-out on the outskirts of town. Tough luck, tIme to start searching for another one.',
'you ran out of food and had to trade your hat for supplies. Better start looking for another hat.',
"your hat was stolen by a mischievous prairie dog. You won't catch the dog, but you can always find another hat.",
'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.'
]
if (n.days) {
return `After ${n.days} days, ` + LOST_BLURBS[index]
}
return FOUND_BLURBS[index]
}
return (
<div className='d-flex font-weight-bold ml-2 py-1'>
<div style={{ fontSize: '2rem' }}>{n.days ? <BaldIcon className='fill-grey' height={40} width={40} /> : <CowboyHatIcon className='fill-grey' height={40} width={40} />}</div>
<div className='ml-1 p-1'>
you {n.days ? 'lost your' : 'found a'} cowboy hat
<div><small style={{ lineHeight: '140%', display: 'inline-block' }}>{blurb(n)}</small></div>
</div>
</div>
)
}

View File

@ -14,6 +14,7 @@ import LightningIcon from '../svgs/bolt.svg'
import ModalButton from './modal-button'
import { encodeLNUrl } from '../lib/lnurl'
import Avatar from './avatar'
import CowboyHat from './cowboy-hat'
export default function UserHeader ({ user }) {
const [editting, setEditting] = useState(false)
@ -132,7 +133,7 @@ export default function UserHeader ({ user }) {
)
: (
<div className='d-flex align-items-center mb-2'>
<div className={styles.username}>@{user.name}</div>
<div className={styles.username}>@{user.name}<CowboyHat className='' streak={user.streak} badge /></div>
{isMe &&
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
</div>

View File

@ -1,6 +1,7 @@
import Link from 'next/link'
import { Image } from 'react-bootstrap'
import { abbrNum } from '../lib/format'
import CowboyHat from './cowboy-hat'
import styles from './item.module.css'
import userStyles from './user-header.module.css'
@ -18,8 +19,8 @@ export default function UserList ({ users }) {
</Link>
<div className={styles.hunk}>
<Link href={`/${user.name}`} passHref>
<a className={`${styles.title} text-reset`}>
@{user.name}
<a className={`${styles.title} d-inline-flex align-items-center text-reset`}>
@{user.name}<CowboyHat className='ml-1 fill-grey' height={14} width={14} streak={user.streak} />
</a>
</Link>
<div className={styles.other}>

View File

@ -9,6 +9,7 @@ export const COMMENT_FIELDS = gql`
text
user {
name
streak
id
}
sats
@ -29,6 +30,7 @@ export const COMMENT_FIELDS = gql`
bountyPaidTo
user {
name
streak
id
}
}

View File

@ -13,6 +13,7 @@ export const INVITE_FIELDS = gql`
revoked
user {
name
streak
id
}
poor

View File

@ -11,10 +11,12 @@ export const ITEM_FIELDS = gql`
url
user {
name
streak
id
}
fwdUser {
name
streak
id
}
sats
@ -51,6 +53,7 @@ export const ITEM_FIELDS = gql`
}
user {
name
streak
id
}
}

View File

@ -28,6 +28,11 @@ export const NOTIFICATIONS = gql`
text
}
}
... on Streak {
id
sortTime
days
}
... on Earn {
sortTime
earnedSats

View File

@ -7,6 +7,7 @@ export const ME = gql`
me {
id
name
streak
sats
stacked
freePosts
@ -24,6 +25,7 @@ export const ME = gql`
noteDeposits
noteInvites
noteJobIndicator
noteCowboyHat
hideInvoiceDesc
hideFromTopUsers
wildWestMode
@ -44,6 +46,7 @@ export const SETTINGS_FIELDS = gql`
noteDeposits
noteInvites
noteJobIndicator
noteCowboyHat
hideInvoiceDesc
hideFromTopUsers
nostrPubkey
@ -72,12 +75,12 @@ gql`
${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!,
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!]) {
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency,
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays) {
...SettingsFields
}
@ -103,6 +106,7 @@ gql`
query searchUsers($q: String!, $limit: Int, $similarity: Float) {
searchUsers(q: $q, limit: $limit, similarity: $similarity) {
name
streak
photoId
stacked
spent
@ -117,6 +121,7 @@ export const USER_FIELDS = gql`
id
createdAt
name
streak
nitems
ncomments
stacked
@ -133,6 +138,7 @@ export const TOP_USERS = gql`
topUsers(cursor: $cursor, when: $when, sort: $sort) {
users {
name
streak
photoId
stacked(when: $when)
spent(when: $when)

View File

@ -102,6 +102,7 @@ export default function Settings ({ data: { settings } }) {
noteDeposits: settings?.noteDeposits,
noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator,
noteCowboyHat: settings?.noteCowboyHat,
hideInvoiceDesc: settings?.hideInvoiceDesc,
hideFromTopUsers: settings?.hideFromTopUsers,
wildWestMode: settings?.wildWestMode,
@ -216,6 +217,11 @@ export default function Settings ({ data: { settings } }) {
<Checkbox
label='there is a new job'
name='noteJobIndicator'
groupClassName='mb-0'
/>
<Checkbox
label='I find or lose a cowboy hat'
name='noteCowboyHat'
/>
<div className='form-label'>privacy</div>
<Checkbox

View File

@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Streak" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"startedAt" DATE NOT NULL,
"endedAt" DATE,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Streak.userId_index" ON "Streak"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Streak.startedAt_userId_unique" ON "Streak"("startedAt", "userId");
-- AddForeignKey
ALTER TABLE "Streak" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "noteCowboyHat" BOOLEAN NOT NULL DEFAULT true;

View File

@ -70,6 +70,7 @@ model User {
noteDeposits Boolean @default(true)
noteInvites Boolean @default(true)
noteJobIndicator Boolean @default(true)
noteCowboyHat Boolean @default(true)
// privacy settings
hideInvoiceDesc Boolean @default(false)
@ -84,12 +85,26 @@ model User {
PollVote PollVote[]
Donation Donation[]
ReferralAct ReferralAct[]
Streak Streak[]
@@index([createdAt])
@@index([inviteId])
@@map(name: "users")
}
model Streak {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
startedAt DateTime @db.Date
endedAt DateTime? @db.Date
userId Int
user User @relation(fields: [userId], references: [id])
@@unique([startedAt, userId])
@@index([userId])
}
model NostrRelay {
addr String @id
createdAt DateTime @default(now()) @map(name: "created_at")

3
svgs/bald.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288.69 325.52">
<path class="cls-1" d="m275.55,123.09c-2.36-8.21-11.36-2.84-11.36-2.84,0,0-.16-.79-1.42-15.31-1.26-14.52-5.21-41.81-34.71-68.16-29.51-26.36-77.51-25.09-77.51-25.09h-12.28c-12.27-.02-38.34,4.73-59.96,16.56-21.61,11.84-33.29,34.56-39.6,51.13-6.31,16.57-8.52,47.18-8.52,47.18,0,0-6.31-3.32-8.84-2.05-2.52,1.26-6.94,3.15-2.52,21.46,4.42,18.3,15.15,30.92,18.14,36.76,3,5.84,8.52,4.42,9.63,3.47,1.1-.94,1.58-3.78,1.58-3.78,0,0,1.1,29.35,2.21,36.29,1.1,6.94,5.2,19.56,8.67,24.14,3.48,4.58,6.79,8.21,6.79,8.21l6.31-.32s-.32,3.47,0,6.15c.32,2.69,2.05,5.84,4.1,7.74,2.05,1.89,23.36,26.03,30.61,30.92,7.26,4.89,13.73,10.57,44,10.57s36.95-8.2,40.74-11.2,20.04-18.46,29.19-29.03c9.15-10.57,21.93-26.98,24.93-35.66s5.37-45.45,5.37-45.45c0,0,.63,1.27,2.68,1.11,2.05-.16,5.84-3.47,7.73-7.58,1.89-4.1,2.68-4.1,8.68-20.19,5.99-16.1,7.73-26.83,5.36-35.03Zm-200.2-.65c-1.08.06-1.56-.72-1.8-1.32-.24-.59.08-2.79.77-3.71s4.37-6.44,15.99-12.77c11.62-6.33,20.02-4.83,20.02-4.83,0,0,2.8.45,5.31,1.11,2.51.65,4.31,2.99,4.73,3.76.42.78.59,1.62,0,2.75-.6,1.14-1.92,1.26-8.37,1.44-6.46.18-11.9,1.49-17.7,3.65-5.8,2.15-10.58,5.91-13.69,7.95-3.11,2.03-4.19,1.91-5.26,1.97Zm27.73,47.96c-8.57,0-15.52-7.91-15.52-17.66s6.95-17.66,15.52-17.66,15.52,7.91,15.52,17.66-6.95,17.66-15.52,17.66Zm88.88,74.66c-3.22,2.29-7.61,2.6-18.21,0-10.59-2.6-33.26-1.24-36.17-1.05-2.91.18-9.47,1.18-12.07,1.79-2.6.62-4.96,1.51-8.92,1.1-3.97-.41-6.57-3.82-6.94-7.17-.37-3.34.62-9.84,5.58-17.96,4.95-8.11,13.76-18.58,37.32-18.58,0,0,11.05-1.11,22.5,4.71,11.46,5.82,18.71,13.81,20.38,20.69,1.67,6.87-.25,14.18-3.47,16.47Zm7.17-74.66c-8.57,0-15.52-7.91-15.52-17.66s6.95-17.66,15.52-17.66,15.52,7.91,15.52,17.66-6.95,17.66-15.52,17.66Zm28.96-49.28c-.24.6-.72,1.38-1.8,1.32-1.07-.06-2.15.06-5.26-1.97-3.11-2.04-7.89-5.8-13.69-7.95-5.8-2.16-11.24-3.47-17.7-3.65-6.45-.18-7.77-.3-8.37-1.44-.59-1.13-.41-1.97,0-2.75.42-.77,2.22-3.11,4.73-3.76,2.51-.66,5.31-1.11,5.31-1.11,0,0,8.4-1.5,20.02,4.83,11.62,6.33,15.3,11.85,15.99,12.77s1.01,3.12.77,3.71Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

21
svgs/cowboy.svg Normal file
View File

@ -0,0 +1,21 @@
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="80 200 450 400">
<path class="st0" d="M499.9,315.6c-17.2-10.5-28.7,0.6-36.6,7.9c-7.9,7.2-26.6,30.8-36.6,38.1c-10,7.2-27.5,13.6-53.9,16.7
c-26.3,3.1-50.5,1.4-47.8,1c2.7-0.4,10.6-2.2,12.4-3c1.8-0.9,11.3-4.3,33.7-11.9c22.4-7.6,36.5-17,39.4-18.7
c2.9-1.7,5.9-3.9,5.4-5.2s-2.2-8.1-5.1-15.8c-2.9-7.7-8.5-29.4-10.1-34.2c-1.6-4.8-3-13.1-5.9-19.5s-4.4-21.3-11.3-38.5
c-6.9-17.2-11.8-19.5-31.7-27.7c-19.8-8.2-57.8-6.9-57.8-6.9s-41.1,1.3-59.5,9.2c-18.4,7.9-22.8,19-23.4,20.6
c-0.6,1.6-12.9,44.2-12.9,44.2l-27.4,93.1c0,0-6-5.6-10.7-10.8c-4.5-5-29.9-36.9-44-41.1c-14.1-4.2-20.4,0.8-26.2,5.5
c-5.8,4.7-11.1,18.3-8.5,32.7c2.6,14.4,29.8,45.3,51,59.7c21.2,14.4,40.8,21.5,43.6,23.1c2.8,1.6,6.5,2.8,16,15.2
s32.5,32.3,45.8,41.6c13.4,9.4,34.8,22.1,44.6,26.8c9.8,4.7,14.9,3.6,14.9,3.6c8.8,0,36.1-15.4,50.6-25.1
c14.4-9.7,32.3-25.1,34.1-26.6c1.6-1.3,3.2-3.4,3.7-3.9c0.7-0.8,6-8.1,15.3-17s14.6-12.4,14.6-12.4l0.2-1.8c0,0,27-9.2,53.4-30.3
c26.5-21.1,39.1-43.5,41.4-48.5C512.8,350.8,517.1,326,499.9,315.6L499.9,315.6z M366.5,413.2c-17,7.4-32,14.7-53.7,17.6
c-21.7,2.9-39.3,0.3-57.4-5.8c-18.1-6.1-38.3-16.6-49.2-23.6c-10.9-7-22.7-16.6-22.7-16.6s74.7,8.7,110.6,8.4
c36-0.3,71.5-2.9,83.3-4.2c11.8-1.3,32.8-4.2,32.8-4.2S383.5,405.7,366.5,413.2L366.5,413.2z"/>
<path class="st0" d="M415.7,449.1c0,0.1-0.2,1.2-6,4.5c-6.3,3.6-21.6,22.6-25.8,27.2c-4.2,4.7-27.5,22.3-27.5,22.3
S310.7,535,295.9,535s-43.1-21.3-58.4-31.3c-15.3-10-26.9-21.4-31.2-25.9c-4.2-4.5-13.8-15-20.5-21.7c-6.7-6.7-8.7-7-8.7-7
s-0.4,2.3,0,5.1c0.4,2.8,3.5,16.6,5.1,20.1s4.4,8.3,4.4,8.3s13.1,49.1,15.1,53.5s11.9,16.9,11.9,16.9l6,0.2l0.7,9
c0,0,0.7,0.9,2.5,2.5c1.8,1.6,18.6,20.4,22.3,23.6c3.6,3.2,15.6,15.6,28.3,17.8c12.7,2.2,23.3,2,40.8,0.9
c17.5-1.2,36.7-18.8,54.6-39.9c17.9-21.1,24.3-31.8,26.4-40.5s3.1-40.5,3.2-41.4c0.1-0.9,1.9,1.3,4.7,1.5c2.8,0.1,6.9-7.9,10.4-19.4
C416.6,456,415.8,449.5,415.7,449.1L415.7,449.1z M318.4,556.4c-6.8,7-16.8,12-20.7,12s-11.4-2.5-19.9-10.2
c-8.5-7.7-11.9-13-11.9-13s15.9,1.8,31,1.8s31-1.8,31-1.8S325.2,549.4,318.4,556.4L318.4,556.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -10,6 +10,7 @@ const { earn } = require('./earn')
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
const { indexItem, indexAllItems } = require('./search')
const { timestampItem } = require('./ots')
const { computeStreaks } = require('./streak')
const fetch = require('cross-fetch')
async function work () {
@ -47,6 +48,7 @@ async function work () {
await boss.work('indexAllItems', indexAllItems(args))
await boss.work('auction', auction(args))
await boss.work('earn', earn(args))
await boss.work('streak', computeStreaks(args))
console.log('working jobs')
}

51
worker/streak.js Normal file
View File

@ -0,0 +1,51 @@
function computeStreaks ({ models }) {
return async function () {
console.log('computing streaks')
// get all eligible users in the last day
// if the user doesn't have an active streak, add one
// if they have an active streak but didn't maintain it, end it
await models.$executeRaw(
`WITH day_streaks (id) AS (
SELECT "userId"
FROM
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
FROM "ItemAct"
WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date
GROUP BY "userId")
UNION ALL
(SELECT "userId", sats as sats_spent
FROM "Donation"
WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date
)) spending
GROUP BY "userId"
HAVING sum(sats_spent) >= 100
), existing_streaks (id) AS (
SELECT "userId"
FROM "Streak"
WHERE "Streak"."endedAt" IS NULL
), new_streaks (id) AS (
SELECT day_streaks.id
FROM day_streaks
LEFT JOIN existing_streaks ON existing_streaks.id = day_streaks.id
WHERE existing_streaks.id IS NULL
), ending_streaks (id) AS (
SELECT existing_streaks.id
FROM existing_streaks
LEFT JOIN day_streaks ON existing_streaks.id = day_streaks.id
WHERE day_streaks.id IS NULL
), streak_insert AS (
INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at)
SELECT id, (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, now_utc(), now_utc()
FROM new_streaks
)
UPDATE "Streak"
SET "endedAt" = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, updated_at = now_utc()
FROM ending_streaks
WHERE ending_streaks.id = "Streak"."userId"`)
console.log('done computing streaks')
}
}
module.exports = { computeStreaks }