fix: refine poll rendering and inline voting UX
This commit is contained in:
@@ -2,11 +2,11 @@
|
|||||||
import type {ComponentProps} from "svelte"
|
import type {ComponentProps} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {PollResponse} from "nostr-tools/kinds"
|
import {PollResponse} from "nostr-tools/kinds"
|
||||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import {deriveEvents} from "@app/core/state"
|
import {deriveEvents} from "@app/core/state"
|
||||||
import {getPollResults} from "@app/util/polls"
|
import {getPollResults} from "@app/util/polls"
|
||||||
|
|
||||||
const props: ComponentProps<typeof ContentMinimal> = $props()
|
const props: ComponentProps<typeof Content> = $props()
|
||||||
|
|
||||||
const responses = deriveEvents([{kinds: [PollResponse], "#e": [props.event.id]}])
|
const responses = deriveEvents([{kinds: [PollResponse], "#e": [props.event.id]}])
|
||||||
|
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<span class="text-sm">{props.event.content || "Poll"}</span>
|
<Content event={props.event} url={props.url} />
|
||||||
<span class="text-xs opacity-50">{$results.voters} voter{$results.voters === 1 ? "" : "s"}</span>
|
<span class="text-xs opacity-50">{$results.voters} voter{$results.voters === 1 ? "" : "s"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<p class="text-xl">{props.event.content || "Poll"}</p>
|
<Content event={props.event} showEntire url={props.url} />
|
||||||
|
|
||||||
{#if props.url}
|
{#if props.url}
|
||||||
<PollVotes url={props.url} event={props.event} />
|
<PollVotes url={props.url} event={props.event} />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onDestroy} from "svelte"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey, publishThunk, abortThunk} from "@welshman/app"
|
||||||
import {PollResponse} from "nostr-tools/kinds"
|
import {PollResponse} from "nostr-tools/kinds"
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
import {formatTimestampRelative} from "@welshman/lib"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import {noop} from "@welshman/lib"
|
||||||
|
import {stopPropagation} from "@lib/html"
|
||||||
import {deriveEvents} from "@app/core/state"
|
import {deriveEvents} from "@app/core/state"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {publishPollResponse} from "@app/core/commands"
|
import {makePollResponse} from "@app/core/commands"
|
||||||
import {
|
import {
|
||||||
getPollEndsAt,
|
getPollEndsAt,
|
||||||
getPollOptions,
|
getPollOptions,
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
const options = getPollOptions(event)
|
const options = getPollOptions(event)
|
||||||
const closed = isPollClosed(event)
|
const closed = isPollClosed(event)
|
||||||
const endsAt = getPollEndsAt(event)
|
const endsAt = getPollEndsAt(event)
|
||||||
|
const publishDelay = pollType === "multiplechoice" ? 10_000 : undefined
|
||||||
|
|
||||||
const getOwnResponse = (responses: TrustedEvent[]) => {
|
const getOwnResponse = (responses: TrustedEvent[]) => {
|
||||||
let latest: TrustedEvent | undefined
|
let latest: TrustedEvent | undefined
|
||||||
@@ -46,43 +49,66 @@
|
|||||||
return latest
|
return latest
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = $derived(getPollResults(event, $responses))
|
const publishSelection = (selection: string[]) => {
|
||||||
const ownResponse = $derived(getOwnResponse($responses))
|
if (activeThunk) {
|
||||||
|
abortThunk(activeThunk)
|
||||||
const submit = async () => {
|
|
||||||
if (closed) {
|
|
||||||
return pushToast({theme: "error", message: "This poll is closed."})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = pollType === "singlechoice" ? [selectedIds[0]].filter(Boolean) : selectedIds
|
if (selection.length === 0) {
|
||||||
|
activeThunk = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeThunk = publishThunk({
|
||||||
|
relays: [url],
|
||||||
|
event: makePollResponse({event, selectedIds: selection}),
|
||||||
|
delay: publishDelay,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishCurrentSelection = () => {
|
||||||
|
const selection = pollType === "singlechoice" ? selectedIds.slice(0, 1) : selectedIds
|
||||||
|
|
||||||
if (selection.length === 0) {
|
if (selection.length === 0) {
|
||||||
return pushToast({theme: "error", message: "Please select at least one option."})
|
return pushToast({theme: "error", message: "Please select at least one option."})
|
||||||
}
|
}
|
||||||
|
|
||||||
publishPollResponse({relays: [url], event, selectedIds: selection})
|
publishSelection(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const results = $derived(getPollResults(event, $responses))
|
||||||
|
const ownResponse = $derived(getOwnResponse($responses))
|
||||||
|
|
||||||
const setSingleChoice = (id: string) => {
|
const setSingleChoice = (id: string) => {
|
||||||
selectedIds = [id]
|
selectedIds = [id]
|
||||||
|
publishCurrentSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleMultipleChoice = (id: string) => {
|
const toggleMultipleChoice = (id: string) => {
|
||||||
selectedIds = selectedIds.includes(id)
|
selectedIds = selectedIds.includes(id)
|
||||||
? selectedIds.filter(selectedId => selectedId !== id)
|
? selectedIds.filter(selectedId => selectedId !== id)
|
||||||
: [...selectedIds, id]
|
: [...selectedIds, id]
|
||||||
|
|
||||||
|
publishCurrentSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedIds = $state<string[]>([])
|
let selectedIds = $state<string[]>([])
|
||||||
|
let activeThunk: ReturnType<typeof publishThunk> | undefined
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (ownResponse) {
|
if (ownResponse) {
|
||||||
selectedIds = getPollResponseSelections(ownResponse, pollType)
|
selectedIds = getPollResponseSelections(ownResponse, pollType)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (activeThunk) {
|
||||||
|
abortThunk(activeThunk)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 rounded-box bg-base-200 p-4">
|
<div class="card2 bg-alt p-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{pollType === "multiplechoice" ? "Multiple choice" : "Single choice"}
|
{pollType === "multiplechoice" ? "Multiple choice" : "Single choice"}
|
||||||
@@ -111,12 +137,14 @@
|
|||||||
type="radio"
|
type="radio"
|
||||||
class="radio radio-primary radio-sm"
|
class="radio radio-primary radio-sm"
|
||||||
checked={selectedIds[0] === option.id}
|
checked={selectedIds[0] === option.id}
|
||||||
|
onclick={stopPropagation(noop)}
|
||||||
onchange={() => setSingleChoice(option.id)} />
|
onchange={() => setSingleChoice(option.id)} />
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-primary checkbox-sm"
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
checked={selectedIds.includes(option.id)}
|
checked={selectedIds.includes(option.id)}
|
||||||
|
onclick={stopPropagation(noop)}
|
||||||
onchange={() => toggleMultipleChoice(option.id)} />
|
onchange={() => toggleMultipleChoice(option.id)} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -130,10 +158,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !closed}
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Button class="btn btn-primary btn-sm" onclick={submit}>Cast vote</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user