first commit

This commit is contained in:
Ste Vaidis 2023-09-10 21:02:36 +03:00
commit 565e9c9804
34 changed files with 19186 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

0
README.md Normal file
View File

17966
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "poll-play",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"classnames": "^2.3.2",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"socket.io-client": "^4.7.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"tailwindcss": "^3.3.3"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
src/App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

84
src/App.js Normal file
View File

@ -0,0 +1,84 @@
import { useState, useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import io from "socket.io-client"
import { socket } from './socket';
import { ConnectionState } from './ConnectionState';
import { ConnectionManager } from './ConnectionManager';
import Header from './layout/header';
import Footer from './layout/footer';
import Create from './view/Create';
import Poll from './view/Poll';
import Join from './view/Join';
import Home from './view/Home';
function App() {
const [data, setData] = useState();
const [poll, setPoll] = useState();
const [user, setUser] = useState();
const [isConnected, setIsConnected] = useState(socket.connected);
const navigate = useNavigate();
useEffect(() => {
function onConnect() {
console.log('APP onConnect')
setIsConnected(true);
}
function onDisconnect() {
console.log('APP onDisconnect')
setIsConnected(false);
}
function onCreate(data) {
console.log('APP onCreate')
navigate(`/poll/${data}/join`)
}
function onRegister(data) {
console.log('APP onRegister data: ', data)
setUser(data.user)
localStorage.setItem(data.poll, data.token)
navigate(`/poll/${data.poll}`)
}
function onPoll(data) {
console.log('APP onPoll data: ', data)
setPoll(data)
}
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
socket.on('create', onCreate);
socket.on('register', onRegister);
socket.on('poll', onPoll);
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.off('create', onCreate);
socket.off('register', onRegister);
socket.off('poll', onPoll);
};
}, []);
return (
<div className="min-h-screen place-content-center">
<Header />
<div className="mx-4">
<div className="max-w-md mt-8 mx-auto mr-2rounded-xl overflow-hidden md:max-w-2xl bg-black bg-opacity-50 shadow-lg py-6 px-6">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/create" element={<Create socket={socket} io={io} />} />
<Route path="/poll/:id" element={<Poll socket={socket} io={io} user={user} poll={poll} />} />
<Route path="/poll/:id/join" element={<Join socket={socket} />} />
</Routes>
</div>
</div>
{/* <ConnectionState isConnected={isConnected} /> */}
{/* <ConnectionManager /> */}
<Footer />
</div>
)
}
export default App;

8
src/App.test.js Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

19
src/ConnectionManager.js Normal file
View File

@ -0,0 +1,19 @@
import React from 'react';
import { socket } from './socket';
export function ConnectionManager() {
function connect() {
socket.connect();
}
function disconnect() {
socket.disconnect();
}
return (
<>
<button onClick={ connect }>Connect</button>
<button onClick={ disconnect }>Disconnect</button>
</>
);
}

5
src/ConnectionState.js Normal file
View File

@ -0,0 +1,5 @@
import React from 'react';
export function ConnectionState({ isConnected }) {
return <p>Status: { '' + isConnected ? 'Connected' : 'Disconnected' }</p>;
}

196
src/index.css Normal file
View File

@ -0,0 +1,196 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply text-4xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
}
body {
background: linear-gradient(126deg, #0094a1, #b03ab6);
background-size: 400% 400%;
-webkit-animation: AnimationName 20s ease infinite;
-moz-animation: AnimationName 20s ease infinite;
animation: AnimationName 20s ease infinite;
}
@-webkit-keyframes AnimationName {
0%{background-position:10% 0%}
50%{background-position:91% 100%}
100%{background-position:10% 0%}
}
@-moz-keyframes AnimationName {
0%{background-position:10% 0%}
50%{background-position:91% 100%}
100%{background-position:10% 0%}
}
@keyframes AnimationName {
0%{background-position:10% 0%}
50%{background-position:91% 100%}
100%{background-position:10% 0%}
}
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
.gg-copy {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs,1));
width: 14px;
height: 18px;
border: 2px solid;
cursor: pointer;
}
.gg-copy::after,
.gg-copy::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute
}
.gg-copy::before {
background:
linear-gradient( to left,
currentColor 5px, transparent 0)
no-repeat right top/5px 2px,
linear-gradient( to left,
currentColor 5px, transparent 0)
no-repeat left bottom/ 2px 5px;
box-shadow: inset -4px -4px 0 -2px;
bottom: -6px;
right: -6px;
width: 14px;
height: 18px
}
.gg-copy::after {
width: 6px;
height: 2px;
background: currentColor;
left: 2px;
top: 2px;
box-shadow: 0 4px 0,0 8px 0
}
.toggle-bg:after {
content: '';
@apply absolute top-0.5 left-0.5 bg-white border border-gray-300 rounded-full h-5 w-5 transition shadow-sm;
}
input:checked + .toggle-bg:after {
transform: translateX(100%);
@apply border-white;
}
input:checked + .toggle-bg {
@apply bg-blue-600 border-blue-600;
}

20
src/index.js Normal file
View File

@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

7
src/layout/footer.jsx Normal file
View File

@ -0,0 +1,7 @@
const Footer = () => {
return (<div className="mt-16 mb-12 text-center">
Poll v0.1 Made with and React by <a href="mailto:ste.vaidis@gmail.com">ste.vaidis@gmail.com</a>
</div>)
}
export default Footer

24
src/layout/header.jsx Normal file
View File

@ -0,0 +1,24 @@
import { useNavigate } from "react-router-dom";
import { IconSignal, IconPlus } from "../ui/icons";
import { Button } from "../ui/button";
const Header = () => {
const navigate = useNavigate();
return (
<div className="flex flex-grow justify-between bg-black bg-opacity-40 p-2">
<div className="flex flex-row cursor-pointer" onClick={() => navigate('/')}>
<div className="text-2xl font-bold text-white mt-1">Poll</div>
</div>
<div className="m-1">
<Button
label="Create new poll"
icon={<IconPlus />}
onClick={() => navigate('/create')}
/>
</div>
</div>
)
}
export default Header

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

13
src/reportWebVitals.js Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

5
src/socket.jsx Normal file
View File

@ -0,0 +1,5 @@
import { io } from 'socket.io-client';
const URL = 'http://192.168.1.14:4000';
export const socket = io(URL);

37
src/ui/button.jsx Normal file
View File

@ -0,0 +1,37 @@
import classNames from "classnames";
export const Button = ({
label,
onClick,
disabled,
icon,
big
}) => {
const clsCommon = 'flex text-white text-center w-full justify-center rounded-lg';
const clsEnabled = 'bg-gradient-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600 focus:outline-none shadow-md rounded-lg mx-auto';
const clsDisabled = 'text-slate-500 bg-black';
const clsBig = `p-4`
const clsSmall = `py-1 px-3`
const cls = classNames(
clsCommon,
{[clsEnabled]: !disabled},
{[clsDisabled]: disabled},
{[clsBig]: big},
{[clsSmall]: !big}
)
return (
<button
disabled={disabled}
onClick={onClick}
type="button"
className={cls}
>
<div className="flex flex-row align-middle items-center">
<div className="mr-2">{icon}</div>
<div className="">{label}</div>
</div>
</button>
);
};

50
src/ui/checkbox.jsx Normal file
View File

@ -0,0 +1,50 @@
import * as React from "react";
import { useRef } from "react";
const cls = "mr-2";
export const Checkbox = React.forwardRef(
(props, ref) => {
const {
id,
value,
placeholder,
name,
getRef,
label,
onClick,
onChange,
onFocus,
onBlur,
onKeyUp,
onKeyDown,
} = props;
const checkboxRef = useRef(null);
return (
<>
<label
htmlFor={id}
className="flex items-center cursor-pointer relative mb-4"
>
<input
type="checkbox"
id={id}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
ref={checkboxRef}
className="sr-only"
/>
<div className="toggle-bg bg-black h-6 w-11 rounded-full"></div>
<span className="ml-3 text-white text-sm font-medium">
Hide names for anonymous voting
</span>
</label>
</>
);
}
);

125
src/ui/icons.jsx Normal file
View File

@ -0,0 +1,125 @@
export const IconPoll = () => <svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M19 4H5C4.44771 4 4 4.44772 4 5V19C4 19.5523 4.44772 20 5 20H19C19.5523 20 20 19.5523 20 19V5C20 4.44771 19.5523 4 19 4ZM5 2C3.34315 2 2 3.34315 2 5V19C2 20.6569 3.34315 22 5 22H19C20.6569 22 22 20.6569 22 19V5C22 3.34315 20.6569 2 19 2H5Z"
fill="currentColor"
/>
<path d="M11 7H13V17H11V7Z" fill="currentColor" />
<path d="M15 13H17V17H15V13Z" fill="currentColor" />
<path d="M7 10H9V17H7V10Z" fill="currentColor" />
</svg>
export const IconUser2 = () => <svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17C14.2091 17 16 15.2091 16 13H8C8 15.2091 9.79086 17 12 17Z"
fill="currentColor"
/>
<path
d="M10 10C10 10.5523 9.55228 11 9 11C8.44772 11 8 10.5523 8 10C8 9.44772 8.44772 9 9 9C9.55228 9 10 9.44772 10 10Z"
fill="currentColor"
/>
<path
d="M15 11C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9C14.4477 9 14 9.44772 14 10C14 10.5523 14.4477 11 15 11Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z"
fill="currentColor"
/>
</svg>
export const IconUser = () => <svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 12C10 12.5523 9.55228 13 9 13C8.44772 13 8 12.5523 8 12C8 11.4477 8.44772 11 9 11C9.55228 11 10 11.4477 10 12Z"
fill="currentColor"
/>
<path
d="M15 13C15.5523 13 16 12.5523 16 12C16 11.4477 15.5523 11 15 11C14.4477 11 14 11.4477 14 12C14 12.5523 14.4477 13 15 13Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.0244 2.00003L12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.74235 17.9425 2.43237 12.788 2.03059L12.7886 2.0282C12.5329 2.00891 12.278 1.99961 12.0244 2.00003ZM12 20C16.4183 20 20 16.4183 20 12C20 11.3014 19.9105 10.6237 19.7422 9.97775C16.1597 10.2313 12.7359 8.52461 10.7605 5.60246C9.31322 7.07886 7.2982 7.99666 5.06879 8.00253C4.38902 9.17866 4 10.5439 4 12C4 16.4183 7.58172 20 12 20ZM11.9785 4.00003L12.0236 4.00003L12 4L11.9785 4.00003Z"
fill="currentColor"
/>
</svg>
export const IconCopy = () => <svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13 7H7V5H13V7Z" fill="currentColor" />
<path d="M13 11H7V9H13V11Z" fill="currentColor" />
<path d="M7 15H13V13H7V15Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z"
fill="currentColor"
/>
</svg>
export const IconSignal = () => <svg
width="32"
height="32"
viewBox="0 0 18 20"
fill="white"
>
<path
d="M15 7C15 6.44772 15.4477 6 16 6C16.5523 6 17 6.44772 17 7V17C17 17.5523 16.5523 18 16 18C15.4477 18 15 17.5523 15 17V7Z"
fill="white"
/>
<path
d="M7 15C7 14.4477 7.44772 14 8 14C8.55228 14 9 14.4477 9 15V17C9 17.5523 8.55228 18 8 18C7.44772 18 7 17.5523 7 17V15Z"
fill="white"
/>
<path
d="M12 10C11.4477 10 11 10.4477 11 11V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V11C13 10.4477 12.5523 10 12 10Z"
fill="white"
/>
</svg>
export const IconNew = () => <svg
viewBox="0 0 24 24"
height="48"
width="48"
fill="currentColor"
>
<path fill="none" d="M0 0h24v24H0z" />
<path d="M6.455 19L2 22.5V4a1 1 0 011-1h18a1 1 0 011 1v14a1 1 0 01-1 1H6.455zM11 10H8v2h3v3h2v-3h3v-2h-3V7h-2v3z" />
</svg>
export const IconPlus = () => <svg
viewBox="0 0 24 24"
fill="currentColor"
height="1.2em"
width="1.2em"
>
<path d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4z" />
<path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z" />
</svg>

43
src/ui/input.jsx Normal file
View File

@ -0,0 +1,43 @@
import * as React from "react";
import { useRef } from "react";
const cls =
"peer block min-h-[auto] w-full rounded border-1 px-3 py-[0.32rem] leading-[2.15] outline-none transition-all duration-200 ease-linear focus:placeholder:opacity-100 peer-focus:text-primary data-[te-input-state-active]:placeholder:opacity-100 motion-reduce:transition-none dark:text-neutral-200 dark:placeholder:text-neutral-200 dark:peer-focus:text-primary [&:not([data-te-input-placeholder-active])]:placeholder:opacity-1 bg-white bg-opacity-5 text-white";
export const Input = React.forwardRef(
(props, ref) => {
const {
id,
value,
placeholder,
name,
getRef,
onClick,
onChange,
onFocus,
onBlur,
onKeyUp,
onKeyDown,
} = props;
const inputRef = useRef(null);
return (
<>
<div className="relative mb-2 mt-2" data-te-input-wrapper-init>
<input
type="text"
id={id}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
ref={inputRef}
className={cls}
/>
</div>
</>
);
}
);

9
src/ui/title.jsx Normal file
View File

@ -0,0 +1,9 @@
export const Title = ({ label, variant }) => {
const Heading = `h${variant}`;
return (
<Heading className="text-white font-medium leading-tight text-primary inline-block align-middle">
{variant === 1 && <span className="mr-4">&#9733;</span>}
{label}
</Heading>
)};

76
src/view/Answers.jsx Normal file
View File

@ -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">&#10026;</span>}
{user.name}
</div>)
}
})
}
</div>
</div>
))}
</div>
);
}
export default Answers

159
src/view/Create.jsx Normal file
View File

@ -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"
>
&#10005;
</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"
>
&#43; 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

36
src/view/Home.jsx Normal file
View File

@ -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">&#9734;</span>$0 cost.</p>
<p><span className="mr-4">&#9734;</span>No sign up. No personal data.</p>
<p><span className="mr-4">&#9734;</span>Anonymous voting option available!</p>
<p><span className="mr-4">&#9734;</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

49
src/view/Join.jsx Normal file
View File

@ -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

61
src/view/Poll.jsx Normal file
View File

@ -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;

11
tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}