Skip to content

Commit 02501db

Browse files
committed
MultiInput component; magic code via MultiInput
1 parent 2111eff commit 02501db

File tree

9 files changed

+210
-33
lines changed

9 files changed

+210
-33
lines changed

awards.csv

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,16 @@ Darth-Coin,issue,#1649,#1421,medium,,,,25k,[email protected],2024-12-07
160160
Soxasora,pr,#1685,,medium,,,,250k,[email protected],2024-12-07
161161
aegroto,pr,#1606,#1242,medium,,,,250k,[email protected],2024-12-07
162162
sfr0xyz,issue,#1696,#1196,good-first-issue,,,,2k,[email protected],2024-12-10
163-
Soxasora,pr,#1794,#756,hard,urgent,,includes #411,3m,bolt11,2024-01-09
164-
Soxasora,pr,#1786,#363,easy,,,,100k,bolt11,2024-01-09
165-
Soxasora,pr,#1768,#1186,medium-hard,,,,500k,bolt11,2024-01-09
166-
Soxasora,pr,#1750,#1035,medium,,,,250k,bolt11,2024-01-09
167-
SatsAllDay,issue,#1794,#411,hard,high,,,200k,[email protected],2024-01-20
168-
felipebueno,issue,#1786,#363,easy,,,,10k,[email protected],???
169-
cyphercosmo,pr,#1745,#1648,good-first-issue,,,2,16k,[email protected],2024-01-20
170-
Radentor,issue,#1768,#1186,medium-hard,,,,50k,???,???
163+
Soxasora,pr,#1794,#756,hard,urgent,,includes #411,3m,bolt11,2025-01-09
164+
Soxasora,pr,#1786,#363,easy,,,,100k,bolt11,2025-01-09
165+
Soxasora,pr,#1768,#1186,medium-hard,,,,500k,bolt11,2025-01-09
166+
Soxasora,pr,#1750,#1035,medium,,,,250k,bolt11,2025-01-09
167+
SatsAllDay,issue,#1794,#411,hard,high,,,200k,[email protected],2025-01-20
168+
felipebueno,issue,#1786,#363,easy,,,,10k,[email protected],2025-01-27
169+
cyphercosmo,pr,#1745,#1648,good-first-issue,,,2,16k,[email protected],2025-01-27
170+
Radentor,issue,#1768,#1186,medium-hard,,,,50k,[email protected],2025-01-27
171+
Soxasora,pr,#1841,#1692,good-first-issue,,,,20k,[email protected],2025-01-27
172+
Soxasora,pr,#1839,#1790,easy,,,1,90k,[email protected],2025-01-27
173+
Soxasora,pr,#1820,#1819,easy,,,1,90k,[email protected],2025-01-27
174+
SatsAllDay,issue,#1820,#1819,easy,,,1,9k,[email protected],2025-01-27
175+
Soxasora,pr,#1814,#1736,easy,,,,100k,[email protected],2025-01-27

components/form.js

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ function FormGroup ({ className, label, children }) {
486486

487487
function InputInner ({
488488
prepend, append, hint, warn, showValid, onChange, onBlur, overrideValue, appendValue,
489-
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
489+
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength, hideError,
490490
...props
491491
}) {
492492
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
@@ -574,7 +574,7 @@ function InputInner ({
574574
onKeyDown={onKeyDownInner}
575575
onChange={onChangeInner}
576576
onBlur={onBlurInner}
577-
isInvalid={invalid}
577+
isInvalid={!hideError && invalid} // if hideError is true, handle error showing separately
578578
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
579579
/>
580580
{(isClient && clear && field.value && !props.readOnly) &&
@@ -1241,5 +1241,102 @@ export function PasswordInput ({ newPass, qr, copy, readOnly, append, value: ini
12411241
)
12421242
}
12431243

1244+
export function MultiInput ({
1245+
name, label, groupClassName, length = 4, charLength = 1, upperCase, showSequence,
1246+
onChange, autoFocus, hideError, inputType = 'text',
1247+
...props
1248+
}) {
1249+
const [inputs, setInputs] = useState(new Array(length).fill(''))
1250+
const inputRefs = useRef(new Array(length).fill(null))
1251+
const [, meta, helpers] = useField({ name })
1252+
1253+
useEffect(() => {
1254+
autoFocus && inputRefs.current[0].focus() // focus the first input if autoFocus is true
1255+
}, [autoFocus])
1256+
1257+
const updateInputs = useCallback((newInputs) => {
1258+
setInputs(newInputs)
1259+
const combinedValue = newInputs.join('') // join the inputs to get the value
1260+
helpers.setValue(combinedValue) // set the value to the formik field
1261+
onChange?.(combinedValue)
1262+
}, [onChange, helpers])
1263+
1264+
const handleChange = useCallback((formik, e, index) => { // formik is not used but it's required to get the value
1265+
const newValue = upperCase // convert the input to uppercase if upperCase is true
1266+
? e.target.value.slice(-charLength).toUpperCase()
1267+
: e.target.value.slice(-charLength)
1268+
1269+
const newInputs = [...inputs]
1270+
newInputs[index] = newValue
1271+
updateInputs(newInputs)
1272+
1273+
// focus the next input if the current input is filled
1274+
if (newValue.length === charLength && index < length - 1) {
1275+
inputRefs.current[index + 1].focus()
1276+
}
1277+
}, [inputs, charLength, upperCase, onChange, length])
1278+
1279+
const handleKeyDown = useCallback((e, index) => {
1280+
switch (e.key) {
1281+
case 'Backspace': {
1282+
e.preventDefault()
1283+
const newInputs = [...inputs]
1284+
// if current input is empty move focus to the previous input else clear the current input
1285+
const targetIndex = inputs[index] === '' && index > 0 ? index - 1 : index
1286+
newInputs[targetIndex] = ''
1287+
updateInputs(newInputs)
1288+
inputRefs.current[targetIndex]?.focus()
1289+
break
1290+
}
1291+
case 'ArrowLeft': {
1292+
if (index > 0) { // focus the previous input if it's not the first input
1293+
e.preventDefault()
1294+
inputRefs.current[index - 1]?.focus()
1295+
}
1296+
break
1297+
}
1298+
case 'ArrowRight': {
1299+
if (index < length - 1) { // focus the next input if it's not the last input
1300+
e.preventDefault()
1301+
inputRefs.current[index + 1]?.focus()
1302+
}
1303+
break
1304+
}
1305+
}
1306+
}, [inputs, length, updateInputs])
1307+
1308+
return (
1309+
<FormGroup label={label} className={groupClassName}>
1310+
<div className='d-flex flex-row gap-2'>
1311+
{inputs.map((value, index) => (
1312+
<InputInner
1313+
name={name}
1314+
key={index}
1315+
type={inputType}
1316+
value={value}
1317+
innerRef={(el) => { inputRefs.current[index] = el }}
1318+
onChange={(formik, e) => handleChange(formik, e, index)}
1319+
onKeyDown={e => handleKeyDown(e, index)}
1320+
style={{
1321+
textAlign: 'center',
1322+
maxWidth: `${charLength * 40}px` // adjusts the max width of the input based on the charLength
1323+
}}
1324+
prepend={showSequence && <InputGroup.Text>{index + 1}</InputGroup.Text>} // show the index of the input
1325+
hideError
1326+
{...props}
1327+
/>
1328+
))}
1329+
</div>
1330+
<div>
1331+
{hideError && meta.touched && meta.error && ( // custom error message is showed if hideError is true
1332+
<BootstrapForm.Control.Feedback type='invalid' className='d-block'>
1333+
{meta.error}
1334+
</BootstrapForm.Control.Feedback>
1335+
)}
1336+
</div>
1337+
</FormGroup>
1338+
)
1339+
}
1340+
12441341
export const ClientInput = Client(Input)
12451342
export const ClientCheckbox = Client(Checkbox)

components/media-or-link.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,12 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
133133
// hack
134134
// if it's not a video it will throw an error, so we can assume it's an image
135135
const img = new window.Image()
136-
img.onload = () => setIsImage(true)
137136
img.src = src
137+
img.decode().then(() => { // decoding beforehand to prevent wrong image cropping
138+
setIsImage(true)
139+
}).catch((e) => {
140+
console.error('Cannot decode image', e)
141+
})
138142
}
139143
video.src = src
140144

lib/validate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export const emailSchema = object({
383383
})
384384

385385
export const emailTokenSchema = object({
386-
token: string().required('required').trim().matches(/^[0-9a-z]{6}$/i, 'must be 6 alphanumeric characters')
386+
token: string().required('required').trim().matches(/^[0-9a-z]{6}$/i, 'must be 6 alphanumeric character')
387387
})
388388

389389
export const urlSchema = object({

pages/auth/error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function AuthError ({ error }) {
2828
<StaticLayout>
2929
<Image className='rounded-1 shadow-sm' width='500' height='375' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/double.gif`} fluid />
3030
<h2 className='pt-4'>Where did the magic go?</h2>
31-
<h4 className='text-muted pt-2'>Get another magic code by logging in or try again by going back.</h4>
31+
<h4 className='text-muted text-center pt-2'>Get another magic code by logging in or try again by going back.</h4>
3232
<Button
3333
className='align-items-center my-3'
3434
style={{ borderWidth: '2px' }}

pages/email.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { StaticLayout } from '@/components/layout'
33
import { getGetServerSideProps } from '@/api/ssrApollo'
44
import { useRouter } from 'next/router'
55
import { useState, useEffect, useCallback } from 'react'
6-
import { Form, SubmitButton, PasswordInput } from '@/components/form'
6+
import { Form, SubmitButton, MultiInput } from '@/components/form'
77
import { emailTokenSchema } from '@/lib/validate'
88

99
// force SSR to include CSP nonces
@@ -31,24 +31,37 @@ export default function Email () {
3131
<Image className='rounded-1 shadow-sm' width='640' height='302' src={`${process.env.NEXT_PUBLIC_ASSET_PREFIX}/cowboy-saloon.gif`} fluid />
3232
</video>
3333
<h2 className='pt-4'>Check your email</h2>
34-
<h4 className='text-muted pt-2 pb-4'>a 5-minutes magic code has been sent to {callback ? callback.email : 'your email address'}</h4>
35-
<MagicCodeForm onSubmit={(token) => pushCallback(token)} />
34+
<h4 className='text-muted pt-2 pb-4'>a magic code has been sent to {callback ? callback.email : 'your email address'}</h4>
35+
<MagicCodeForm onSubmit={(token) => pushCallback(token)} disabled={!callback} />
3636
</div>
3737
</StaticLayout>
3838
)
3939
}
4040

41-
export const MagicCodeForm = ({ onSubmit }) => {
41+
export const MagicCodeForm = ({ onSubmit, disabled }) => {
4242
return (
4343
<Form
4444
initial={{
4545
token: ''
4646
}}
4747
schema={emailTokenSchema}
48-
onSubmit={({ token }) => { onSubmit(token.toLowerCase()) }}
48+
onSubmit={(values) => {
49+
onSubmit(values.token.toLowerCase()) // token is displayed in uppercase but we need to check it in lowercase
50+
}}
4951
>
50-
<PasswordInput name='token' required placeholder='input your 6-digit magic code' />
51-
<SubmitButton variant='primary' className='px-4'>verify</SubmitButton>
52+
<MultiInput
53+
length={8}
54+
charLength={1}
55+
name='token'
56+
required
57+
upperCase // display token in uppercase
58+
autoFocus
59+
groupClassName='d-flex flex-wrap justify-content-center'
60+
inputType='text'
61+
hideError // hide error message on every input, allow custom error message
62+
disabled={disabled} // disable the form if no callback is provided
63+
/>
64+
<SubmitButton variant='primary' className='px-4' disabled={disabled}>verify</SubmitButton>
5265
</Form>
5366
)
5467
}

scripts/welcome.js

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
#!/usr/bin/env node
22

3-
const SN_API_URL = process.env.SN_API_URL ?? 'http://localhost:3000'
3+
function usage () {
4+
console.log('Usage: scripts/welcome.js <fetch-after> [--prod]')
5+
process.exit(1)
6+
}
7+
8+
let args = process.argv.slice(2)
9+
10+
const useProd = args.indexOf('--prod') !== -1
11+
const SN_API_URL = useProd ? 'https://stacker.news' : 'http://localhost:3000'
12+
args = args.filter(arg => arg !== '--prod')
13+
console.log('> url:', SN_API_URL)
14+
415
// this is the item id of the last bio that was included in the previous post of the series
5-
// TODO: make this configurable
6-
const FETCH_AFTER = 838433
16+
const FETCH_AFTER = args[0]
17+
console.log('> fetch-after:', FETCH_AFTER)
18+
if (!FETCH_AFTER) {
19+
usage()
20+
}
721

822
async function gql (query, variables = {}) {
923
const response = await fetch(`${SN_API_URL}/api/graphql`, {
@@ -56,25 +70,39 @@ function filterBios (bios) {
5670
return newBios
5771
}
5872

73+
async function populate (bios) {
74+
return await Promise.all(
75+
bios.map(
76+
async bio => {
77+
bio.user.since = await fetchItem(bio.user.since)
78+
bio.user.items = await fetchUserItems(bio.user.name)
79+
bio.user.credits = sumBy(bio.user.items, 'credits')
80+
bio.user.sats = sumBy(bio.user.items, 'sats') - bio.user.credits
81+
bio.user.satstandard = bio.user.sats / (bio.user.sats + bio.user.credits)
82+
return bio
83+
}
84+
)
85+
)
86+
}
87+
5988
async function printTable (bios) {
60-
console.log('| nym | bio (stacking since) | items | sats stacked |')
61-
console.log('| --- | -------------------- | ----- | ------------ |')
89+
console.log('| nym | bio (stacking since) | items | sats/ccs stacked | sat standard |')
90+
console.log('| --- | -------------------- | ----- | ---------------- | ------------ |')
6291

6392
for (const bio of bios) {
6493
const { user } = bio
6594

6695
const bioCreatedAt = formatDate(bio.createdAt)
67-
let col2 = `[${formatDate(bio.createdAt)}](${itemLink(bio.id)})`
68-
if (Number(bio.id) !== user.since) {
69-
const since = await fetchItem(user.since)
70-
const sinceCreatedAt = formatDate(since.createdAt)
96+
let col2 = dateLink(bio)
97+
if (Number(bio.id) !== user.since.id) {
98+
const sinceCreatedAt = formatDate(user.since.createdAt)
7199
// stacking since might not be the same item as the bio
72100
// but it can still have been created on the same day
73101
if (bioCreatedAt !== sinceCreatedAt) {
74-
col2 += ` ([${formatDate(since.createdAt)}](${itemLink(since.id)}))`
102+
col2 += ` (${dateLink(user.since)})`
75103
}
76104
}
77-
console.log(`| @${user.name} | ${col2} | ${user.nitems} | ${user.optional.stacked || '???'} |`)
105+
console.log(`| @${user.name} | ${col2} | ${user.nitems} | ${user.sats}/${user.credits} | ${user.satstandard.toFixed(2)} |`)
78106
}
79107

80108
console.log(`${bios.length} rows`)
@@ -86,10 +114,18 @@ function formatDate (date) {
86114
return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
87115
}
88116

117+
function sumBy (arr, key) {
118+
return arr.reduce((acc, item) => acc + item[key], 0)
119+
}
120+
89121
function itemLink (id) {
90122
return `https://stacker.news/items/${id}`
91123
}
92124

125+
function dateLink (item) {
126+
return `[${formatDate(item.createdAt)}](${itemLink(item.id)})`
127+
}
128+
93129
async function fetchItem (id) {
94130
const data = await gql(`
95131
query Item($id: ID!) {
@@ -102,7 +138,24 @@ async function fetchItem (id) {
102138
return data.item
103139
}
104140

141+
async function fetchUserItems (name) {
142+
const data = await gql(`
143+
query UserItems($name: String!) {
144+
items(sort: "user", name: $name) {
145+
items {
146+
id
147+
createdAt
148+
sats
149+
credits
150+
}
151+
}
152+
}`, { name }
153+
)
154+
return data.items.items
155+
}
156+
105157
fetchRecentBios()
106158
.then(data => filterBios(data.items.items))
159+
.then(populate)
107160
.then(printTable)
108161
.catch(console.error)

wallets/cln/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ export const fields = [
2121
name: 'rune',
2222
label: 'invoice only rune',
2323
help: {
24-
text: 'We only accept runes that *only* allow `method=invoice`.\nRun this to generate one:\n\n```lightning-cli createrune restrictions=\'["method=invoice"]\'```'
24+
text: 'We only accept runes that *only* allow `method=invoice`.\n\n' +
25+
'Run this if you are on v23.08 to generate one:\n\n' +
26+
'```lightning-cli createrune restrictions=\'["method=invoice"]\'```\n\n' +
27+
'Or this if you are on v24.11 or later:\n\n' +
28+
'```lightning-cli createrune restrictions=\'[["method=invoice"]]\'```\n\n' +
29+
'[see `createrune` documentation](https://docs.corelightning.org/reference/lightning-createrune#restriction-format)'
2530
},
2631
type: 'text',
2732
placeholder: 'S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ==',

worker/earn.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function earnStmts (data, { models }) {
189189
})]
190190
}
191191

192-
const DAILY_STIMULUS_SATS = 75_000
192+
const DAILY_STIMULUS_SATS = 50_000
193193
export async function earnRefill ({ models, lnd }) {
194194
return await performPaidAction('DONATE',
195195
{ sats: DAILY_STIMULUS_SATS },

0 commit comments

Comments
 (0)