mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
a986804cd1
commit
5ef95ac39a
@ -1,94 +1,215 @@
|
||||
import React from "react"
|
||||
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"
|
||||
|
||||
const PollOption = (props) => {
|
||||
const { option, editMode, onRemove } = props
|
||||
async function onClick() {
|
||||
if (typeof props.onClick === "function") {
|
||||
await props.onClick(props.option.id)
|
||||
}
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classNames(
|
||||
className={classnames(
|
||||
"poll-option",
|
||||
{
|
||||
["editable"]: !!editMode
|
||||
["checked"]: props.checked,
|
||||
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
"--percentage": `${props.percentage}%`
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{
|
||||
editMode && <antd.Input
|
||||
placeholder="Option"
|
||||
defaultValue={option.label}
|
||||
props.checked && <motion.div
|
||||
className="percentage-indicator"
|
||||
animate={{ width: `${props.percentage}%` }}
|
||||
initial={{ width: 0 }}
|
||||
transition={{ ease: "easeOut" }}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!editMode && <span>
|
||||
{option.label}
|
||||
<div className="poll-option-content">
|
||||
{
|
||||
props.checked && createIconRender("FaCheck")
|
||||
}
|
||||
|
||||
{
|
||||
props.showPercentage && <span>
|
||||
{Math.floor(props.percentage)}%
|
||||
</span>
|
||||
}
|
||||
|
||||
<span>
|
||||
{props.option.label}
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
editMode && <antd.Button
|
||||
onClick={onRemove}
|
||||
icon={createIconRender("CloseOutlined")}
|
||||
size="small"
|
||||
type="text"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Poll = (props) => {
|
||||
const { editMode, onClose } = props
|
||||
const { editMode, onClose, formRef } = props
|
||||
|
||||
const [options, setOptions] = React.useState(props.options ?? [])
|
||||
const [hasVoted, setHasVoted] = React.useState(false)
|
||||
const [totalVotes, setTotalVotes] = React.useState(0)
|
||||
|
||||
async function addOption() {
|
||||
setOptions((prev) => {
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
label: null
|
||||
useWsEvents({
|
||||
"post.poll.vote": (data) => {
|
||||
const { post_id, option_id, user_id, previous_option_id } = data
|
||||
|
||||
if (post_id !== props.post_id) {
|
||||
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) {
|
||||
setOptions((prev) => {
|
||||
return [
|
||||
...prev.slice(0, index),
|
||||
...prev.slice(index + 1)
|
||||
]
|
||||
})
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (options) {
|
||||
const totalVotes = options.reduce((sum, option) => {
|
||||
return sum + option.count
|
||||
}, 0)
|
||||
|
||||
setTotalVotes(totalVotes)
|
||||
|
||||
const hasVoted = options.some((option) => {
|
||||
return option.voted
|
||||
})
|
||||
|
||||
setHasVoted(hasVoted)
|
||||
}
|
||||
}, [options])
|
||||
|
||||
return <div className="poll">
|
||||
{
|
||||
options.map((option, index) => {
|
||||
!editMode && options.map((option, index) => {
|
||||
const percentage = totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
|
||||
|
||||
return <PollOption
|
||||
key={index}
|
||||
option={option}
|
||||
editMode={editMode}
|
||||
onRemove={() => {
|
||||
removeOption(index)
|
||||
}}
|
||||
onClick={onVote}
|
||||
checked={option.voted}
|
||||
percentage={percentage}
|
||||
showPercentage={hasVoted}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
editMode && <div className="poll-edit-actions">
|
||||
<antd.Button
|
||||
onClick={addOption}
|
||||
icon={createIconRender("PlusOutlined")}
|
||||
editMode && <antd.Form
|
||||
name="post-poll"
|
||||
className="post-poll-edit"
|
||||
ref={formRef}
|
||||
initialValues={{
|
||||
options: options
|
||||
}}
|
||||
>
|
||||
<antd.Form.List
|
||||
name="options"
|
||||
>
|
||||
Add Option
|
||||
</antd.Button>
|
||||
{(fields, { add, remove }) => {
|
||||
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
|
||||
onClick={onClose}
|
||||
icon={createIconRender("CloseOutlined")}
|
||||
|
@ -13,41 +13,6 @@
|
||||
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -58,3 +23,74 @@
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import classnames from "classnames"
|
||||
import Plyr from "plyr-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
import Poll from "@components/Poll"
|
||||
import { Icons } from "@components/Icons"
|
||||
import { processString } from "@utils"
|
||||
|
||||
@ -212,6 +213,13 @@ export default class PostCard extends React.PureComponent {
|
||||
flags={this.state.data.flags}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
this.state.data.poll_options && <Poll
|
||||
post_id={this.state.data._id}
|
||||
options={this.state.data.poll_options}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<PostActions
|
||||
|
@ -35,6 +35,8 @@ export default class PostCreator extends React.Component {
|
||||
postingPolicy: DEFAULT_POST_POLICY,
|
||||
}
|
||||
|
||||
pollRef = React.createRef()
|
||||
|
||||
creatorRef = React.createRef()
|
||||
|
||||
cleanPostData = () => {
|
||||
@ -105,6 +107,12 @@ export default class PostCreator extends React.Component {
|
||||
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
|
||||
|
||||
if (this.props.reply_to) {
|
||||
@ -496,6 +504,7 @@ export default class PostCreator extends React.Component {
|
||||
status: "done",
|
||||
}
|
||||
}),
|
||||
postPoll: post.poll_options
|
||||
})
|
||||
}
|
||||
// fetch the posting policy
|
||||
@ -567,6 +576,7 @@ export default class PostCreator extends React.Component {
|
||||
<div className="avatar">
|
||||
<img src={app.userData?.avatar} />
|
||||
</div>
|
||||
|
||||
<antd.Input.TextArea
|
||||
placeholder="What are you thinking?"
|
||||
value={postMessage}
|
||||
@ -578,6 +588,7 @@ export default class PostCreator extends React.Component {
|
||||
draggable={false}
|
||||
allowClear
|
||||
/>
|
||||
|
||||
<div>
|
||||
<antd.Button
|
||||
type="primary"
|
||||
@ -609,6 +620,7 @@ export default class PostCreator extends React.Component {
|
||||
|
||||
{
|
||||
this.state.postPoll && <Poll
|
||||
formRef={this.pollRef}
|
||||
options={this.state.postPoll}
|
||||
onClose={this.handleDeletePoll}
|
||||
editMode
|
||||
|
Loading…
x
Reference in New Issue
Block a user