merge from local

This commit is contained in:
SrGooglo 2024-10-25 09:39:15 +00:00
parent a986804cd1
commit 5ef95ac39a
4 changed files with 260 additions and 83 deletions

View File

@ -1,94 +1,215 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classNames from "classnames" import classnames from "classnames"
import { Icons, createIconRender } from "@components/Icons"
import { motion } from "framer-motion"
import { createIconRender } from "@components/Icons" import useWsEvents from "@hooks/useWsEvents"
import PostModel from "@models/post"
import "./index.less" import "./index.less"
const PollOption = (props) => { const PollOption = (props) => {
const { option, editMode, onRemove } = props async function onClick() {
if (typeof props.onClick === "function") {
await props.onClick(props.option.id)
}
}
return <div return <div
className={classNames( className={classnames(
"poll-option", "poll-option",
{ {
["editable"]: !!editMode ["checked"]: props.checked,
} }
)} )}
style={{
"--percentage": `${props.percentage}%`
}}
onClick={onClick}
> >
{ {
editMode && <antd.Input props.checked && <motion.div
placeholder="Option" className="percentage-indicator"
defaultValue={option.label} animate={{ width: `${props.percentage}%` }}
initial={{ width: 0 }}
transition={{ ease: "easeOut" }}
/> />
} }
{ <div className="poll-option-content">
!editMode && <span> {
{option.label} props.checked && createIconRender("FaCheck")
}
{
props.showPercentage && <span>
{Math.floor(props.percentage)}%
</span>
}
<span>
{props.option.label}
</span> </span>
} </div>
{
editMode && <antd.Button
onClick={onRemove}
icon={createIconRender("CloseOutlined")}
size="small"
type="text"
/>
}
</div> </div>
} }
const Poll = (props) => { const Poll = (props) => {
const { editMode, onClose } = props const { editMode, onClose, formRef } = props
const [options, setOptions] = React.useState(props.options ?? []) const [options, setOptions] = React.useState(props.options ?? [])
const [hasVoted, setHasVoted] = React.useState(false)
const [totalVotes, setTotalVotes] = React.useState(0)
async function addOption() { useWsEvents({
setOptions((prev) => { "post.poll.vote": (data) => {
return [ const { post_id, option_id, user_id, previous_option_id } = data
...prev,
{ if (post_id !== props.post_id) {
label: null return false
}
console.debug(`U[${user_id}] vote to option [${option_id}]`)
setOptions((prev) => {
prev = prev.map((option) => {
return option
})
if (user_id === app.userData._id) {
// remove all `voted` properties
prev = prev.map((option) => {
delete option.voted
option.voted = option.id === option_id
return option
})
} }
]
if (previous_option_id) {
const previousOptionIndex = prev.findIndex((option) => option.id === previous_option_id)
if (previousOptionIndex !== -1) {
prev[previousOptionIndex].count = prev[previousOptionIndex].count - 1
}
}
if (option_id) {
const newOptionIndex = prev.findIndex((option) => option.id === option_id)
if (newOptionIndex !== -1) {
prev[newOptionIndex].count += 1
}
}
return prev
})
}
}, {
socketName: "posts"
})
async function onVote(id) {
console.debug(`Voting poll option`, {
option_id: id,
post_id: props.post_id,
})
await PostModel.votePoll({
post_id: props.post_id,
option_id: id,
}) })
} }
async function removeOption(index) { React.useEffect(() => {
setOptions((prev) => { if (options) {
return [ const totalVotes = options.reduce((sum, option) => {
...prev.slice(0, index), return sum + option.count
...prev.slice(index + 1) }, 0)
]
}) setTotalVotes(totalVotes)
}
const hasVoted = options.some((option) => {
return option.voted
})
setHasVoted(hasVoted)
}
}, [options])
return <div className="poll"> return <div className="poll">
{ {
options.map((option, index) => { !editMode && options.map((option, index) => {
const percentage = totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
return <PollOption return <PollOption
key={index} key={index}
option={option} option={option}
editMode={editMode} onClick={onVote}
onRemove={() => { checked={option.voted}
removeOption(index) percentage={percentage}
}} showPercentage={hasVoted}
/> />
}) })
} }
{ {
editMode && <div className="poll-edit-actions"> editMode && <antd.Form
<antd.Button name="post-poll"
onClick={addOption} className="post-poll-edit"
icon={createIconRender("PlusOutlined")} ref={formRef}
initialValues={{
options: options
}}
>
<antd.Form.List
name="options"
> >
Add Option {(fields, { add, remove }) => {
</antd.Button> return <>
{
fields.map((field, index) => {
return <div
key={field.key}
className="post-poll-edit-option"
>
<antd.Form.Item
{...field}
name={[field.name, "label"]}
>
<antd.Input
placeholder="Type a option"
/>
</antd.Form.Item>
{
fields.length > 1 && <antd.Button
onClick={() => remove(field.name)}
icon={createIconRender("MdRemove")}
/>
}
</div>
})
}
<antd.Button
onClick={() => add()}
icon={createIconRender("PlusOutlined")}
>
Add Option
</antd.Button>
</>
}}
</antd.Form.List>
</antd.Form>
}
{
editMode && <div className="poll-edit-actions">
<antd.Button <antd.Button
onClick={onClose} onClick={onClose}
icon={createIconRender("CloseOutlined")} icon={createIconRender("CloseOutlined")}

View File

@ -13,41 +13,6 @@
padding: 10px; padding: 10px;
.poll-option {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 5px;
width: 100%;
transition: all 150ms ease-in-out;
border-radius: 6px;
background-color: var(--background-color-accent);
cursor: pointer;
.ant-input {
background-color: transparent;
width: 100%;
border: 0;
color: var(--text-color);
&::placeholder {
color: var(--text-color);
opacity: 0.5;
}
}
}
.poll-edit-actions { .poll-edit-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -57,4 +22,75 @@
gap: 10px; gap: 10px;
} }
}
.poll-option {
z-index: 100;
position: relative;
width: 100%;
height: auto;
transition: all 150ms ease-in-out;
border-radius: 6px;
background-color: var(--background-color-accent);
cursor: pointer;
overflow: hidden;
// &.checked {
// background-color: rgba(var(--bg_color_4), 0.8);
// }
.percentage-indicator {
z-index: 95;
position: absolute;
background-color: var(--colorPrimary);
opacity: 0.5;
width: var(--percentage);
height: 100%;
border-radius: 0 6px 6px 0;
}
.poll-option-content {
z-index: 100;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 5px;
}
}
.post-poll-edit {
display: flex;
flex-direction: column;
gap: 10px;
.post-poll-edit-option {
display: flex;
flex-direction: row;
gap: 10px;
.ant-form-item {
margin: 0;
padding: 0;
width: 100%;
}
}
} }

View File

@ -3,6 +3,7 @@ import classnames from "classnames"
import Plyr from "plyr-react" import Plyr from "plyr-react"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import Poll from "@components/Poll"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import { processString } from "@utils" import { processString } from "@utils"
@ -212,6 +213,13 @@ export default class PostCard extends React.PureComponent {
flags={this.state.data.flags} flags={this.state.data.flags}
/> />
} }
{
this.state.data.poll_options && <Poll
post_id={this.state.data._id}
options={this.state.data.poll_options}
/>
}
</div> </div>
<PostActions <PostActions

View File

@ -35,6 +35,8 @@ export default class PostCreator extends React.Component {
postingPolicy: DEFAULT_POST_POLICY, postingPolicy: DEFAULT_POST_POLICY,
} }
pollRef = React.createRef()
creatorRef = React.createRef() creatorRef = React.createRef()
cleanPostData = () => { cleanPostData = () => {
@ -105,6 +107,12 @@ export default class PostCreator extends React.Component {
timestamp: DateTime.local().toISO(), timestamp: DateTime.local().toISO(),
} }
if (this.pollRef.current) {
let { options } = this.pollRef.current.getFieldsValue()
payload.poll_options = options.filter((option) => !!option.label)
}
let response = null let response = null
if (this.props.reply_to) { if (this.props.reply_to) {
@ -496,6 +504,7 @@ export default class PostCreator extends React.Component {
status: "done", status: "done",
} }
}), }),
postPoll: post.poll_options
}) })
} }
// fetch the posting policy // fetch the posting policy
@ -567,6 +576,7 @@ export default class PostCreator extends React.Component {
<div className="avatar"> <div className="avatar">
<img src={app.userData?.avatar} /> <img src={app.userData?.avatar} />
</div> </div>
<antd.Input.TextArea <antd.Input.TextArea
placeholder="What are you thinking?" placeholder="What are you thinking?"
value={postMessage} value={postMessage}
@ -578,6 +588,7 @@ export default class PostCreator extends React.Component {
draggable={false} draggable={false}
allowClear allowClear
/> />
<div> <div>
<antd.Button <antd.Button
type="primary" type="primary"
@ -609,6 +620,7 @@ export default class PostCreator extends React.Component {
{ {
this.state.postPoll && <Poll this.state.postPoll && <Poll
formRef={this.pollRef}
options={this.state.postPoll} options={this.state.postPoll}
onClose={this.handleDeletePoll} onClose={this.handleDeletePoll}
editMode editMode