first commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { useState, useEffect, useRef, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
const Answers = (props) => {
|
||||
const { poll, user, socket, id, pid } = props;
|
||||
const [winner, setWinner] = useState(0);
|
||||
const token = localStorage.getItem(id)
|
||||
|
||||
const onVote = async (answer) => {
|
||||
const data = { user:user, answer:answer, pid:pid, token:token }
|
||||
socket.emit("vote", data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
var res = poll.users.reduce(function (obj, v) {
|
||||
obj[v.vote] = (obj[v.vote] || 0) + 1;
|
||||
return obj;
|
||||
}, {})
|
||||
const arr = Object.values(res);
|
||||
const max = Math.max(...arr)
|
||||
setWinner(max)
|
||||
}, [poll])
|
||||
|
||||
const countVotes = (index) => {
|
||||
let count = 0
|
||||
poll.users.filter(function (item) {
|
||||
if (item.vote === index) {
|
||||
count = count + 1
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={`w-2/12 py-2 text-white text-2xl text-center rounded-l-lg ${winner === count ? 'bg-lime-500' : 'bg-black bg-opacity-40'} `}>
|
||||
{count}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const buttonCN = classNames(`w-10/12 text-center bg-white w`)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{poll.answers.map((answer, index) => (
|
||||
<div key={index} className="flex flex-col my-8">
|
||||
<div className="flex flex-row">
|
||||
{countVotes(index)}
|
||||
<button className="w-10/12 p-2 text-center align-middle bg-white rounded-r-lg drop-shadow-lg" onClick={() => onVote(index)} >
|
||||
{answer}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{
|
||||
poll.users.map((user, i) => {
|
||||
if (user.name != props.user && poll.anonymous) {
|
||||
return null
|
||||
}
|
||||
if (index === user.vote) {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={` text-white text-sm float-left py-1 px-2 mr-2 mb-2 rounded-md ${user.name === props.user ? 'bg-blue-500 bg-opacity-100' : 'bg-black bg-opacity-40'}`}
|
||||
>
|
||||
{user.name === props.user && <span className="mr-2">✪</span>}
|
||||
{user.name}
|
||||
</div>)
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Answers
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import { Title } from '../ui/title';
|
||||
import { Input } from '../ui/input';
|
||||
import { Checkbox } from '../ui/checkbox';
|
||||
|
||||
const Create = (props) => {
|
||||
const {socket, io } = props;
|
||||
const navigate = useNavigate();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
answers: ["", ""],
|
||||
anonymous: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
formData.answers.indexOf("") === -1 &&
|
||||
formData.answers.length > 1 &&
|
||||
formData.title
|
||||
) {
|
||||
setDisabled(false);
|
||||
} else {
|
||||
setDisabled(true);
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
const handleInput = (e) => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
[e.target.name]: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAnswers = (e, i) => {
|
||||
const newArr = [...formData.answers];
|
||||
newArr[i] = e.target.value;
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
answers: newArr,
|
||||
}));
|
||||
};
|
||||
|
||||
const addAnswer = (e) => {
|
||||
e.preventDefault();
|
||||
const newArr = [...formData.answers];
|
||||
newArr.push("");
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
answers: newArr,
|
||||
}));
|
||||
};
|
||||
|
||||
const delAnswer = (index) => {
|
||||
const newArr = [...formData.answers];
|
||||
newArr.splice(index, 1);
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
answers: newArr,
|
||||
}));
|
||||
}
|
||||
|
||||
const handleAnonymous = () => {
|
||||
setFormData((prevState) => ({
|
||||
...prevState,
|
||||
anonymous: !prevState.anonymous,
|
||||
}));
|
||||
}
|
||||
|
||||
const handlePublish = () => {
|
||||
const poll = {
|
||||
// id: memoid,
|
||||
title: formData.title,
|
||||
answers: formData.answers,
|
||||
anonymous: formData.anonymous
|
||||
};
|
||||
console.log('CREATE handlePublish poll: ', poll)
|
||||
socket.emit("create", poll);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<div className="mb-8">
|
||||
<Title variant={1} label="Create Poll" />
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<Title variant={2} label="Question" />
|
||||
<Input
|
||||
name="title"
|
||||
getRef={(ref) => handleInput(ref.current)}
|
||||
placeholder="Title"
|
||||
value={formData.title}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<Title variant={2} label="Answers" />
|
||||
{formData.answers.map((option, i) => (
|
||||
<div key={i} className="flex">
|
||||
<div className="w-11/12">
|
||||
<Input
|
||||
id={option}
|
||||
name={option}
|
||||
getRef={(ref) => handleInput(ref.current)}
|
||||
value={formData.answers[i]}
|
||||
onChange={(e) => handleAnswers(e, i)}
|
||||
/>
|
||||
</div>
|
||||
{i > 1 && <div className="w-1/12 flex">
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => delAnswer(i)}
|
||||
className="text-red-500 text-2xl leading-10 font-extrabold ml-4"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-right pr-16">
|
||||
<button
|
||||
disabled={formData.answers.length > 100 ? true : false}
|
||||
onClick={(e) => addAnswer(e)}
|
||||
className="text-green-400"
|
||||
>
|
||||
+ Add answer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 ml-4 w-full">
|
||||
<Checkbox
|
||||
id='anon'
|
||||
label="Don't show voters names"
|
||||
onChange={handleAnonymous}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
label={disabled ? 'Fill the form to publish the poll' : 'Publish the poll'}
|
||||
onClick={handlePublish}
|
||||
big={true}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Create
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Title } from "../ui/title";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconPlus } from "../ui/icons";
|
||||
|
||||
const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Title variant={1} label="Lets make a poll real fast!" />
|
||||
<div className="text-white my-8">
|
||||
<p><span className="mr-4">☆</span>$0 cost.</p>
|
||||
<p><span className="mr-4">☆</span>No sign up. No personal data.</p>
|
||||
<p><span className="mr-4">☆</span>Anonymous voting option available!</p>
|
||||
<p><span className="mr-4">☆</span>Blazing fast poll creation with only 3 quick steps:</p>
|
||||
<ul className="list-decimal my-8 pl-12">
|
||||
<li className="pl-2">Create the poll with only title and questions.</li>
|
||||
<li className="pl-2">Join the poll with a nickname.</li>
|
||||
<li className="pl-2">Share the poll url to votes.</li>
|
||||
</ul>
|
||||
<p>Surprice the voters, its free!</p>
|
||||
</div>
|
||||
<div className="w-15">
|
||||
<Button
|
||||
className="p-15"
|
||||
label="Create new poll"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => navigate('/create')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { Title } from "../ui/title";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
const URL ="http://192.168.1.14:4001"
|
||||
|
||||
const Join = (props) => {
|
||||
const {socket} = props
|
||||
const [user, setUser] = useState('');
|
||||
const { id } = useParams();
|
||||
|
||||
const handleInput = (e) => {
|
||||
setUser(e.target.value);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const data = {poll:id, user:user};
|
||||
socket.emit('join', data); // triggers onRegister
|
||||
console.log('JOIN submit data: ', data)
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="">
|
||||
<Title variant={2} label="Nickname:" />
|
||||
<Input
|
||||
name="title"
|
||||
getRef={(ref) => handleInput(ref.current)}
|
||||
placeholder="Choose a name to vote...."
|
||||
value={user}
|
||||
onChange={handleInput}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-8'>
|
||||
<Button
|
||||
disabled={!user}
|
||||
label="Let's vote!"
|
||||
onClick={submit}
|
||||
big
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Join
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
|
||||
import { Title } from "../ui/title";
|
||||
import Answers from './Answers'
|
||||
|
||||
function Poll(props) {
|
||||
const { user, poll, socket } = props
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const token = localStorage.getItem(id)
|
||||
const [copied, setCopied] = useState(false);
|
||||
console.log('Poll props:', props)
|
||||
const FRONT_URL = "http://192.168.1.14:3000"
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return navigate(`/poll/${id}/join`)
|
||||
}
|
||||
socket.emit('poll', token);
|
||||
console.log('Poll token:', token)
|
||||
}, [id, token, navigate]);
|
||||
|
||||
const onCopy = () => {
|
||||
console.log('onCopy')
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="poll">
|
||||
{poll &&
|
||||
<div className="mb-8">
|
||||
<Title variant={1} label={poll.title} />
|
||||
<p className="text-white text-sm">Hello <strong>{user}</strong>, please choose the answer you like</p>
|
||||
<Answers poll={poll} id={id} user={user} socket={socket} />
|
||||
</div>
|
||||
}
|
||||
{/* SHARE */}
|
||||
<div className="mt-2 text-white">
|
||||
<p className="text-sm mb-3">Share the poll URL to the voters</p>
|
||||
<div className="flex p-4 bg-black bg-opacity-20 rounded-xl justify-between">
|
||||
<div className="text-sm">
|
||||
{FRONT_URL}/poll/{id}
|
||||
</div>
|
||||
<div className="">
|
||||
<CopyToClipboard onCopy={onCopy} text={`${URL}/poll/${id}`}>
|
||||
<i className="flex mr-2 gg-copy"></i>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`transition-opacity duration-600 flex justify-end text-sm mr-2 ${copied ? 'opacity-100' : 'opacity-0'}`}>
|
||||
Copied
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Poll;
|
||||
Reference in New Issue
Block a user