feat: add NIP-88 poll support
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import {randomId, removeUndefined} from "@welshman/lib"
|
||||
import {makeEvent} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {Poll} from "nostr-tools/kinds"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import PlusCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
import {PROTECTED} from "@app/core/state"
|
||||
import {canEnforceNip70} from "@app/core/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
}
|
||||
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const addOption = () => {
|
||||
options = [...options, ""]
|
||||
}
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
options = options.filter((_, optionIndex) => optionIndex !== index)
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!title.trim()) {
|
||||
return pushToast({theme: "error", message: "Please provide a title for your poll."})
|
||||
}
|
||||
|
||||
const nonEmptyOptions = removeUndefined(options.map(option => option.trim() || undefined))
|
||||
|
||||
if (nonEmptyOptions.length < 2) {
|
||||
return pushToast({theme: "error", message: "Please provide at least two options."})
|
||||
}
|
||||
|
||||
const parsedEndsAt = endsAt ? Math.floor(new Date(endsAt).getTime() / 1000) : undefined
|
||||
|
||||
const tags: string[][] = [
|
||||
...nonEmptyOptions.map(option => ["option", randomId(), option]),
|
||||
["polltype", pollType],
|
||||
["relay", url],
|
||||
]
|
||||
|
||||
if (parsedEndsAt) {
|
||||
tags.push(["endsAt", String(parsedEndsAt)])
|
||||
}
|
||||
|
||||
if (h) {
|
||||
tags.push(["h", h])
|
||||
}
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
}
|
||||
|
||||
publishThunk({
|
||||
relays: [url],
|
||||
event: makeEvent(Poll, {content: title.trim(), tags}),
|
||||
})
|
||||
|
||||
history.back()
|
||||
}
|
||||
|
||||
let title = $state("")
|
||||
let pollType = $state<"singlechoice" | "multiplechoice">("singlechoice")
|
||||
let endsAt = $state("")
|
||||
let options = $state(["Yes", "No"])
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Create a Poll</ModalTitle>
|
||||
<ModalSubtitle>Ask the room a question with one or more answers.</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="col-8 relative">
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Question*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus={!isMobile}
|
||||
bind:value={title}
|
||||
class="grow"
|
||||
type="text"
|
||||
placeholder="What would you like to ask?" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Options*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each options as option, index (index)}
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={options[index]} class="grow" type="text" placeholder={`Option ${index + 1}`} />
|
||||
</label>
|
||||
<Button class="btn btn-ghost btn-sm" onclick={() => removeOption(index)}>
|
||||
<Icon icon={MinusCircle} size={4} />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button class="btn btn-ghost btn-sm self-start" onclick={addOption}>
|
||||
<Icon icon={PlusCircle} size={4} />
|
||||
Add option
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
Poll type
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select class="select select-bordered w-full max-w-xs" bind:value={pollType}>
|
||||
<option value="singlechoice">Single choice</option>
|
||||
<option value="multiplechoice">Multiple choice</option>
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
Ends at
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<input bind:value={endsAt} class="input input-bordered w-full max-w-xs" type="datetime-local" />
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary">Create Poll</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user