Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

633 event post type #1140

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { msatsToSats } from '@/lib/format'
import { parse } from 'tldts'
import uu from 'url-unshort'
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
import { actSchema, advSchema, bountySchema, eventSchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers, notifyMention } from '@/lib/webPush'
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '@/lib/item'
import { datePivot, whenRange } from '@/lib/time'
Expand Down Expand Up @@ -784,6 +784,14 @@ export default {
return await createItem(parent, item, { me, models, lnd, hash, hmac })
}
},
upsertEvent: async (parent, { id, hash, hmac, ...item }, { me, models }) => {
await ssValidate(eventSchema, item, { models, me })
if (id) {
return await updateItem(parent, { id, ...item }, { me, models, hash, hmac })
} else {
return await createItem(parent, item, { me, models, hash, hmac })
}
},
upsertPoll: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
const numExistingChoices = id
? await models.pollOption.count({
Expand Down Expand Up @@ -1387,7 +1395,7 @@ export const SELECT =
"Item"."rootId", "Item".upvotes, "Item".company, "Item".location, "Item".remote, "Item"."deletedAt",
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats,
"Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes",
"Item"."weightedDownVotes", "Item".freebie, "Item".bio, "Item"."otsHash", "Item"."bountyPaidTo",
"Item"."weightedDownVotes", "Item".freebie, "Item".bio, "Item"."otsHash", "Item"."bountyPaidTo", "Item"."eventDate", "Item"."eventLocation",
ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls", "Item".outlawed,
"Item"."pollExpiresAt"`

Expand Down
5 changes: 4 additions & 1 deletion api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ export default gql`
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int, hash: String, hmac: String, boost: Int, forward: [ItemForwardInput]): Item!
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
text: String!, url: String!, maxBid: Int!, status: String, logo: Int, hash: String, hmac: String): Item!
text: String!, url: String!, maxBid: Int!, status: String, logo: Int, hash: String, hmac: String): Item!
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String, pollExpiresAt: Date): Item!
upsertEvent(id: ID, sub: String, title: String!, eventDate: Date!, eventLocation: String!, text: String, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
updateNoteId(id: ID!, noteId: String!): Item!
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
act(id: ID!, sats: Int, act: String, idempotent: Boolean, hash: String, hmac: String): ItemActResult!
Expand Down Expand Up @@ -124,6 +125,8 @@ export default gql`
forwards: [ItemForward]
imgproxyUrls: JSONObject
rel: String
eventDate: Date
eventLocation: String
}

input ItemForwardInput {
Expand Down
156 changes: 156 additions & 0 deletions components/event-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Form, Input, MarkdownInput, DateTimeInput } from '@/components/form'
import { useRouter } from 'next/router'
import { gql, useApolloClient, useMutation } from '@apollo/client'
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
import useCrossposter from './use-crossposter'
import { eventSchema } from '@/lib/validate'
import { SubSelectInitial } from './sub-select'
import { useCallback } from 'react'
import { normalizeForwards, toastDeleteScheduled } from '@/lib/form'
import { MAX_TITLE_LENGTH } from '@/lib/constants'
import { useMe } from './me'
import { useToast } from './toast'
import { ItemButtonBar } from './post'
import Countdown from './countdown'

export function EventForm ({
item,
sub,
editThreshold,
titleLabel = 'Title',
dateLabel = 'Date',
locationLabel = 'Location',
textLabel = 'Description',
handleSubmit,
children
}) {
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const toaster = useToast()
const crossposter = useCrossposter()
const schema = eventSchema({ client, me })
const [upsertEvent] = useMutation(
gql`
mutation upsertEvent(
$sub: String
$id: ID
$title: String!
$eventDate: Date!
$eventLocation: String!
$text: String
$boost: Int
$forward: [ItemForwardInput]
$hash: String
$hmac: String
) {
upsertEvent(
sub: $sub
id: $id
title: $title
eventDate: $eventDate
eventLocation: $eventLocation
text: $text
boost: $boost
forward: $forward
hash: $hash
hmac: $hmac
) {
id
deleteScheduledAt
}
}
`
)

const onSubmit = useCallback(
async ({ crosspost, boost, ...values }) => {
const { data, error } = await upsertEvent({
variables: {
sub: item?.subName || sub?.name,
id: item?.id,
boost: boost ? Number(boost) : undefined,
...values,
forward: normalizeForwards(values.forward)
}
})
if (error) {
throw new Error({ message: error.toString() })
}

const eventId = data?.upsertEvent?.id

if (crosspost && eventId) {
await crossposter(eventId)
}

if (item) {
await router.push(`/items/${item.id}`)
} else {
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
toastDeleteScheduled(toaster, data, 'upsertEvent', !!item, values.text)
}, [upsertEvent, router]
)

return (
<Form
initial={{
title: item?.title || '',
eventDate: item?.eventDate || '',
eventLocation: item?.eventLocation || '',
text: item?.text || '',
crosspost: item ? !!item.noteId : me?.privates?.nostrCrossposting,
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}
invoiceable={{ requireSession: true }}
onSubmit={
handleSubmit ||
onSubmit
}
storageKeyPrefix={item ? undefined : 'event'}
>
{children}
<DateTimeInput
label={dateLabel}
name='eventDate'
className='pr-4'
required
/>
<Input
label={titleLabel}
name='title'
required
autoFocus
clear
maxLength={MAX_TITLE_LENGTH}
/>

<Input
label={locationLabel}
name='eventLocation'
required
/>
<MarkdownInput
topLevel
label={textLabel}
name='text'
minRows={6}
hint={
editThreshold
? (
<div className='text-muted fw-bold'>
<Countdown date={editThreshold} />
</div>
)
: null
}
/>
<AdvPostForm edit={!!item} item={item} />
<ItemButtonBar itemId={item?.id} canDelete={false} />
</Form>
)
}
4 changes: 3 additions & 1 deletion components/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
const router = useRouter()

const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)

const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, timeZone: 'America/Chicago' }
return (
<>
{rank
Expand Down Expand Up @@ -77,6 +77,8 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
<BountyIcon className={`${styles.bountyIcon} ${item.bountyPaidTo?.length ? 'fill-success' : 'fill-grey'}`} height={16} width={16} />
</ActionTooltip>
</span>}
{item.eventDate && <span> {new Date(item.eventDate).toLocaleString('en-us', dateOptions)} </span>}
{item.eventLocation && <span>at {item.eventLocation}</span>}
{item.forwards?.length > 0 && <span className={styles.icon}><Prism className='fill-grey ms-1' height={14} width={14} /></span>}
{image && <span className={styles.icon}><ImageIcon className='fill-grey ms-2' height={16} width={16} /></span>}
</Link>
Expand Down
15 changes: 15 additions & 0 deletions components/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DiscussionForm } from './discussion-form'
import { LinkForm } from './link-form'
import { PollForm } from './poll-form'
import { BountyForm } from './bounty-form'
import { EventForm } from './event-form'
import SubSelect from './sub-select'
import { useCallback, useState } from 'react'
import FeeButton, { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
Expand Down Expand Up @@ -67,6 +68,15 @@ export function PostForm ({ type, sub, children }) {
</Link>
)
}

if (sub?.postTypes?.includes('EVENT')) {
const array = postButtons.length < 2 ? postButtons : morePostButtons
array.push(
<Link key='EVENT' href={prefix + '/post?type=event'}>
<Button onClick={checkSession} variant={postButtons.length < 2 ? 'secondary' : 'info'}>event</Button>
</Link>
)
}
} else {
postButtons = [
<Link key='LINK' href={prefix + '/post?type=link'}>
Expand All @@ -82,6 +92,9 @@ export function PostForm ({ type, sub, children }) {
</Link>,
<Link key='BOUNTY' href={prefix + '/post?type=bounty'}>
<Button onClick={checkSession} variant='info'>bounty</Button>
</Link>,
<Link key='EVENT' href={prefix + '/post?type=event'}>
<Button variant='info'>event</Button>
</Link>
]
}
Expand Down Expand Up @@ -145,6 +158,8 @@ export function PostForm ({ type, sub, children }) {
FormType = PollForm
} else if (type === 'bounty') {
FormType = BountyForm
} else if (type === 'event') {
FormType = EventForm
}

return (
Expand Down
2 changes: 1 addition & 1 deletion components/recent-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function RecentHeader ({ type, sub }) {

const items = sub
? ITEM_TYPES_UNIVERSAL.concat(sub.postTypes.map(p =>
['LINK', 'DISCUSSION', 'POLL', 'JOB'].includes(p) ? `${p.toLowerCase()}s` : 'bounties'
['LINK', 'DISCUSSION', 'POLL', 'JOB', 'EVENT'].includes(p) ? `${p.toLowerCase()}s` : 'bounties'
dillon-co marked this conversation as resolved.
Show resolved Hide resolved
))
: ITEM_TYPES

Expand Down
10 changes: 10 additions & 0 deletions components/territory-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ export default function TerritoryForm ({ sub }) {
groupClassName='ms-1 mb-0'
/>
</Col>
<Col xs={4} sm='auto'>
<Checkbox
inline
label='events'
value='EVENT'
name='postTypes'
id='polls-checkbox'
groupClassName='ms-1 mb-0'
/>
</Col>
</Row>
</CheckboxGroup>
{sub?.billingType !== 'ONCE' &&
Expand Down
2 changes: 2 additions & 0 deletions fragments/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const ITEM_FIELDS = gql`
mine
imgproxyUrls
rel
eventDate
eventLocation
}`

export const ITEM_FULL_FIELDS = gql`
Expand Down
2 changes: 1 addition & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const UPLOAD_TYPES_ALLOW = [
]
export const BOUNTY_MIN = 1000
export const BOUNTY_MAX = 10000000
export const POST_TYPES = ['LINK', 'DISCUSSION', 'BOUNTY', 'POLL']
export const POST_TYPES = ['LINK', 'DISCUSSION', 'BOUNTY', 'POLL', 'EVENT']
export const TERRITORY_BILLING_TYPES = ['MONTHLY', 'YEARLY', 'ONCE']
export const TERRITORY_GRACE_DAYS = 5
export const COMMENT_DEPTH_LIMIT = 8
Expand Down
3 changes: 2 additions & 1 deletion lib/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const toastDeleteScheduled = (toaster, upsertResponseData, dataKey, isEdi
upsertPoll: 'poll',
upsertBounty: 'bounty',
upsertJob: 'job',
upsertComment: 'comment'
upsertComment: 'comment',
upsertEvent: 'event'
}[dataKey] ?? 'item'

const message = `${itemType === 'comment' ? 'your comment' : isEdit ? `this ${itemType}` : `your new ${itemType}`} will be deleted at ${deleteScheduledAt.toLocaleString()}`
Expand Down
16 changes: 16 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,22 @@ export function bountySchema (args) {
})
}

export function eventSchema (args) {
return object({
title: titleValidator,
text: textValidator(MAX_POST_TEXT_LENGTH),
eventDate: date()
.required('date is required')
.min(new Date(), 'date must be in the future'),
...advPostSchemaMembers(args),
...subSelectSchemaMembers(args)
}).test({
name: 'post-type-supported',
test: ({ sub }) => subHasPostType(sub, 'EVENT', args),
message: 'territory does not support events'
})
}

export function discussionSchema (args) {
return object({
title: titleValidator,
Expand Down
Loading
Loading