mySQL
This commit is contained in:
94
src/App.jsx
94
src/App.jsx
@@ -5,11 +5,13 @@ import {
|
||||
Route,
|
||||
Navigate,
|
||||
} from 'react-router-dom';
|
||||
import Lobby from './components/Lobby/Lobby';
|
||||
import VotingTable from './components/VotingTable/VotingTable';
|
||||
import UserSessionListener from './components/UserSessionListener/UserSessionListener';
|
||||
import { signInAnonymously, onAuthStateChanged } from 'firebase/auth';
|
||||
import { auth } from './services/firebase';
|
||||
import Lobby from './components/Lobby';
|
||||
import VotingTable from './components/VotingTable';
|
||||
import UserSessionListener from './components/UserSessionListener';
|
||||
import axios from 'axios';
|
||||
import cyarenLogo from './assets/images/CyarenLogo.png';
|
||||
import cardLogo from './assets/images/logo.png';
|
||||
import styles from './App.module.css';
|
||||
|
||||
const App = () => {
|
||||
const [userId, setUserId] = useState('');
|
||||
@@ -21,37 +23,67 @@ const App = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Sign in the user anonymously
|
||||
signInAnonymously(auth).catch((error) => {
|
||||
console.error('Anonymous sign-in error:', error);
|
||||
});
|
||||
|
||||
// Handle user state changes
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
if (user) {
|
||||
console.log('User is signed in:', user);
|
||||
} else {
|
||||
console.log('User is signed out');
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
// Check if there's a user session by making a request to the backend
|
||||
axios
|
||||
.get('/api/auth/session')
|
||||
.then((response) => {
|
||||
const { userId } = response.data;
|
||||
if (userId) {
|
||||
setUserId(userId); // User is logged in
|
||||
} else {
|
||||
// If no user session exists, create an anonymous user session
|
||||
axios
|
||||
.post('/api/auth/signin-anonymous')
|
||||
.then((response) => {
|
||||
setUserId(response.data.userId);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error signing in anonymously:', error);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking user session:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<UserSessionListener />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/lobby/:sessionId?"
|
||||
element={<Lobby onJoinSession={handleJoinSession} />}
|
||||
loading={loading}
|
||||
/>
|
||||
<Route
|
||||
path="/session/:sessionId"
|
||||
element={<VotingTable userId={userId} />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to={`/lobby`} />} />
|
||||
</Routes>
|
||||
<div className={styles.app}>
|
||||
<section className={styles.header}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/lobby/:sessionId?"
|
||||
element={<Lobby onJoinSession={handleJoinSession} />}
|
||||
loading={loading}
|
||||
/>
|
||||
<Route
|
||||
path="/session/:sessionId"
|
||||
element={<VotingTable userId={userId} />}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to={`/lobby`} />} />
|
||||
</Routes>
|
||||
</section>
|
||||
<section className={styles.footer}>
|
||||
<div className={styles.logoEnvelope}>
|
||||
<img className={styles.cyaren} src={cyarenLogo} alt="cyaren-logo" />
|
||||
</div>
|
||||
<img
|
||||
className={`${styles.cardLogo} ${styles.moveLeft}`}
|
||||
src={cardLogo}
|
||||
alt="card-logo"
|
||||
/>
|
||||
<img
|
||||
className={`${styles.cardLogo} ${styles.moveRight}`}
|
||||
src={cardLogo}
|
||||
alt="card-logo"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
69
src/App.module.css
Normal file
69
src/App.module.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.app {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app section.header {
|
||||
flex: 1 auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app section.footer {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding-bottom: 20px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.app section.footer .logoEnvelope {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
width 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app section.footer:hover .logoEnvelope {
|
||||
width: 140px;
|
||||
opacity: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cyaren {
|
||||
/* width: calc(10%); */
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.cardLogo {
|
||||
width: calc(2%);
|
||||
min-width: 30px;
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
transition:
|
||||
margin 0.2s ease-in-out,
|
||||
opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.app section.footer:hover .moveLeft {
|
||||
margin-right: 120px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app section.footer:hover .moveRight {
|
||||
margin-left: 120px;
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'blackmild-regular';
|
||||
src: url('/src/assets/fonts/Blackmild\ Regular/Blackmild\ Regular.otf')
|
||||
format('opentype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'cardo-regular';
|
||||
src: url('/src/assets/fonts/Cardo/Cardo-Regular.ttf') format('opentype');
|
||||
src: url('/src/assets/fonts/Cardo/Cardo-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -16,7 +8,7 @@
|
||||
@font-face {
|
||||
font-family: 'cinzel';
|
||||
src: url('/src/assets/fonts/Cinzel/Cinzel-VariableFont_wght.ttf')
|
||||
format('opentype');
|
||||
format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
BIN
src/assets/images/CyarenLogo.png
Normal file
BIN
src/assets/images/CyarenLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
9
src/assets/images/bracket.svg
Normal file
9
src/assets/images/bracket.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="119" height="30" viewBox="0 0 119 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 21.5C9.13162 21.6574 8.56984 21.6774 8.56984 21.6774C8.56984 21.6774 10.8859 17.7503 14 22C16 25.5 11.0613 29.7046 6.70152 28.9176C-4.5 26.5 -0.12039 14.287 10 13.5C16.8508 13.3426 31.9248 23.4088 40.1768 23.7236C48.4287 24.0383 49.2074 18.5295 49.2074 17.4277C49.5 13 42.0001 11.2225 42 15C42.1558 17.3609 44.3807 17.8999 44.3807 17.8999C41.4224 19.3165 39.3983 16.3259 40.7995 13.0206C42.2008 9.7153 50.92 11.4467 50.92 16.3259C50.92 21.2052 49 25.5 40 26C31 26.5 20.403 17.1129 10.4382 16.0111C1.87485 15.2242 0 27 6.85715 27.5011C13.7143 28.0021 13.9583 21.5 11 21.5Z" fill="currentColor"/>
|
||||
<path d="M38.3105 17.6692C38.3105 17.6692 34.7779 15.843 34.0833 12.5105C33.772 11.0171 35.3114 9.60623 36.3302 11.4808C37.1536 13.4492 36.2118 14.4557 38.3105 17.6692Z" fill="currentColor"/>
|
||||
<path d="M35.4119 19.3831C35.4119 19.3831 31.4572 19.4494 29.2987 16.8551C28.3315 15.6925 29.0298 13.7285 30.7964 14.8914C32.4347 16.2287 32.0739 17.5559 35.4119 19.3831Z" fill="currentColor"/>
|
||||
<path d="M108 21.5C109.868 21.6574 110.43 21.6774 110.43 21.6774C110.43 21.6774 108.114 17.7503 105 22C103 25.5 107.939 29.7046 112.298 28.9176C123.5 26.5 119.12 14.287 109 13.5C102.149 13.3426 87.0752 23.4088 78.8232 23.7236C70.5713 24.0383 69.7926 18.5295 69.7926 17.4277C69.5 13 76.9999 11.2225 77 15C76.8442 17.3609 74.6193 17.8999 74.6193 17.8999C77.5776 19.3165 79.6017 16.3259 78.2005 13.0206C76.7992 9.7153 68.08 11.4467 68.08 16.3259C68.08 21.2052 70 25.5 79 26C88 26.5 98.597 17.1129 108.562 16.0111C117.125 15.2242 119 27 112.143 27.5011C105.286 28.0021 105.042 21.5 108 21.5Z" fill="currentColor"/>
|
||||
<path d="M80.6895 17.6692C80.6895 17.6692 84.2221 15.843 84.9167 12.5105C85.228 11.0171 83.6886 9.60623 82.6698 11.4808C81.8464 13.4492 82.7882 14.4557 80.6895 17.6692Z" fill="currentColor"/>
|
||||
<path d="M83.5881 19.3831C83.5881 19.3831 87.5428 19.4494 89.7013 16.8551C90.6685 15.6925 89.9702 13.7285 88.2036 14.8914C86.5653 16.2287 86.9261 17.5559 83.5881 19.3831Z" fill="currentColor"/>
|
||||
<path d="M65.5 18H54C54 18 57.8682 16.7591 58.9422 10.1231C49.8164 17.5555 47.4006 2.4253 57.8685 6.93779C53.037 -2.61808 67.5307 -2.08719 61.6261 7.20324C72.3622 2.15986 70.7518 18.3518 60.821 10.1231C61.6258 16.4937 65.5 18 65.5 18Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
Binary file not shown.
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
BIN
src/assets/images/smalllogo.png
Normal file
BIN
src/assets/images/smalllogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
src/assets/images/titlecard-mini.png
Normal file
BIN
src/assets/images/titlecard-mini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 KiB |
Binary file not shown.
29
src/components/BoardCard copy/index.jsx
Normal file
29
src/components/BoardCard copy/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import VoteValue from '../VoteValue';
|
||||
import cardBack from '../../assets/images/cardback.png';
|
||||
import cardFront from '../../assets/images/cardfront.png';
|
||||
import styles from './BoardCard.module.css';
|
||||
|
||||
const BoardCard = ({ vote, isDimmed, isRevealed = true }) => {
|
||||
const [isFlipped, setIsFlipped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRevealed) {
|
||||
setTimeout(() => setIsFlipped(true), 300);
|
||||
} else if (isFlipped) {
|
||||
setIsFlipped(false);
|
||||
}
|
||||
}, [isFlipped, isRevealed]);
|
||||
|
||||
return (
|
||||
<div className={`${isDimmed ? styles.isDimmed : ''} ${styles.boardCard}`}>
|
||||
<div className={`${styles.card} ${isFlipped ? styles.flip : ''}`}>
|
||||
<img className={styles.cardBack} src={cardBack} alt="card-back" />
|
||||
<img className={styles.cardFront} src={cardFront} alt="card-front" />
|
||||
<VoteValue className={styles.vote} value={vote} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardCard;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import cardBack from '../../assets/images/cardback.png';
|
||||
import { ReactComponent as BracketIcon } from '../../assets/images/bracket.svg';
|
||||
import FrontCard from '../FrontCard/FrontCard';
|
||||
import FrontCard from '../FrontCard';
|
||||
import styles from './BoardCard.module.css';
|
||||
|
||||
const BoardCard = ({ username, vote, isRevealed, isDimmed, isHighlighted }) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import card from '../../assets/images/card.png';
|
||||
import VoteValue from '../VoteValue/VoteValue';
|
||||
import VoteValue from '../VoteValue';
|
||||
import styles from './FrontCard.module.css';
|
||||
|
||||
const FrontCard = ({ value, className }) => {
|
||||
14
src/components/LoadingRoute/index.jsx
Normal file
14
src/components/LoadingRoute/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
const LoadingRoute = ({ loading = false, element, ...rest }) => {
|
||||
debugger;
|
||||
console.log('this.type', this.type);
|
||||
return <Route {...rest} element={!loading ? element : <Fragment />} />;
|
||||
};
|
||||
|
||||
// const LoadingRoute = Route;
|
||||
|
||||
// console.log('LoadingRoute', LoadingRoute);
|
||||
|
||||
export default LoadingRoute;
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ref, onValue, set } from 'firebase/database';
|
||||
import { realtimeDb } from '../../services/firebase';
|
||||
import { generateUniqueId } from '../../utils/crypto';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import appLogo from '../../assets/images/applogo.png';
|
||||
import styles from './Lobby.module.css';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { auth } from '../../services/firebase';
|
||||
import api from '../../services/api';
|
||||
import { generateUniqueId } from '../../shared/utils/crypto';
|
||||
|
||||
const Lobby = ({ onJoinSession = () => {} }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -18,60 +16,40 @@ const Lobby = ({ onJoinSession = () => {} }) => {
|
||||
// Get or generate the session ID
|
||||
if (!sessionId) {
|
||||
const newSessionId = generateUniqueId();
|
||||
// Update the URL with the session ID if it wasn't already there
|
||||
navigate(`/lobby/${newSessionId}`);
|
||||
} else {
|
||||
// Fetch users in the session
|
||||
api
|
||||
.get(`/api/session/${sessionId}/users`)
|
||||
.then((response) => {
|
||||
setUsers(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching users:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}/users`);
|
||||
|
||||
// Listen for changes to the session users data
|
||||
const unsubscribe = onValue(sessionRef, (snapshot) => {
|
||||
const users = snapshot.val();
|
||||
if (!users) return;
|
||||
|
||||
const userList = Object.entries(users).map(([uid, userInfo]) => ({
|
||||
uid,
|
||||
username: userInfo.username,
|
||||
}));
|
||||
setUsers(userList);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [navigate, sessionId]);
|
||||
|
||||
const handleJoinSession = async () => {
|
||||
const user = auth.currentUser;
|
||||
const userId = user ? user.uid : generateUniqueId();
|
||||
const userId = generateUniqueId(); // Generate unique ID for user
|
||||
|
||||
// Check if the uid is already part of the session
|
||||
if (users.includes(userId)) {
|
||||
// Check if the username is already taken
|
||||
if (users.some((user) => user.username === username)) {
|
||||
setIsUsernameTaken(true);
|
||||
return;
|
||||
} else {
|
||||
// Set the initial vote for the user
|
||||
const voteRef = ref(realtimeDb, `sessions/${sessionId}/votes/${userId}`);
|
||||
await set(voteRef, -1);
|
||||
}
|
||||
|
||||
// Add the user to the session's user list with their uid and username
|
||||
const userRef = ref(realtimeDb, `sessions/${sessionId}/users/${userId}`);
|
||||
await set(userRef, {
|
||||
username: username,
|
||||
uid: userId,
|
||||
});
|
||||
|
||||
// Also save the current session ID under the user's record in the database
|
||||
const userSessionRef = ref(realtimeDb, `users/${userId}`);
|
||||
await set(userSessionRef, {
|
||||
currentSessionId: `session/${sessionId}`,
|
||||
});
|
||||
// Add the user to the session
|
||||
try {
|
||||
await api.post(`/api/session/${sessionId}/user`, { userId, username });
|
||||
|
||||
// Call the onJoinSession callback with the username
|
||||
onJoinSession(userId);
|
||||
|
||||
// Navigate to the session
|
||||
navigate(`/session/${sessionId}`);
|
||||
} catch (error) {
|
||||
console.error('Error joining session:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,8 +81,8 @@ const Lobby = ({ onJoinSession = () => {} }) => {
|
||||
<br />
|
||||
<h3>Current Users In Session</h3>
|
||||
<section className={styles.list}>
|
||||
{users.map(({ uid, username }) => (
|
||||
<section key={uid}>{username}</section>
|
||||
{users.map(({ id, username }) => (
|
||||
<section key={id}>{username}</section>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
.root {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 35px;
|
||||
}
|
||||
|
||||
/* Container for the entire menu */
|
||||
.menuContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Label with circular border */
|
||||
.menuLabel {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid #f0dcb1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Container for the buttons */
|
||||
.menuButtons {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: none;
|
||||
background-color: transparent;
|
||||
padding: 4px 0;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* Buttons styling */
|
||||
.menuButtons button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
background-color: #f0dcb1;
|
||||
border: 1px solid #1b191a;
|
||||
border-radius: 4px;
|
||||
color: #1b191a;
|
||||
padding: 4px 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
user-select: none;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.menuButtons button:hover {
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
|
||||
background-color: #cdba95;
|
||||
}
|
||||
|
||||
.menuButtons button:active {
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
|
||||
background-color: #f0dcb1;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Show buttons on hover */
|
||||
.menuContainer:hover .menuButtons {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menuContainer .invite {
|
||||
}
|
||||
|
||||
.menuContainer .invite .checkmark {
|
||||
font-weight: 800;
|
||||
}
|
||||
49
src/components/ProfileMenuButton/index.jsx
Normal file
49
src/components/ProfileMenuButton/index.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
import styles from './ProfileMenuButton.module.css';
|
||||
import { getAcronym } from '../../utils/strings';
|
||||
|
||||
const ProfileMenuButton = ({ username, className, onLogout, onInvite }) => {
|
||||
const [isInviteCopied, setIsInviteCopied] = useState(false);
|
||||
const acronym = getAcronym(username).slice(0, 2);
|
||||
const timeoutIdRef = useRef(null);
|
||||
|
||||
async function handleInvite() {
|
||||
try {
|
||||
await onInvite();
|
||||
setIsInviteCopied(true);
|
||||
timeoutIdRef.current = setTimeout(() => {
|
||||
setIsInviteCopied(false);
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup the timeout if the component unmounts before the timeout is complete
|
||||
return () => clearTimeout(timeoutIdRef.current);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${styles.root} ${className}`}>
|
||||
<div className={styles.menuContainer}>
|
||||
<div className={styles.menuLabel}>{acronym}</div>
|
||||
<div className={styles.menuButtons}>
|
||||
<button onClick={handleInvite} className={styles.invite}>
|
||||
{!isInviteCopied ? (
|
||||
'Copy Invite'
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.checkmark}>✓ </span>Copied!
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onLogout}>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileMenuButton;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './Results.module.css';
|
||||
import VoteValue from '../VoteValue/VoteValue';
|
||||
import VoteValue from '../VoteValue';
|
||||
|
||||
const Results = ({ votes }) => {
|
||||
const voteCounts = Object.values(votes).reduce((acc, vote) => {
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ref, onValue, remove } from 'firebase/database';
|
||||
import { realtimeDb } from '../../services/firebase';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { generateUniqueId } from '../../utils/crypto';
|
||||
import { auth } from '../../services/firebase';
|
||||
|
||||
const UserSessionListener = () => {
|
||||
const navigate = useNavigate();
|
||||
const user = auth.currentUser;
|
||||
const userId = user ? user.uid : generateUniqueId();
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const userRef = ref(realtimeDb, `users/${userId}/currentSessionId`);
|
||||
|
||||
const unsubscribe = onValue(userRef, async (snapshot) => {
|
||||
const currentSessionId = snapshot.val() || generateUniqueId();
|
||||
|
||||
// Only redirect if the user is not already being redirected
|
||||
if (
|
||||
!isRedirecting &&
|
||||
currentSessionId &&
|
||||
currentSessionId.startsWith('lobby')
|
||||
) {
|
||||
setIsRedirecting(true);
|
||||
|
||||
console.log(`Removing user from: users/${userId}`);
|
||||
|
||||
// Remove the entire user entry from the database
|
||||
const fullUserRef = ref(realtimeDb, `users/${userId}`);
|
||||
await remove(fullUserRef);
|
||||
|
||||
navigate(`/${currentSessionId}`);
|
||||
|
||||
setIsRedirecting(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [userId, navigate, isRedirecting]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UserSessionListener;
|
||||
48
src/components/UserSessionListener/index.jsx
Normal file
48
src/components/UserSessionListener/index.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import api from '../../services/api'; // Import the Axios instance with the interceptor
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { generateUniqueId } from '../../shared/utils/crypto';
|
||||
|
||||
const UserSessionListener = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
const userId = generateUniqueId(); // Use the unique ID generator for anonymous users
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserSession = async () => {
|
||||
try {
|
||||
// Get the user's current session ID from the API
|
||||
const response = await api.get(`/users/${userId}/session`);
|
||||
|
||||
const currentSessionId =
|
||||
response.data.currentSessionId || generateUniqueId();
|
||||
|
||||
// Only redirect if the user is not already being redirected
|
||||
if (
|
||||
!isRedirecting &&
|
||||
currentSessionId &&
|
||||
currentSessionId.startsWith('lobby')
|
||||
) {
|
||||
setIsRedirecting(true);
|
||||
|
||||
// Delete the user from the session via API
|
||||
await api.delete(`/users/${userId}`);
|
||||
|
||||
// Redirect to the session
|
||||
navigate(`/${currentSessionId}`);
|
||||
|
||||
setIsRedirecting(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user session:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserSession();
|
||||
}, [userId, navigate, isRedirecting]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UserSessionListener;
|
||||
@@ -1,3 +1,9 @@
|
||||
button.voteCard {
|
||||
font-family: 'Cardo', serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.voteCard {
|
||||
width: 50px;
|
||||
height: 75px;
|
||||
@@ -31,12 +37,6 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cardo-regular {
|
||||
font-family: 'Cardo', serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './VoteCard.module.css';
|
||||
import FrontCard from '../FrontCard/FrontCard';
|
||||
import FrontCard from '../FrontCard';
|
||||
|
||||
const VoteCard = ({ number, isSelected, onClick, className }) => {
|
||||
return (
|
||||
<button
|
||||
className={`${styles.voteCard} ${isSelected ? styles.selected : ''} ${styles['cardo-regular']} ${className}`}
|
||||
className={`${styles.voteCard} ${isSelected ? styles.selected : ''} ${className}`}
|
||||
onClick={() => onClick(number)}
|
||||
>
|
||||
<FrontCard
|
||||
@@ -2,29 +2,27 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%; /* or specify a specific width */
|
||||
height: 100%; /* or specify a specific height */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.coffee,
|
||||
.infinity {
|
||||
width: 100%; /* Adjust this to the specific size you want */
|
||||
height: auto; /* Maintains aspect ratio */
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 2rem; /* Adjust this to fit your design */
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
width: 100%; /* Makes the text conform to the container width */
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
/* text-shadow: none; */
|
||||
/* color: #1b191a; */
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { ReactComponent as Coffee } from '../../assets/images/coffee.svg';
|
||||
import { ReactComponent as Infinity } from '../../assets/images/infinity.svg';
|
||||
import styles from './VoteValue.module.css';
|
||||
|
||||
const VoteValue = ({ value, className }) => {
|
||||
function VoteValue({ value, className }) {
|
||||
const isInfinity = `${value}` === '9999';
|
||||
const isCoffee = `${value}` === '0';
|
||||
const isNoVote = `${value}` === '';
|
||||
|
||||
return (
|
||||
<div className={`${styles.voteValueContainer} ${className}`}>
|
||||
@@ -13,11 +14,13 @@ const VoteValue = ({ value, className }) => {
|
||||
<Coffee className={styles.coffee} />
|
||||
) : isInfinity ? (
|
||||
<Infinity className={styles.infinity} />
|
||||
) : isNoVote ? (
|
||||
<div className={styles.value}>?</div>
|
||||
) : (
|
||||
<div className={styles.value}>{value >= 0 ? value : ''}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default VoteValue;
|
||||
@@ -1,65 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ref, set, get } from 'firebase/database';
|
||||
import { realtimeDb } from '../../services/firebase';
|
||||
import VoteCard from '../VoteCard/VoteCard';
|
||||
import styles from './VotingBoard.module.css';
|
||||
|
||||
const fibonacciNumbers = [0, 1, 2, 3, 5, 8, 13, 21, 9999];
|
||||
|
||||
const VotingBoard = ({ sessionId, userId }) => {
|
||||
const [selectedVote, setSelectedVote] = useState(null);
|
||||
|
||||
const handleVote = async (number) => {
|
||||
setSelectedVote(number);
|
||||
|
||||
const userVoteRef = ref(
|
||||
realtimeDb,
|
||||
`sessions/${sessionId}/votes/${userId}`
|
||||
);
|
||||
|
||||
// Get the current vote for this user (if any)
|
||||
const snapshot = await get(userVoteRef);
|
||||
const existingVote =
|
||||
snapshot.exists() && snapshot.val() > -1 ? snapshot.val() : null;
|
||||
|
||||
// Set the new vote for the user
|
||||
await set(userVoteRef, number);
|
||||
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}`);
|
||||
const sessionSnapshot = await get(sessionRef);
|
||||
|
||||
if (sessionSnapshot.exists()) {
|
||||
const sessionData = sessionSnapshot.val() || {};
|
||||
|
||||
// Increment votedUsers only if the user hasn't voted before
|
||||
if (existingVote === null) {
|
||||
const votedUsers = sessionData.votedUsers || 0;
|
||||
await set(sessionRef, {
|
||||
...sessionData,
|
||||
votedUsers: votedUsers + 1,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error("Session doesn't exist");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.votingBoard}>
|
||||
<h2>Select Your Vote</h2>
|
||||
<div className={styles.voteCards}>
|
||||
{fibonacciNumbers.map((num) => (
|
||||
<VoteCard
|
||||
key={num}
|
||||
className={styles.voteCard}
|
||||
number={num}
|
||||
onClick={() => handleVote(num)}
|
||||
isSelected={selectedVote === num}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VotingBoard;
|
||||
45
src/components/VotingBoard/index.jsx
Normal file
45
src/components/VotingBoard/index.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import api from '../../services/api'; // Import your Axios instance from `api.js`
|
||||
import VoteCard from '../VoteCard';
|
||||
import styles from './VotingBoard.module.css';
|
||||
|
||||
const fibonacciNumbers = ['', 0, 1, 2, 3, 5, 8, 13, 21, 9999];
|
||||
|
||||
const VotingBoard = ({ sessionId, userId }) => {
|
||||
const [selectedVote, setSelectedVote] = useState(null);
|
||||
|
||||
const handleVote = async (number) => {
|
||||
setSelectedVote(number);
|
||||
|
||||
try {
|
||||
// Send the vote to your server via POST request using the `api.js` instance
|
||||
await api.post(`/session/${sessionId}/votes`, {
|
||||
userId,
|
||||
vote: number,
|
||||
});
|
||||
|
||||
console.log('Vote submitted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error submitting vote:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.votingBoard}>
|
||||
<h2>Select Your Vote</h2>
|
||||
<div className={styles.voteCards}>
|
||||
{fibonacciNumbers.map((num) => (
|
||||
<VoteCard
|
||||
key={num}
|
||||
className={styles.voteCard}
|
||||
number={num}
|
||||
onClick={() => handleVote(num)}
|
||||
isSelected={selectedVote === num}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VotingBoard;
|
||||
@@ -1,294 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { ref, onValue, get, set, remove } from 'firebase/database';
|
||||
import { realtimeDb } from '../../services/firebase';
|
||||
import VotingBoard from '../VotingBoard/VotingBoard';
|
||||
import BoardCard from '../BoardCard/BoardCard';
|
||||
import Results from '../Results/Results';
|
||||
import styles from './VotingTable.module.css';
|
||||
import titleCardSrc from '../../assets/images/titlecard.png';
|
||||
import { ReactComponent as ExitIcon } from '../../assets/images/exit.svg';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { auth } from '../../services/firebase';
|
||||
import ProfileMenuButton from '../ProfileMenuButton';
|
||||
|
||||
const VotingTable = ({ userId }) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [votes, setVotes] = useState({});
|
||||
const [, setVotedUsers] = useState(0);
|
||||
const [allVoted, setAllVoted] = useState(false);
|
||||
const [isFinal, setIsFinal] = useState(false);
|
||||
const { sessionId } = useParams();
|
||||
const [currentUID, setCurrentUID] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const maxUsers = 32;
|
||||
const username =
|
||||
users.find((user) => user.uid === currentUID)?.username || '';
|
||||
|
||||
const goToLobby = useCallback(() => {
|
||||
navigate(`/lobby/${sessionId}`);
|
||||
}, [sessionId, navigate]);
|
||||
|
||||
function handleLogout() {
|
||||
handleDropUser(currentUID);
|
||||
}
|
||||
|
||||
const handleDropUser = async (userId) => {
|
||||
const userVoteRef = ref(
|
||||
realtimeDb,
|
||||
`sessions/${sessionId}/votes/${userId}`
|
||||
);
|
||||
const userRef = ref(realtimeDb, `sessions/${sessionId}/users/${userId}`);
|
||||
|
||||
await remove(userVoteRef);
|
||||
await remove(userRef);
|
||||
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}`);
|
||||
const snapshot = await get(sessionRef);
|
||||
|
||||
if (snapshot.exists()) {
|
||||
const sessionData = snapshot.val() || {};
|
||||
|
||||
if (typeof sessionData.users === 'undefined') {
|
||||
sessionData.users = {};
|
||||
}
|
||||
if (typeof sessionData.votes === 'undefined') {
|
||||
sessionData.votes = {};
|
||||
}
|
||||
|
||||
const userList = Object.entries(sessionData.users).reduce(
|
||||
(res, [uid, userInfo]) => {
|
||||
res[uid] = {
|
||||
uid,
|
||||
username: userInfo.username,
|
||||
};
|
||||
return res;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (sessionData.votes && sessionData.votes[userId]) {
|
||||
delete sessionData.votes[userId];
|
||||
}
|
||||
|
||||
// Update the session data
|
||||
await set(sessionRef, {
|
||||
...sessionData,
|
||||
votedUsers: Object.entries(sessionData.votes).reduce(
|
||||
(res, [uid, vote]) => (userId !== uid && vote >= 0 ? res + 1 : res),
|
||||
0
|
||||
),
|
||||
users: userList,
|
||||
});
|
||||
|
||||
// Redirect the dropped user to the lobby
|
||||
const userLobbyRef = ref(realtimeDb, `users/${userId}`);
|
||||
await set(userLobbyRef, {
|
||||
currentSessionId: `lobby/${sessionId}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalizeVoting = async () => {
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}`);
|
||||
const snapshot = await get(sessionRef);
|
||||
if (snapshot.exists()) {
|
||||
const sessionData = snapshot.val() || {};
|
||||
|
||||
await set(sessionRef, {
|
||||
...sessionData,
|
||||
isFinal: true,
|
||||
});
|
||||
setIsFinal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetVoting = async () => {
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}`);
|
||||
|
||||
// Get the current session data
|
||||
const snapshot = await get(sessionRef);
|
||||
if (snapshot.exists()) {
|
||||
const sessionData = snapshot.val() || {};
|
||||
|
||||
// Reset the votes while retaining users
|
||||
const resetVotes = Object.keys(sessionData.votes).reduce((acc, user) => {
|
||||
acc[user] = -1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Update the session in the database with reset votes and reset isFinal
|
||||
await set(sessionRef, {
|
||||
...sessionData,
|
||||
votes: resetVotes,
|
||||
votedUsers: 0,
|
||||
isFinal: false,
|
||||
});
|
||||
|
||||
// Update the component state
|
||||
setVotes(resetVotes);
|
||||
setVotedUsers(0);
|
||||
setIsFinal(false);
|
||||
setAllVoted(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const user = auth.currentUser;
|
||||
|
||||
if (!user) {
|
||||
goToLobby();
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentUID(user.uid);
|
||||
|
||||
const sessionRef = ref(realtimeDb, `sessions/${sessionId}`);
|
||||
|
||||
get(sessionRef).then((snapshot) => {
|
||||
if (!snapshot.exists()) {
|
||||
// Initialize the session if it doesn't exist
|
||||
set(sessionRef, {
|
||||
votes: {},
|
||||
maxUsers: maxUsers,
|
||||
votedUsers: 0,
|
||||
isFinal: false,
|
||||
users: {},
|
||||
});
|
||||
} else {
|
||||
// If the session exists, ensure all necessary fields are set
|
||||
const sessionData = snapshot.val() || {};
|
||||
|
||||
// Check and add missing fields
|
||||
if (!sessionData.maxUsers) {
|
||||
sessionData.maxUsers = maxUsers;
|
||||
}
|
||||
if (!sessionData.votedUsers) {
|
||||
sessionData.votedUsers = 0;
|
||||
}
|
||||
if (!sessionData.isFinal) {
|
||||
sessionData.isFinal = false;
|
||||
}
|
||||
if (typeof sessionData.votes === 'undefined') {
|
||||
sessionData.votes = {};
|
||||
}
|
||||
if (typeof sessionData.users === 'undefined') {
|
||||
sessionData.users = {};
|
||||
}
|
||||
|
||||
// Update the session with missing fields if needed
|
||||
set(sessionRef, sessionData);
|
||||
|
||||
const userList = Object.entries(sessionData.users).map(
|
||||
([uid, userInfo]) => ({
|
||||
uid,
|
||||
username: userInfo.username, // Ensure username is passed
|
||||
})
|
||||
);
|
||||
|
||||
// Load the data into the state
|
||||
setUsers(userList);
|
||||
setVotes(sessionData.votes);
|
||||
setVotedUsers(sessionData.votedUsers);
|
||||
setIsFinal(sessionData.isFinal);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for changes to the session data
|
||||
const unsubscribe = onValue(sessionRef, (snapshot) => {
|
||||
const sessionData = snapshot.val();
|
||||
if (!sessionData) return;
|
||||
|
||||
if (typeof sessionData.users === 'undefined') {
|
||||
sessionData.users = {};
|
||||
}
|
||||
if (typeof sessionData.votes === 'undefined') {
|
||||
sessionData.votes = {};
|
||||
}
|
||||
|
||||
const userList = Object.entries(sessionData.users).map(
|
||||
([uid, userInfo]) => ({
|
||||
uid,
|
||||
username: userInfo.username,
|
||||
})
|
||||
);
|
||||
|
||||
setUsers(userList);
|
||||
setVotes(sessionData.votes || {});
|
||||
setVotedUsers(sessionData.votedUsers || 0);
|
||||
setIsFinal(sessionData.isFinal || false);
|
||||
|
||||
// Check if all users have voted
|
||||
setAllVoted(
|
||||
sessionData.votedUsers ===
|
||||
Object.keys(sessionData.votes || {}).length &&
|
||||
Object.keys(sessionData.votes || {}).length > 0
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [sessionId, maxUsers, navigate, setCurrentUID, goToLobby]);
|
||||
|
||||
return (
|
||||
<div className={styles.votingTable}>
|
||||
{/* <ExitIcon
|
||||
className={styles.logout}
|
||||
onClick={() => handleDropUser(currentUID)}
|
||||
title="Logout"
|
||||
/> */}
|
||||
<ProfileMenuButton username={username} onLogout={handleLogout} />
|
||||
<img className={styles.titleCard} src={titleCardSrc} alt="round-table" />
|
||||
<div className={styles.tableCards}>
|
||||
{users.map(({ uid, username }) => (
|
||||
<div key={uid} className={styles.userCardWrapper}>
|
||||
<BoardCard
|
||||
username={username}
|
||||
vote={votes[uid]}
|
||||
isRevealed={isFinal && votes[uid] !== -1}
|
||||
isDimmed={votes[uid] === -1}
|
||||
isHighlighted={currentUID === uid}
|
||||
/>
|
||||
{
|
||||
<ExitIcon
|
||||
className={styles.dropButton}
|
||||
onClick={() => handleDropUser(uid)}
|
||||
title="Drop User"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
{allVoted && !isFinal && (
|
||||
<div
|
||||
className={`${styles.reveal} ${styles.button}`}
|
||||
onClick={handleFinalizeVoting}
|
||||
>
|
||||
Reveal Votes
|
||||
</div>
|
||||
)}
|
||||
{isFinal && (
|
||||
<div
|
||||
className={`${styles.restart} ${styles.button}`}
|
||||
onClick={handleResetVoting}
|
||||
>
|
||||
Restart Voting
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isFinal ? (
|
||||
<VotingBoard
|
||||
sessionId={sessionId}
|
||||
userId={userId}
|
||||
maxUsers={maxUsers}
|
||||
/>
|
||||
) : (
|
||||
<Results votes={votes} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VotingTable;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import VotingTable from './VotingTable';
|
||||
import { generateUniqueId } from '../../utils/crypto';
|
||||
import { generateUniqueId } from '../../shared/utils/crypto';
|
||||
|
||||
export default {
|
||||
title: 'Components/VotingTable',
|
||||
|
||||
203
src/components/VotingTable/index.jsx
Normal file
203
src/components/VotingTable/index.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import VotingBoard from '../VotingBoard';
|
||||
import BoardCard from '../BoardCard';
|
||||
import Results from '../Results';
|
||||
import styles from './VotingTable.module.css';
|
||||
import titleCardSrc from '../../assets/images/titlecard.png';
|
||||
import { ReactComponent as ExitIcon } from '../../assets/images/exit.svg';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import ProfileMenuButton from '../ProfileMenuButton';
|
||||
import api from '../../services/api';
|
||||
|
||||
const VotingTable = ({ userId }) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [votes, setVotes] = useState({});
|
||||
const [, setVotedUsers] = useState(0);
|
||||
const [isFinal, setIsFinal] = useState(false);
|
||||
const { sessionId } = useParams();
|
||||
const [currentUID, setCurrentUID] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const maxUsers = 32;
|
||||
const username =
|
||||
users.find((user) => user.uid === currentUID)?.username || '';
|
||||
|
||||
const goToLobby = useCallback(() => {
|
||||
navigate(`/lobby/${sessionId}`);
|
||||
}, [sessionId, navigate]);
|
||||
|
||||
async function handleLogout() {
|
||||
await handleDropUser(currentUID);
|
||||
}
|
||||
|
||||
async function handleInvite() {
|
||||
try {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy invite link: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDropUser = async (userId) => {
|
||||
try {
|
||||
// Remove user vote and user from session via API calls
|
||||
await api.delete(`/api/session/${sessionId}/votes/${userId}`);
|
||||
await api.delete(`/api/session/${sessionId}/users/${userId}`);
|
||||
|
||||
// Fetch updated session data after user removal
|
||||
const sessionResponse = await api.get(`/api/session/${sessionId}`);
|
||||
const sessionData = sessionResponse.data;
|
||||
|
||||
if (!sessionData) return;
|
||||
|
||||
// Update state with the new session data
|
||||
const userList = Object.entries(sessionData.users || {}).map(
|
||||
([uid, userInfo]) => ({
|
||||
uid,
|
||||
username: userInfo.username,
|
||||
})
|
||||
);
|
||||
|
||||
setUsers(userList);
|
||||
setVotes(sessionData.votes || {});
|
||||
|
||||
// Redirect the dropped user to the lobby
|
||||
await api.post(`/api/users/${userId}`, {
|
||||
currentSessionId: `lobby/${sessionId}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing user:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinalizeVoting = async () => {
|
||||
try {
|
||||
// Finalize voting by setting the isFinal flag in the session via API call
|
||||
await api.post(`/api/session/${sessionId}/finalize`, {
|
||||
isFinal: true,
|
||||
});
|
||||
setIsFinal(true);
|
||||
} catch (error) {
|
||||
console.error('Error finalizing voting:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetVoting = async () => {
|
||||
try {
|
||||
// Reset the session's votes and isFinal flag via API call
|
||||
await api.post(`/api/session/${sessionId}/reset`, {
|
||||
votes: {},
|
||||
isFinal: false,
|
||||
});
|
||||
setVotes({});
|
||||
setVotedUsers(0);
|
||||
setIsFinal(false);
|
||||
} catch (error) {
|
||||
console.error('Error resetting voting:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSessionData = async () => {
|
||||
try {
|
||||
// Fetch session data from the API
|
||||
const sessionResponse = await api.get(`/api/session/${sessionId}`);
|
||||
const sessionData = sessionResponse.data;
|
||||
|
||||
if (!sessionData) {
|
||||
// Initialize the session if it doesn't exist
|
||||
await api.post(`/api/session/${sessionId}`, {
|
||||
votes: {},
|
||||
maxUsers: maxUsers,
|
||||
isFinal: false,
|
||||
users: {},
|
||||
});
|
||||
} else {
|
||||
// Load the existing session data
|
||||
const userList = Object.entries(sessionData.users || {}).map(
|
||||
([uid, userInfo]) => ({
|
||||
uid,
|
||||
username: userInfo.username,
|
||||
})
|
||||
);
|
||||
setUsers(userList);
|
||||
setVotes(sessionData.votes || {});
|
||||
setIsFinal(sessionData.isFinal || false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching session data:', error);
|
||||
goToLobby(); // Redirect to lobby if the session doesn't exist
|
||||
}
|
||||
};
|
||||
|
||||
fetchSessionData();
|
||||
|
||||
// Polling or WebSocket could be used to listen for real-time updates
|
||||
const intervalId = setInterval(fetchSessionData, 5000); // Poll every 5 seconds
|
||||
return () => clearInterval(intervalId); // Clean up the interval
|
||||
}, [sessionId, maxUsers, navigate, setCurrentUID, goToLobby]);
|
||||
|
||||
const allVoted = Object.values(votes).every((v) => v >= 0);
|
||||
|
||||
return (
|
||||
<div className={styles.votingTable}>
|
||||
<ProfileMenuButton
|
||||
username={username}
|
||||
onLogout={handleLogout}
|
||||
onInvite={handleInvite}
|
||||
/>
|
||||
<img className={styles.titleCard} src={titleCardSrc} alt="title-card" />
|
||||
<div className={styles.tableCards}>
|
||||
{users.map(({ uid, username }) => (
|
||||
<div key={uid} className={styles.userCardWrapper}>
|
||||
<BoardCard
|
||||
username={username}
|
||||
vote={votes[uid]}
|
||||
isRevealed={isFinal && votes[uid] !== -1}
|
||||
isDimmed={votes[uid] === -1}
|
||||
isHighlighted={currentUID === uid}
|
||||
/>
|
||||
{
|
||||
<ExitIcon
|
||||
className={styles.dropButton}
|
||||
onClick={
|
||||
uid === currentUID ? handleLogout : () => handleDropUser(uid)
|
||||
}
|
||||
title="Drop User"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
{allVoted && !isFinal && (
|
||||
<div
|
||||
className={`${styles.reveal} ${styles.button}`}
|
||||
onClick={handleFinalizeVoting}
|
||||
>
|
||||
Reveal Votes
|
||||
</div>
|
||||
)}
|
||||
{isFinal && (
|
||||
<div
|
||||
className={`${styles.restart} ${styles.button}`}
|
||||
onClick={handleResetVoting}
|
||||
>
|
||||
Restart Voting
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isFinal ? (
|
||||
<VotingBoard
|
||||
sessionId={sessionId}
|
||||
userId={userId}
|
||||
maxUsers={maxUsers}
|
||||
/>
|
||||
) : (
|
||||
<Results votes={votes} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VotingTable;
|
||||
@@ -25,6 +25,10 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
39
src/services/api.js
Normal file
39
src/services/api.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// Create an Axios instance with base URL and credentials
|
||||
const api = axios.create({
|
||||
baseURL: '/api', // The base URL for your API
|
||||
withCredentials: true, // Send cookies with requests
|
||||
});
|
||||
|
||||
// Set up an Axios interceptor to handle token expiration (401 error)
|
||||
api.interceptors.response.use(
|
||||
(response) => response, // If the response is OK, just return it
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (
|
||||
error.response &&
|
||||
error.response.status === 401 &&
|
||||
!originalRequest._retry
|
||||
) {
|
||||
originalRequest._retry = true; // Prevent an infinite retry loop
|
||||
|
||||
// Try refreshing the token
|
||||
try {
|
||||
await api.post('/api/auth/refresh-token');
|
||||
// Retry the original request with the new token
|
||||
return api(originalRequest);
|
||||
} catch (refreshError) {
|
||||
console.log('originalRequest', originalRequest);
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
// Handle failed refresh (e.g., redirect to login page)
|
||||
window.location.href = '/lobby';
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error); // Reject if it's not a 401 or after retry
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
1
src/shared
Symbolic link
1
src/shared
Symbolic link
@@ -0,0 +1 @@
|
||||
/Users/cmedina/Documents/Developer/Apps/reactApps/fibonacci-fold/shared
|
||||
@@ -1,9 +0,0 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// Generate a session ID hash
|
||||
export const generateUniqueId = () => {
|
||||
const uuid = uuidv4();
|
||||
const hash = CryptoJS.SHA256(uuid).toString();
|
||||
return hash.substring(0, 28); // Use the first 8 characters of the hash
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
export function mockThis(input) {
|
||||
const defaultValues = {
|
||||
string: 'default string',
|
||||
number: 0,
|
||||
boolean: false,
|
||||
object: {},
|
||||
array: [],
|
||||
function: () => {}, // Default empty function
|
||||
};
|
||||
|
||||
const nativeTypes = new Set([
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
'Date',
|
||||
'RegExp',
|
||||
'ArrayBuffer',
|
||||
'SharedArrayBuffer',
|
||||
'DataView',
|
||||
'Promise',
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
'Int16Array',
|
||||
'Uint16Array',
|
||||
'Int32Array',
|
||||
'Uint32Array',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'BigInt64Array',
|
||||
'BigUint64Array',
|
||||
]);
|
||||
|
||||
function getDefaultValue(value) {
|
||||
const valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||
return defaultValues[valueType] !== undefined
|
||||
? defaultValues[valueType]
|
||||
: null;
|
||||
}
|
||||
|
||||
function createGenericMock() {
|
||||
return new Proxy(() => {}, {
|
||||
get(target, prop) {
|
||||
return createGenericMock();
|
||||
},
|
||||
apply() {
|
||||
return createGenericMock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mockFunction(func) {
|
||||
return new Proxy(func, {
|
||||
apply(target, thisArg, argumentsList) {
|
||||
const returnValue = target.apply(thisArg, argumentsList);
|
||||
return mockValue(returnValue);
|
||||
},
|
||||
get(target, prop) {
|
||||
return createGenericMock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mockObject(obj) {
|
||||
if (nativeTypes.has(obj.constructor.name)) {
|
||||
return obj; // Return native types without modification
|
||||
}
|
||||
|
||||
return new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
if (prop === 'getProvider') {
|
||||
return () => ({
|
||||
getImmediate: (arg) => {
|
||||
// Handle different argument types passed to getImmediate
|
||||
if (arg && arg.identifier === '(default)') {
|
||||
return {
|
||||
triggerHeartbeat: () => ({
|
||||
isInitialized: () => true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
// Return a generic mock for other cases
|
||||
return createGenericMock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (prop in target) {
|
||||
return mockValue(target[prop]);
|
||||
}
|
||||
return createGenericMock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mockValue(value) {
|
||||
if (typeof value === 'function') {
|
||||
return mockFunction(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return mockObject(value);
|
||||
} else {
|
||||
return getDefaultValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
return mockValue(input);
|
||||
}
|
||||
|
||||
export default mockThis;
|
||||
5
src/utils/strings.js
Normal file
5
src/utils/strings.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const getAcronym = (str = '', separator = ' ', union = '') => {
|
||||
const words = str.split(separator).filter((word) => word);
|
||||
const letters = words.map((word) => word[0]);
|
||||
return letters.join(union);
|
||||
};
|
||||
Reference in New Issue
Block a user