Skip to content

Commit

Permalink
Merge branch 'master' into anon_webln_qr_fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn authored Mar 1, 2025
2 parents ba93d34 + 5e7fd69 commit d9e4e4f
Show file tree
Hide file tree
Showing 25 changed files with 1,170 additions and 219 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/extend-awards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: extend-awards
run-name: Extending awards
on:
pull_request:
types: [ closed ]
branches:
- master
jobs:
unfiltered:
runs-on: ubuntu-latest
steps:
- run: echo '${{ toJson(github) }}'
if_merged:
if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- run: pip install requests
- run: python extend-awards.py '${{ toJson(github) }}'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: peter-evans/create-pull-request@v7
with:
commit-message: extending awards
title: Extending awards
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,7 @@ scripts/nwc-keys.json
docker/lnbits/data

# lndk
!docker/lndk/tls-*.pem
!docker/lndk/tls-*.pem

# nostr link extract
scripts/nostr-link-extract.config.json
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Go to [localhost:3000](http://localhost:3000).
- ssh: `git clone [email protected]:stackernews/stacker.news.git`
- https: `git clone https://github.com/stackernews/stacker.news.git`
- Install [docker](https://docs.docker.com/compose/install/)
- If you're running MacOS or Windows, I ***highly recommend*** using [OrbStack](https://orbstack.dev/) instead of Docker Desktop
- Please make sure that at least 10 GB of free space is available, otherwise you may encounter issues while setting up the development environment.

<br>
Expand Down
33 changes: 33 additions & 0 deletions api/resolvers/growth.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,39 @@ export default {
FROM ${viewGroup(range, 'stacking_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
itemGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
const range = whenRange(when, from, to)

const subExists = await models.sub.findUnique({ where: { name: sub } })
if (!subExists) throw new Error('Sub not found')

return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'posts', 'value', coalesce(sum(posts),0)),
json_build_object('name', 'comments', 'value', coalesce(sum(comments),0))
) AS data
FROM ${viewGroup(range, 'sub_stats')}
WHERE sub_name = $3
GROUP BY time
ORDER BY time ASC`, ...range, sub)
},
revenueGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
const range = whenRange(when, from, to)

const subExists = await models.sub.findUnique({ where: { name: sub } })
if (!subExists) throw new Error('Sub not found')

return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'revenue', 'value', coalesce(sum(msats_revenue/1000),0)),
json_build_object('name', 'stacking', 'value', coalesce(sum(msats_stacked/1000),0)),
json_build_object('name', 'spending', 'value', coalesce(sum(msats_spent/1000),0))
) AS data
FROM ${viewGroup(range, 'sub_stats')}
WHERE sub_name = $3
GROUP BY time
ORDER BY time ASC`, ...range, sub)
}
}
}
2 changes: 2 additions & 0 deletions api/typeDefs/growth.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default gql`
spenderGrowth(when: String, from: String, to: String): [TimeData!]!
stackingGrowth(when: String, from: String, to: String): [TimeData!]!
stackerGrowth(when: String, from: String, to: String): [TimeData!]!
itemGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
revenueGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
}
type TimeData {
Expand Down
3 changes: 3 additions & 0 deletions awards.csv
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,6 @@ SatsAllDay,issue,#1820,#1819,easy,,,1,9k,[email protected],2025-01-27
Soxasora,pr,#1814,#1736,easy,,,,100k,[email protected],2025-01-27
jason-me,pr,#1857,,easy,,,,100k,[email protected],2025-02-08
ed-kung,pr,#1901,#323,good-first-issue,,,,20k,[email protected],2025-02-14
Scroogey-SN,pr,#1911,#1905,good-first-issue,,,1,18k,???,???
Scroogey-SN,pr,#1928,#1924,good-first-issue,,,,20k,???,???
dtonon,issue,#1928,#1924,good-first-issue,,,,2k,???,???
2 changes: 1 addition & 1 deletion components/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default function Footer ({ links = true }) {
<Rewards />
</div>
<div className='mb-0' style={{ fontWeight: 500 }}>
<Link href='/stackers/day' className='nav-link p-0 p-0 d-inline-flex'>
<Link href='/stackers/all/day' className='nav-link p-0 p-0 d-inline-flex'>
analytics
</Link>
<span className='mx-2 text-muted'> \ </span>
Expand Down
125 changes: 78 additions & 47 deletions components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import TextareaAutosize from 'react-textarea-autosize'
import { useToast } from './toast'
import { numWithUnits } from '@/lib/format'
import textAreaCaret from 'textarea-caret'
import ReactDatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
import useDebounceCallback, { debounce } from './use-debounce-callback'
import { FileUpload } from './file-upload'
Expand All @@ -38,9 +37,10 @@ import QrIcon from '@/svgs/qr-code-line.svg'
import QrScanIcon from '@/svgs/qr-scan-line.svg'
import { useShowModal } from './modal'
import { QRCodeSVG } from 'qrcode.react'
import { Scanner } from '@yudiel/react-qr-scanner'
import dynamic from 'next/dynamic'
import { qrImageSettings } from './qr'
import { useIsClient } from './use-client'
import PageLoading from './page-loading'

export class SessionRequiredError extends Error {
constructor () {
Expand Down Expand Up @@ -971,6 +971,19 @@ export function Select ({ label, items, info, groupClassName, onChange, noForm,
)
}

function DatePickerSkeleton () {
return (
<div className='react-datepicker-wrapper'>
<input className='form-control clouds fade-out p-0 px-2 mb-0' />
</div>
)
}

const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
ssr: false,
loading: () => <DatePickerSkeleton />
})

export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to, className, ...props }) {
const formik = noForm ? null : useFormikContext()
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
Expand Down Expand Up @@ -1038,19 +1051,23 @@ export function DatePicker ({ fromName, toName, noForm, onChange, when, from, to
}

return (
<ReactDatePicker
className={`form-control text-center ${className}`}
selectsRange
maxDate={new Date()}
minDate={new Date('2021-05-01')}
{...props}
selected={new Date(innerFrom)}
startDate={new Date(innerFrom)}
endDate={innerTo ? new Date(innerTo) : undefined}
dateFormat={dateFormat}
onChangeRaw={onChangeRawHandler}
onChange={innerOnChange}
/>
<>
{ReactDatePicker && (
<ReactDatePicker
className={`form-control text-center ${className}`}
selectsRange
maxDate={new Date()}
minDate={new Date('2021-05-01')}
{...props}
selected={new Date(innerFrom)}
startDate={new Date(innerFrom)}
endDate={innerTo ? new Date(innerTo) : undefined}
dateFormat={dateFormat}
onChangeRaw={onChangeRawHandler}
onChange={innerOnChange}
/>
)}
</>
)
}

Expand All @@ -1070,19 +1087,27 @@ export function DateTimeInput ({ label, groupClassName, name, ...props }) {

function DateTimePicker ({ name, className, ...props }) {
const [field, , helpers] = useField({ ...props, name })
const ReactDatePicker = dynamic(() => import('react-datepicker').then(mod => mod.default), {
ssr: false,
loading: () => <span>loading date picker</span>
})
return (
<ReactDatePicker
{...field}
{...props}
showTimeSelect
dateFormat='Pp'
className={`form-control ${className}`}
selected={(field.value && new Date(field.value)) || null}
value={(field.value && new Date(field.value)) || null}
onChange={(val) => {
helpers.setValue(val)
}}
/>
<>
{ReactDatePicker && (
<ReactDatePicker
{...field}
{...props}
showTimeSelect
dateFormat='Pp'
className={`form-control ${className}`}
selected={(field.value && new Date(field.value)) || null}
value={(field.value && new Date(field.value)) || null}
onChange={(val) => {
helpers.setValue(val)
}}
/>
)}
</>
)
}

Expand Down Expand Up @@ -1149,6 +1174,10 @@ function QrPassword ({ value }) {
function PasswordScanner ({ onScan, text }) {
const showModal = useShowModal()
const toaster = useToast()
const Scanner = dynamic(() => import('@yudiel/react-qr-scanner').then(mod => mod.Scanner), {
ssr: false,
loading: () => <PageLoading />
})

return (
<InputGroup.Text
Expand All @@ -1158,26 +1187,28 @@ function PasswordScanner ({ onScan, text }) {
return (
<div>
{text && <h5 className='line-height-md mb-4 text-center'>{text}</h5>}
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
onScan(result)
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>
{Scanner && (
<Scanner
formats={['qr_code']}
onScan={([{ rawValue: result }]) => {
onScan(result)
onClose()
}}
styles={{
video: {
aspectRatio: '1 / 1'
}
}}
onError={(error) => {
if (error instanceof DOMException) {
console.log(error)
} else {
toaster.danger('qr scan: ' + error?.message || error?.toString?.())
}
onClose()
}}
/>
)}
</div>
)
})
Expand Down
79 changes: 79 additions & 0 deletions components/sub-analytics-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useRouter } from 'next/router'
import { Select, DatePicker } from './form'
import { useSubs } from './sub-select'
import { WHENS } from '@/lib/constants'
import { whenToFrom } from '@/lib/time'
import styles from './sub-select.module.css'
import classNames from 'classnames'

export function SubAnalyticsHeader ({ pathname = null }) {
const router = useRouter()

const path = pathname || 'stackers'

const select = async values => {
const { sub, when, ...query } = values

if (when !== 'custom') { delete query.from; delete query.to }
if (query.from && !query.to) return

await router.push({

pathname: `/${path}/${sub}/${when}`,
query
})
}

const when = router.query.when || 'day'
const sub = router.query.sub || 'all'

const subs = useSubs({ prependSubs: ['all'], sub, appendSubs: [], filterSubs: () => true })

return (
<div className='text-muted fw-bold my-0 d-flex align-items-center flex-wrap'>
<div className='text-muted fw-bold mb-2 d-flex align-items-center'>
stacker analytics in
<Select
groupClassName='mb-0 mx-2'
className={classNames(styles.subSelect, styles.subSelectSmall)}
name='sub'
size='sm'
items={subs}
value={sub}
noForm
onChange={(formik, e) => {
const range = when === 'custom' ? { from: router.query.from, to: router.query.to } : {}
select({ sub: e.target.value, when, ...range })
}}
/>
for
<Select
groupClassName='mb-0 mx-2'
className='w-auto'
name='when'
size='sm'
items={WHENS}
value={when}
noForm
onChange={(formik, e) => {
const range = e.target.value === 'custom' ? { from: whenToFrom(when), to: Date.now() } : {}
select({ sub, when: e.target.value, ...range })
}}
/>
</div>
{when === 'custom' &&
<DatePicker
noForm
fromName='from'
toName='to'
className='p-0 px-2 mb-0'
onChange={(formik, [from, to], e) => {
select({ sub, when, from: from.getTime(), to: to.getTime() })
}}
from={router.query.from}
to={router.query.to}
when={when}
/>}
</div>
)
}
Loading

0 comments on commit d9e4e4f

Please sign in to comment.