Compare commits

...

1 Commits

Author SHA1 Message Date
Khushvendra a15391a66a refactor: simplify NIP-88 poll UI components 2026-04-04 15:58:40 +05:30
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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src", "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", "format:all": "prettier --write src",
"prepare": "husky" "prepare": "husky"
}, },
-14
View File
@@ -1,23 +1,9 @@
<script lang="ts"> <script lang="ts">
import type {ComponentProps} from "svelte" 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 PollVotes from "@app/components/PollVotes.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
const props: ComponentProps<typeof Content> = $props() const props: ComponentProps<typeof Content> = $props()
onMount(() => {
if (!props.url) {
return
}
request({
relays: [props.url],
filters: [{kinds: [PollResponse], "#e": [props.event.id]}],
})
})
</script> </script>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
+78 -20
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <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 {makeEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app" import {publishThunk} from "@welshman/app"
import {Poll} from "nostr-tools/kinds" import {Poll} from "nostr-tools/kinds"
@@ -52,45 +52,99 @@
options = options.map(option => (option.id === id ? {...option, value} : option)) options = options.map(option => (option.id === id ? {...option, value} : option))
} }
const reorderOptions = (targetId: string) => { const swapOptions = (sourceId: string, targetId: string) => {
if (!draggedOptionId) { if (sourceId === targetId) {
return 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) const targetIndex = options.findIndex(option => option.id === targetId)
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) { if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
return 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
} }
const onDragStart = (e: DragEvent, id: string) => { const getOptionId = (target: EventTarget | null) => {
draggedOptionId = id 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) { if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move" 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() e.preventDefault()
reorderOptions(targetId)
} }
const onDrop = (e: DragEvent, targetId: string) => { const handleOptionDrop = (e: DragEvent) => {
e.preventDefault() e.preventDefault()
reorderOptions(targetId)
const targetId = getOptionId(e.currentTarget)
if (!draggedOptionId || !targetId) {
draggedOptionId = undefined
return
}
swapOptions(draggedOptionId, targetId)
draggedOptionId = undefined draggedOptionId = undefined
} }
const onDragEnd = () => { const handleOptionDragEnd = () => {
draggedOptionId = undefined 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 () => { const submit = async () => {
if (!title.trim()) { if (!title.trim()) {
return pushToast({theme: "error", message: "Please provide a title for your poll."}) return pushToast({theme: "error", message: "Please provide a title for your poll."})
@@ -132,6 +186,8 @@
history.back() history.back()
} }
const onSubmit = preventDefault(submit)
let title = $state("") let title = $state("")
let pollType = $state<PollType>("singlechoice") let pollType = $state<PollType>("singlechoice")
let endsAt = $state<number | undefined>() let endsAt = $state<number | undefined>()
@@ -142,7 +198,7 @@
let draggedOptionId = $state<string | undefined>() let draggedOptionId = $state<string | undefined>()
</script> </script>
<Modal tag="form" onsubmit={preventDefault(submit)}> <Modal tag="form" onsubmit={onSubmit}>
<ModalBody> <ModalBody>
<ModalHeader> <ModalHeader>
<ModalTitle>Create a Poll</ModalTitle> <ModalTitle>Create a Poll</ModalTitle>
@@ -175,24 +231,26 @@
{#each options as option, index (option.id)} {#each options as option, index (option.id)}
<div <div
class="flex items-center gap-2" class="flex items-center gap-2"
data-option-id={option.id}
draggable="true" draggable="true"
role="listitem" role="listitem"
ondragstart={e => onDragStart(e, option.id)} ondragstart={handleOptionDragStart}
ondragover={e => onDragOver(e, option.id)} ondragover={handleOptionDragOver}
ondrop={e => onDrop(e, option.id)} ondrop={handleOptionDrop}
ondragend={onDragEnd}> ondragend={handleOptionDragEnd}>
<div class="cursor-move opacity-70" aria-label="Drag handle"> <div class="cursor-move opacity-70" aria-label="Drag handle">
<Icon icon={HamburgerMenu} size={4} /> <Icon icon={HamburgerMenu} size={4} />
</div> </div>
<label class="input input-bordered flex w-full items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<input <input
data-option-id={option.id}
value={option.value} value={option.value}
class="grow" class="grow"
type="text" type="text"
placeholder={`Option ${index + 1}`} placeholder={`Option ${index + 1}`}
oninput={e => updateOption(option.id, e.currentTarget.value)} /> oninput={handleOptionInput} />
</label> </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} /> <Icon icon={MinusCircle} size={4} />
</Button> </Button>
</div> </div>
+10 -20
View File
@@ -1,8 +1,5 @@
<script lang="ts"> <script lang="ts">
import {tweened} from "svelte/motion"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {noop} from "@welshman/lib"
import {stopPropagation} from "@lib/html"
import {getPollType, isPollClosed} from "@app/util/polls" import {getPollType, isPollClosed} from "@app/util/polls"
type Props = { type Props = {
@@ -23,22 +20,15 @@
const selected = $derived( const selected = $derived(
pollType === "singlechoice" ? selectedIds[0] === option.id : selectedIds.includes(option.id), 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) pollType === "singlechoice" ? setSingleChoice(option.id) : toggleMultipleChoice(option.id)
const votes = $derived(results.options.find(r => r.id === option.id)?.votes || 0) 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 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> </script>
<div class="flex flex-col gap-2 card2 card2-sm bg-alt"> <div class="flex flex-col gap-2 card2 card2-sm bg-alt">
@@ -51,20 +41,20 @@
type="radio" type="radio"
class="radio radio-primary radio-sm" class="radio radio-primary radio-sm"
checked={selected} checked={selected}
onclick={stopPropagation(noop)} onclick={handleInputClick}
onchange={onselect} /> onchange={handleSelectChange} />
{:else} {:else}
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-primary checkbox-sm" class="checkbox checkbox-primary checkbox-sm"
checked={selected} checked={selected}
onclick={stopPropagation(noop)} onclick={handleInputClick}
onchange={onselect} /> onchange={handleSelectChange} />
{/if} {/if}
{/if} {/if}
<span class="truncate">{option.label}</span> <span class="truncate">{option.label}</span>
</label> </label>
<span class="whitespace-nowrap text-xs opacity-75">{votes} vote{votes === 1 ? "" : "s"}</span> <span class="whitespace-nowrap text-xs opacity-75">{votes} vote{votes === 1 ? "" : "s"}</span>
</div> </div>
<progress class="progress progress-primary" value={$tweenedVotes} max={$tweenedMax}></progress> <progress class="progress progress-primary" value={votes} max={maxVotes}></progress>
</div> </div>