Implement poll

This commit is contained in:
AkiraFukushima 2023-11-04 13:03:56 +09:00
parent a7e05c9d03
commit 5fd257dc7a
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
4 changed files with 158 additions and 1 deletions

View File

@ -3,6 +3,7 @@
@tailwind utilities;
.emojione {
display: inline-block;
width: 1.2rem;
height: 1.2rem;
}

View File

@ -45,6 +45,23 @@ export default function Timeline(props: Props) {
}
}
const updateStatus = (current: Array<Entity.Status>, status: Entity.Status) => {
const renew = current.map(s => {
if (s.id === status.id) {
return status
} else if (s.reblog && s.reblog.id === status.id) {
return Object.assign({}, s, { reblog: status })
} else if (status.reblog && s.id === status.reblog.id) {
return status.reblog
} else if (status.reblog && s.reblog && s.reblog.id === status.reblog.id) {
return Object.assign({}, s, { reblog: status.reblog })
} else {
return s
}
})
return renew
}
return (
<section className="h-full w-full">
<div className="w-full bg-blue-950 text-blue-100 p-2 flex justify-between">
@ -59,7 +76,14 @@ export default function Timeline(props: Props) {
<Virtuoso
style={{ height: '100%' }}
data={statuses}
itemContent={(_, status) => <Status client={props.client} status={status} key={status.id} />}
itemContent={(_, status) => (
<Status
client={props.client}
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
/>
)}
/>
</div>
</section>

View File

@ -0,0 +1,124 @@
import dayjs from 'dayjs'
import { Progress, Button, Radio, Label, Checkbox } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
type Props = {
poll: Entity.Poll
client: MegalodonInterface
onRefresh: () => void
}
export default function Poll(props: Props) {
if (props.poll.voted || props.poll.expired) {
return <PollResult {...props} />
} else if (props.poll.multiple) {
return <MultiplePoll {...props} />
} else {
return <SimplePoll {...props} />
}
}
function SimplePoll(props: Props) {
const vote = async () => {
const elements = document.getElementsByName(props.poll.id)
let checked: number | null = null
elements.forEach((element, index) => {
if ((element as HTMLInputElement).checked) {
checked = index
}
})
if (checked !== null) {
await props.client.votePoll(props.poll.id, [checked])
props.onRefresh()
}
}
return (
<div className="my-2">
{props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1">
<Radio id={option.title} name={props.poll.id} value={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={vote}>
Vote
</Button>
<div>{props.poll.votes_count} people</div>
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
</div>
</div>
)
}
function MultiplePoll(props: Props) {
const vote = async () => {
let checked: Array<number> = []
props.poll.options.forEach((value, index) => {
const element = document.getElementById(value.title) as HTMLInputElement
if (element.checked) {
checked = [...checked, index]
}
})
if (checked.length > 0) {
await props.client.votePoll(props.poll.id, checked)
props.onRefresh()
}
}
return (
<div className="my-2">
{props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1">
<Checkbox id={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={vote}>
Vote
</Button>
<div>{props.poll.votes_count} people</div>
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
</div>
</div>
)
}
function PollResult(props: Props) {
return (
<div className="my-2">
{props.poll.options.map((option, index) => (
<div key={index}>
<span className="pr-2">{percent(option.votes_count ?? 0, props.poll.votes_count)}%</span>
<span>{option.title}</span>
<Progress progress={percent(option.votes_count ?? 0, props.poll.votes_count)} />
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button outline={true} size="xs" onClick={props.onRefresh}>
Refresh
</Button>
<div>{props.poll.votes_count} people</div>
{props.poll.expired ? (
<div>Closed</div>
) : (
<div>
<time dateTime={props.poll.expires_at}>{dayjs(props.poll.expires_at).format('YYYY-MM-DD HH:mm:ss')}</time>
</div>
)}
</div>
</div>
)
}
const percent = (votes: number, all: number) => {
if (all > 0) {
return Math.round((votes * 100) / all)
} else {
return 0
}
}

View File

@ -5,15 +5,22 @@ import Body from './Body'
import Media from './Media'
import emojify from '@/utils/emojify'
import Card from './Card'
import Poll from './Poll'
type Props = {
status: Entity.Status
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
}
export default function Status(props: Props) {
const status = originalStatus(props.status)
const onRefresh = async () => {
const res = await props.client.getStatus(status.id)
props.onRefresh(res.data)
}
return (
<div className="border-b mr-2 py-1">
{rebloggedHeader(props.status)}
@ -35,6 +42,7 @@ export default function Status(props: Props) {
</div>
</div>
<Body status={status} />
{status.poll && <Poll poll={status.poll} onRefresh={onRefresh} client={props.client} />}
{status.card && <Card card={status.card} />}
<Media media={status.media_attachments} />
</div>