forked from coracle/flotilla
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a15391a66a |
+1
-1
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user