chore: refine NIP-88 polls code structure and reactivity #153

Closed
Khushvendra wants to merge 1 commits from Khushvendra/flotilla:chore/nip-88-polls-simplify into dev
4 changed files with 89 additions and 55 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
"format": "git diff HEAD --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
"format:all": "prettier --write src",
"prepare": "husky"
},
-14
View File
@@ -1,23 +1,9 @@
<script lang="ts">
import type {ComponentProps} from "svelte"
import {onMount} from "svelte"
import {request} from "@welshman/net"
import {PollResponse} from "nostr-tools/kinds"
import PollVotes from "@app/components/PollVotes.svelte"
import Content from "@app/components/Content.svelte"
const props: ComponentProps<typeof Content> = $props()
onMount(() => {
if (!props.url) {
return
}
request({
relays: [props.url],
filters: [{kinds: [PollResponse], "#e": [props.event.id]}],
})
})
Review

We actually do need this, sync is only a best-effort thing to speed up page transitions.

We actually do need this, sync is only a best-effort thing to speed up page transitions.
</script>
<div class="flex flex-col gap-3">
+78 -20
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {now, randomId, removeUndefined} from "@welshman/lib"
import {makeEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {Poll} from "nostr-tools/kinds"
@@ -52,45 +52,99 @@
options = options.map(option => (option.id === id ? {...option, value} : option))
}
const reorderOptions = (targetId: string) => {
if (!draggedOptionId) {
const swapOptions = (sourceId: string, targetId: string) => {
if (sourceId === targetId) {
return
}
const sourceIndex = options.findIndex(option => option.id === draggedOptionId)
const sourceIndex = options.findIndex(option => option.id === sourceId)
const targetIndex = options.findIndex(option => option.id === targetId)
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
return
}
options = insertAt(targetIndex, options[sourceIndex], removeAt(sourceIndex, options))
const reordered = [...options]
const sourceOption = reordered[sourceIndex]
reordered[sourceIndex] = reordered[targetIndex]
reordered[targetIndex] = sourceOption
options = reordered
Review

I think insertAt/removeAt is actually much cleaner

I think insertAt/removeAt is actually much cleaner
}
const onDragStart = (e: DragEvent, id: string) => {
draggedOptionId = id
const getOptionId = (target: EventTarget | null) => {
if (!(target instanceof HTMLElement)) {
return undefined
}
return target.dataset.optionId
}
const getClosestOptionId = (target: EventTarget | null) => {
if (!(target instanceof HTMLElement)) {
return undefined
}
return target.closest<HTMLElement>("[data-option-id]")?.dataset.optionId
}
const handleOptionDragStart = (e: DragEvent) => {
const optionId = getOptionId(e.currentTarget)
if (!optionId) {
return
}
draggedOptionId = optionId
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move"
e.dataTransfer.setData("text/plain", id)
e.dataTransfer.setData("text/plain", optionId)
}
}
const onDragOver = (e: DragEvent, targetId: string) => {
const handleOptionDragOver = (e: DragEvent) => {
e.preventDefault()
reorderOptions(targetId)
}
const onDrop = (e: DragEvent, targetId: string) => {
const handleOptionDrop = (e: DragEvent) => {
e.preventDefault()
reorderOptions(targetId)
const targetId = getOptionId(e.currentTarget)
if (!draggedOptionId || !targetId) {
draggedOptionId = undefined
return
}
swapOptions(draggedOptionId, targetId)
draggedOptionId = undefined
}
const onDragEnd = () => {
const handleOptionDragEnd = () => {
draggedOptionId = undefined
}
const handleOptionInput = (e: Event) => {
const optionId = getOptionId(e.currentTarget)
if (!optionId || !(e.currentTarget instanceof HTMLInputElement)) {
return
}
updateOption(optionId, e.currentTarget.value)
}
const handleRemoveOption = (e: Event) => {
const optionId = getClosestOptionId(e.target)
if (!optionId) {
return
}
removeOption(optionId)
}
const submit = async () => {
if (!title.trim()) {
return pushToast({theme: "error", message: "Please provide a title for your poll."})
@@ -132,6 +186,8 @@
history.back()
}
const onSubmit = preventDefault(submit)
let title = $state("")
let pollType = $state<PollType>("singlechoice")
let endsAt = $state<number | undefined>()
@@ -142,7 +198,7 @@
let draggedOptionId = $state<string | undefined>()
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
<Modal tag="form" onsubmit={onSubmit}>
<ModalBody>
<ModalHeader>
<ModalTitle>Create a Poll</ModalTitle>
@@ -175,24 +231,26 @@
{#each options as option, index (option.id)}
<div
class="flex items-center gap-2"
data-option-id={option.id}
draggable="true"
role="listitem"
ondragstart={e => onDragStart(e, option.id)}
ondragover={e => onDragOver(e, option.id)}
ondrop={e => onDrop(e, option.id)}
ondragend={onDragEnd}>
ondragstart={handleOptionDragStart}
ondragover={handleOptionDragOver}
ondrop={handleOptionDrop}
ondragend={handleOptionDragEnd}>
<div class="cursor-move opacity-70" aria-label="Drag handle">
<Icon icon={HamburgerMenu} size={4} />
</div>
<label class="input input-bordered flex w-full items-center gap-2">
<input
data-option-id={option.id}
value={option.value}
class="grow"
type="text"
placeholder={`Option ${index + 1}`}
oninput={e => updateOption(option.id, e.currentTarget.value)} />
oninput={handleOptionInput} />
</label>
<Button class="btn btn-ghost btn-sm" onclick={() => removeOption(option.id)}>
<Button class="btn btn-ghost btn-sm" onclick={handleRemoveOption}>
<Icon icon={MinusCircle} size={4} />
</Button>
</div>
+10 -20
View File
@@ -1,8 +1,5 @@
<script lang="ts">
import {tweened} from "svelte/motion"
import type {TrustedEvent} from "@welshman/util"
import {noop} from "@welshman/lib"
import {stopPropagation} from "@lib/html"
import {getPollType, isPollClosed} from "@app/util/polls"
type Props = {
@@ -23,22 +20,15 @@
const selected = $derived(
pollType === "singlechoice" ? selectedIds[0] === option.id : selectedIds.includes(option.id),
)
const onselect = () =>
const handleInputClick = (e: MouseEvent) => {
e.stopPropagation()
}
const handleSelectChange = () =>
pollType === "singlechoice" ? setSingleChoice(option.id) : toggleMultipleChoice(option.id)
const votes = $derived(results.options.find(r => r.id === option.id)?.votes || 0)
const maxVotes = $derived(Math.max(...results.options.map(r => r.votes), 1))
const tweenedVotes = tweened(votes, {duration: 300})
const tweenedMax = tweened(maxVotes, {duration: 300})
$effect(() => {
tweenedVotes.set(votes)
})
$effect(() => {
tweenedMax.set(maxVotes)
})
</script>
<div class="flex flex-col gap-2 card2 card2-sm bg-alt">
@@ -51,20 +41,20 @@
type="radio"
class="radio radio-primary radio-sm"
checked={selected}
onclick={stopPropagation(noop)}
onchange={onselect} />
onclick={handleInputClick}
onchange={handleSelectChange} />
{:else}
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
checked={selected}
onclick={stopPropagation(noop)}
onchange={onselect} />
onclick={handleInputClick}
onchange={handleSelectChange} />
{/if}
{/if}
<span class="truncate">{option.label}</span>
</label>
<span class="whitespace-nowrap text-xs opacity-75">{votes} vote{votes === 1 ? "" : "s"}</span>
</div>
<progress class="progress progress-primary" value={$tweenedVotes} max={$tweenedMax}></progress>
<progress class="progress progress-primary" value={votes} max={maxVotes}></progress>
</div>