stacker.news/components/territory-form.js

305 lines
11 KiB
JavaScript
Raw Normal View History

import AccordianItem from './accordian-item'
import { Col, InputGroup, Row, Form as BootstrapForm, Badge } from 'react-bootstrap'
2023-11-21 23:32:22 +00:00
import { Checkbox, CheckboxGroup, Form, Input, MarkdownInput } from './form'
import FeeButton, { FeeButtonProvider } from './fee-button'
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
import { useCallback, useMemo, useState } from 'react'
2023-11-21 23:32:22 +00:00
import { useRouter } from 'next/router'
import { MAX_TERRITORY_DESC_LENGTH, POST_TYPES, TERRITORY_BILLING_OPTIONS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { territorySchema } from '@/lib/validate'
2023-11-21 23:32:22 +00:00
import { useMe } from './me'
import Info from './info'
import { abbrNum } from '@/lib/format'
import { purchasedType } from '@/lib/territory'
import { SUB } from '@/fragments/subs'
2023-11-21 23:32:22 +00:00
export default function TerritoryForm ({ sub }) {
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const [upsertSub] = useMutation(
gql`
2024-01-11 23:45:17 +00:00
mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!,
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
$billingAutoRenew: Boolean!, $moderated: Boolean!, $hash: String, $hmac: String, $nsfw: Boolean!) {
2024-01-11 23:45:17 +00:00
upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost,
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
billingAutoRenew: $billingAutoRenew, moderated: $moderated, hash: $hash, hmac: $hmac, nsfw: $nsfw) {
2023-11-21 23:32:22 +00:00
name
}
}`
)
const [unarchiveTerritory] = useMutation(
gql`
mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!,
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
$billingAutoRenew: Boolean!, $moderated: Boolean!, $hash: String, $hmac: String, $nsfw: Boolean!) {
unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost,
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
billingAutoRenew: $billingAutoRenew, moderated: $moderated, hash: $hash, hmac: $hmac, nsfw: $nsfw) {
name
}
}`
)
const schema = territorySchema({ client, me, sub })
const [fetchSub] = useLazyQuery(SUB)
const [archived, setArchived] = useState(false)
const onNameChange = useCallback(async (formik, e) => {
// never show "territory archived" warning during edits
if (sub) return
const name = e.target.value
const { data } = await fetchSub({ variables: { sub: name } })
setArchived(data?.sub?.status === 'STOPPED')
}, [fetchSub, setArchived])
2023-11-21 23:32:22 +00:00
const onSubmit = useCallback(
async ({ ...variables }) => {
const { error } = archived
? await unarchiveTerritory({ variables })
: await upsertSub({ variables: { oldName: sub?.name, ...variables } })
2023-11-21 23:32:22 +00:00
if (error) {
throw new Error({ message: error.toString() })
}
// modify graphql cache to include new sub
client.cache.modify({
fields: {
subs (existing = []) {
2024-01-11 23:45:17 +00:00
const filtered = existing.filter(s => s.name !== variables.name && s.name !== sub?.name)
2023-11-21 23:32:22 +00:00
return [
2024-01-11 23:45:17 +00:00
...filtered,
2023-11-21 23:32:22 +00:00
{ __typename: 'Sub', name: variables.name }]
}
}
})
await router.push(`/~${variables.name}`)
}, [client, upsertSub, unarchiveTerritory, router, archived]
2023-11-21 23:32:22 +00:00
)
const [billing, setBilling] = useState((sub?.billingType || 'MONTHLY').toLowerCase())
const lineItems = useMemo(() => {
const lines = { territory: TERRITORY_BILLING_OPTIONS('first')[billing] }
if (!sub) return lines
// we are changing billing type so prorate the change
if (sub?.billingType?.toLowerCase() !== billing) {
const alreadyBilled = TERRITORY_PERIOD_COST(purchasedType(sub))
lines.paid = {
term: `- ${abbrNum(alreadyBilled)} sats`,
label: 'already paid',
modifier: cost => cost - alreadyBilled
}
return lines
}
}, [sub, billing])
2023-11-21 23:32:22 +00:00
return (
<FeeButtonProvider baseLineItems={lineItems}>
2023-11-21 23:32:22 +00:00
<Form
initial={{
name: sub?.name || '',
desc: sub?.desc || '',
baseCost: sub?.baseCost || 10,
postTypes: sub?.postTypes || POST_TYPES,
allowFreebies: typeof sub?.allowFreebies === 'undefined' ? true : sub?.allowFreebies,
2023-12-08 20:02:00 +00:00
billingType: sub?.billingType || 'MONTHLY',
billingAutoRenew: sub?.billingAutoRenew || false,
moderated: sub?.moderated || false,
nsfw: sub?.nsfw || false
2023-11-21 23:32:22 +00:00
}}
schema={schema}
Frontend payment UX cleanup (#1194) * Replace useInvoiceable with usePayment hook * Show WebLnError in QR code fallback * Fix missing removal of old zap undo code * Fix payment timeout message * Fix unused arg in super() * Also bail if invoice expired * Fix revert on reply error * Use JIT_INVOICE_TIMEOUT_MS constant * Remove unnecessary PaymentContext * Fix me as a dependency in FeeButtonContext * Fix anon sats added before act success * Optimistic updates for zaps * Fix modal not closed after custom zap * Optimistic update for custom zaps * Optimistic update for bounty payments * Consistent error handling for zaps and bounty payments * Optimistic update for poll votes * Use var balance in payment.request * Rename invoiceable to prepaid * Log cancelled invoices * Client notifications We now show notifications that are stored on the client to inform the user about following errors in the prepaid payment flow: - if a payment fails - if an invoice expires before it is paid - if a payment was interrupted (for example via page refresh) - if the action fails after payment * Remove unnecessary passing of act * Use AbortController for zap undos * Fix anon zap update not updating bolt color * Fix zap counted towards anon sats even if logged in * Fix duplicate onComplete call * Fix downzap type error * Fix "missing field 'path' while writing result" error * Pass full item in downzap props The previous commit fixed cache updates for downzaps but then the cache update for custom zaps failed because 'path' wasn't included in the server response. This commit is the proper fix. * Parse lnc rpc error messages * Add hash to InvoiceExpiredError
2024-05-28 17:18:54 +00:00
prepaid
2023-11-21 23:32:22 +00:00
onSubmit={onSubmit}
className='mb-5'
storageKeyPrefix={sub ? undefined : 'territory'}
>
<Input
label='name'
name='name'
required
autoFocus
clear
maxLength={32}
prepend={<InputGroup.Text className='text-monospace'>~</InputGroup.Text>}
onChange={onNameChange}
warn={archived && (
<div className='d-flex align-items-center'>this territory is archived
<Info>
<ul className='fw-bold'>
<li>This territory got archived because the previous founder did not pay for the upkeep</li>
<li>You can proceed but will inherit the old content</li>
</ul>
</Info>
</div>
)}
2024-01-11 23:45:17 +00:00
/>
2023-11-21 23:32:22 +00:00
<MarkdownInput
label='description'
name='desc'
maxLength={MAX_TERRITORY_DESC_LENGTH}
required
minRows={3}
/>
<Input
label='post cost'
name='baseCost'
type='number'
groupClassName='mb-2'
2023-11-21 23:32:22 +00:00
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Checkbox
label='allow free posts'
name='allowFreebies'
groupClassName='ms-1'
/>
2023-11-21 23:32:22 +00:00
<CheckboxGroup label='post types' name='postTypes'>
<Row>
<Col xs={4} sm='auto'>
<Checkbox
inline
label='links'
value='LINK'
name='postTypes'
id='links-checkbox'
2023-11-21 23:32:22 +00:00
groupClassName='ms-1 mb-0'
/>
</Col>
<Col xs={4} sm='auto'>
<Checkbox
inline
label='discussions'
value='DISCUSSION'
name='postTypes'
id='discussions-checkbox'
2023-11-21 23:32:22 +00:00
groupClassName='ms-1 mb-0'
/>
</Col>
<Col xs={4} sm='auto'>
<Checkbox
inline
label='bounties'
value='BOUNTY'
name='postTypes'
id='bounties-checkbox'
2023-11-21 23:32:22 +00:00
groupClassName='ms-1 mb-0'
/>
</Col>
<Col xs={4} sm='auto'>
<Checkbox
inline
label='polls'
value='POLL'
name='postTypes'
id='polls-checkbox'
2023-11-21 23:32:22 +00:00
groupClassName='ms-1 mb-0'
/>
</Col>
</Row>
</CheckboxGroup>
{sub?.billingType !== 'ONCE' &&
<>
<CheckboxGroup
label={
<span className='d-flex align-items-center'>billing
{sub && sub.billingType !== 'ONCE' &&
<Info>
You will be credited what you paid for your current billing period when you change your billing period to a longer duration.
If you change from yearly to monthly, when your year ends, your monthly billing will begin.
</Info>}
</span>
}
name='billing'
groupClassName={billing !== 'once' ? 'mb-0' : ''}
>
<Checkbox
type='radio'
label='100k sats/month'
value='MONTHLY'
name='billingType'
id='monthly-checkbox'
handleChange={checked => checked && setBilling('monthly')}
groupClassName='ms-1 mb-0'
/>
<Checkbox
type='radio'
label='1m sats/year'
value='YEARLY'
name='billingType'
id='yearly-checkbox'
handleChange={checked => checked && setBilling('yearly')}
groupClassName='ms-1 mb-0'
/>
<Checkbox
type='radio'
label='3m sats once'
value='ONCE'
name='billingType'
id='once-checkbox'
handleChange={checked => checked && setBilling('once')}
groupClassName='ms-1 mb-0'
/>
</CheckboxGroup>
{billing !== 'once' &&
<Checkbox
label='auto-renew'
name='billingAutoRenew'
groupClassName='ms-1 mt-2'
/>}
</>}
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
body={
<>
<BootstrapForm.Label>moderation</BootstrapForm.Label>
<Checkbox
inline
label={
<div className='d-flex align-items-center'>enable moderation
<Info>
<ol>
<li>Outlaw posts and comments with a click</li>
<li>Your territory will get a <Badge bg='secondary'>moderated</Badge> badge</li>
</ol>
</Info>
</div>
}
name='moderated'
groupClassName='ms-1'
/>
<BootstrapForm.Label>nsfw</BootstrapForm.Label>
<Checkbox
inline
label={
<div className='d-flex align-items-center'>mark as nsfw
<Info>
<ol>
<li>Let stackers know that your territory may contain explicit content</li>
<li>Your territory will get a <Badge bg='secondary'>nsfw</Badge> badge</li>
</ol>
</Info>
</div>
}
name='nsfw'
groupClassName='ms-1'
/>
</>
}
/>
2023-11-21 23:32:22 +00:00
<div className='mt-3 d-flex justify-content-end'>
<FeeButton
text={sub ? 'save' : 'found it'}
variant='secondary'
disabled={sub?.status === 'STOPPED'}
/>
</div>
</Form>
</FeeButtonProvider>
)
}