This commit is contained in:
2024-09-05 19:33:30 -04:00
parent 51da011d12
commit 7f6116450f
187 changed files with 81911 additions and 388 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
build/

16
.eslintrc.json Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": [
"react-app",
"react-app/jest",
"plugin:storybook/recommended",
"plugin:prettier/recommended"
],
"plugins": ["react", "prettier"],
"rules": {
"prettier/prettier": "error"
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
}
}

View File

@@ -0,0 +1,33 @@
index.html,1725860405793,6310d84ffdca1cba888ffec82dec1bd853728243a85f931c7bbb1ea501234ed9
robots.txt,1725860392506,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
logo192.png,1725860392504,f66564670b5bfe49f3f04a5b63107bae60f4a7736f1d3b83aa0115f7acf45150
favicon.ico,1725860392503,1463be9bedc82e16f9364eec7a1f5152cbe56229401381eed65c1496cb884407
asset-manifest.json,1725860405804,4e32ba450098f872b3a581873dc770a8b5c91d6d56f58e07f8ffe31b70c53ef7
logo512.png,1725860392505,2356e0b814e58a349935c8de17226873d615d088a867d591fef48a64c80c9a2f
static/media/exit.5b9b641b90012b1192056a8c7e1b8c45.svg,1725860405791,4ff12475566874f3f3d3be8e8c97ca14a7b9292a04481947610725c16bb1599d
static/media/coffee.46aafd6dd9e4a1d3460e80ff9969dc6e.svg,1725860405791,f27933e7a7e8113c7a2be9eba7492922dd5918b77575f02a8ad595cb079e5547
static/media/infinity.d099870a8639593717e3b17aea138c92.svg,1725860405791,70485bc15570a561994193d7e3e6b08a1f2de236248676255b871c8697e4c577
manifest.json,1725860392505,4e1d3adad9ce7e17c0d13fac197f1c27091906f4ecf3909c9f51247bff345118
static/media/cardo-greek-ext-400-normal.134b821611b16e279388.woff2,1725860405791,f47df12ed94c7e7bb8a2e37e21d10cde4a47fd41eeda611d924a55187ad01466
static/media/cardo-latin-400-normal.4911b85d7a3867b3911f.woff2,1725860405791,8f43ac21350311e2925bf981b9fd8da34935abad28c7b1028c2ca2756af003f3
static/media/cardo-greek-400-normal.79d37e879083ce714bd7.woff2,1725860405791,074bc81d8ca5a2111b391b121b56d17a67601361e97404d3d7792dbda1dbe5bd
static/media/cardo-greek-400-normal.82ddcf833fd34281b75d.woff,1725860405791,3b4c19d6c84e7a572ee345b68fc874deeec43d4690c7d590a9029fddf3dbddf5
static/media/bracket.910c02ed0a238012f80f22086064c5d9.svg,1725860405791,fc713f945dd7cd2ca06c86d392ebdea3a6cf04a3e009e72877aec5e3be787486
static/media/cardo-greek-ext-400-normal.f2008e9d17e15badfffd.woff,1725860405791,f3e91ac60b6103eb6065b6d12d6adb656d95b1ab9904f98d15916b49849385ec
static/js/main.4e103dc0.js.LICENSE.txt,1725860405805,fd222b36f412805419e026feba067f18ca36cf439c430ae6dfd4bf072d541304
static/js/453.18d94f3f.chunk.js,1725860405805,0c85c1b7dd1771b07c17867a1b4cc9147aac500e2761b78e5f43c3af5a990b97
static/js/453.18d94f3f.chunk.js.map,1725860405805,01852c42a7189dcf6095a7437716308dcf1058fbf9afeee17e1d113d8fc0016f
static/css/main.f0edc1a0.css,1725860405805,1c87f7cfa7da9415d7b6abe70cf41d8705f1d751ee0f4d6b406baafbb9bcca8b
static/css/main.f0edc1a0.css.map,1725860405805,1a40319fdab387a87e11b7c3aadf554d86722ada29a129ba316b832bcdbaefa0
static/media/cardo-latin-ext-400-normal.d90efa9a5c52b57c81be.woff2,1725860405792,ac5c40b8f0f0170763306cbd56736ea93f3bc8a46bd69691097be496e3b9a604
static/media/cardo-latin-400-normal.a3fc903c20de90e51735.woff,1725860405791,cf7c3f21e093e6af8f4490da55d497398a38ff9485afae511311013338b8fe82
static/media/cardo-latin-ext-400-normal.448cff0b9a0afb700151.woff,1725860405791,b285c4d668a7c861ca15b7dd3e126f26f6c06449c41a2c823a10146b0550089c
static/media/Blackmild Regular.5b441560e0d850038d0f.otf,1725860405791,60300d3ccf8a22a528b216703a9677022b27e7f05fe71f9768e58220c132a4c9
static/media/Cinzel-VariableFont_wght.2d087b545bf495f3d86d.ttf,1725860405791,3394f58fb0427fbe8d8dcc90863079661330c57c41d22309fabdda6f9bbbf7e3
static/media/Cardo-Regular.4ad75d4da97e1cf6655b.ttf,1725860405791,9441d454efe51434f20f9f848652531e4caa3ffa57a3c7d2caa08370b01b2c63
static/media/card.1d7fa489536497609c42.png,1725860405805,aed32aacda18dd89590e88b93da24764ec4c8d3a94f9390d90b241fb88f20788
static/js/main.4e103dc0.js,1725860405805,20a9e0c99d6ba25a839ea31b3fcceb3d71c1e6835061ddb862bc51a56f0cf0ca
static/media/applogo.9a90eed8f247993fa372.png,1725860405805,162fdef5335b42be3160731c6f9472f17d8a47b1a91f0f14f2a4fcf3b46559cd
static/media/titlecard.420823b8e1f5db1380e4.png,1725860405806,77bccf483368ffe9622e590756dac4c2011d125589802b193a9d9ac79f1ddf0b
static/media/cardback.6c07d542c2853cbf82d3.png,1725860405806,4cd2749fe35df98d738b7504afe8469dbdd2ba23101544b81c9592be8c5321f4
static/js/main.4e103dc0.js.map,1725860405807,61fad40e9da546f6b8440539286164bb2d5d301525c6149d772184ed61d7dc04

View File

@@ -1,6 +1,6 @@
robots.txt,1724736751497,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2 robots.txt,1724736751497,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
manifest.json,1724736751496,4e1d3adad9ce7e17c0d13fac197f1c27091906f4ecf3909c9f51247bff345118 manifest.json,1724736751496,4e1d3adad9ce7e17c0d13fac197f1c27091906f4ecf3909c9f51247bff345118
index.html,1724743290229,478e680598c372dc98351e6f43bf53591fe6607c1b1f12fc288e36966444171a
logo192.png,1724736751497,f66564670b5bfe49f3f04a5b63107bae60f4a7736f1d3b83aa0115f7acf45150
logo512.png,1724736751496,2356e0b814e58a349935c8de17226873d615d088a867d591fef48a64c80c9a2f logo512.png,1724736751496,2356e0b814e58a349935c8de17226873d615d088a867d591fef48a64c80c9a2f
logo192.png,1724736751497,f66564670b5bfe49f3f04a5b63107bae60f4a7736f1d3b83aa0115f7acf45150
index.html,1725573204717,aa536089eb6759ecf98bc29cb90c3ff760a32446e3c3bef5443cb721dc81efeb
favicon.ico,1724736751495,1463be9bedc82e16f9364eec7a1f5152cbe56229401381eed65c1496cb884407 favicon.ico,1724736751495,1463be9bedc82e16f9364eec7a1f5152cbe56229401381eed65c1496cb884407

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
build/

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2
}

View File

@@ -8,6 +8,7 @@ const config = {
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@chromatic-com/storybook", "@chromatic-com/storybook",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"storybook-addon-mock/register"
], ],
framework: { framework: {
name: "@storybook/react-webpack5", name: "@storybook/react-webpack5",

Binary file not shown.

9
database-debug.log Normal file
View File

@@ -0,0 +1,9 @@
21:13:22.421 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
21:13:22.572 [main] INFO com.firebase.server.forge.App$ - Listening at 127.0.0.1:9000
21:13:31.975 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - fibonacci-fold-default-rtdb successfully activated FBKV (SurveyIdle(0)) wait: 121ms, init: 0ms
21:13:32.035 [NamespaceSystem-blocking-namespace-operation-dispatcher-6] INFO com.firebase.core.namespace.StateManager - Namespace fibonacci-fold-default-rtdb status Active to Active
21:14:00.571 [FirebaseWorkerPool-1-2] WARN com.firebase.core.persistence.backend.dummy.EmulatorMetadataPersistence - Multiple projectIds are not recommended in single project mode. Requested
namespace fake-project-id, but the emulator is configured for fibonacci-fold-default-rtdb. To
opt-out of single project mode add/set the singleProjectMode: false property in
the firebase.json emulators config.
21:14:00.575 [NamespaceSystem-akka.actor.default-dispatcher-10] INFO com.firebase.core.namespace.NamespaceActor - fake-project-id successfully activated FBKV (SurveyIdle(0)) wait: 1ms, init: 0ms

View File

@@ -1,7 +1,12 @@
{ {
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
"rules": { "rules": {
".read": false, ".read": "auth != null",
".write": false ".write": "auth != null",
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
} }
} }

View File

@@ -3,17 +3,28 @@
"rules": "database.rules.json" "rules": "database.rules.json"
}, },
"hosting": { "hosting": {
"public": "public", "public": "build",
"ignore": [ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
"destination": "/index.html" "destination": "/index.html"
} }
] ]
},
"emulators": {
"auth": {
"port": 9099
},
"database": {
"port": 9000
},
"hosting": {
"port": 5000
},
"ui": {
"enabled": true
},
"singleProjectMode": true
} }
} }

16
firestore-debug.log Normal file
View File

@@ -0,0 +1,16 @@
Aug 31, 2024 1:39:31 AM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start
INFO: Started WebSocket server on ws://127.0.0.1:9150
API endpoint: http://127.0.0.1:8080
If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080
If you are running a Firestore in Datastore Mode project, run:
export DATASTORE_EMULATOR_HOST=127.0.0.1:8080
Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues.
Dev App Server is now running.
*** shutting down gRPC server since JVM is shutting down
*** server shut down

View File

@@ -7,10 +7,13 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"crypto-js": "^4.2.0",
"firebase": "^10.13.0", "firebase": "^10.13.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.26.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"uuid": "^10.0.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
@@ -19,14 +22,23 @@
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest", "react-app/jest",
"plugin:storybook/recommended" "plugin:storybook/recommended",
] "plugin:prettier/recommended"
],
"plugins": [
"react",
"prettier"
],
"rules": {
"prettier/prettier": "error"
}
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -41,6 +53,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.25.1",
"@chromatic-com/storybook": "^1.7.0", "@chromatic-com/storybook": "^1.7.0",
"@storybook/addon-essentials": "^8.2.9", "@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9", "@storybook/addon-interactions": "^8.2.9",
@@ -51,11 +64,21 @@
"@storybook/react": "^8.2.9", "@storybook/react": "^8.2.9",
"@storybook/react-webpack5": "^8.2.9", "@storybook/react-webpack5": "^8.2.9",
"@storybook/test": "^8.2.9", "@storybook/test": "^8.2.9",
"dotenv": "^16.4.5",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-storybook": "^0.8.0", "eslint-plugin-storybook": "^0.8.0",
"firebase-mock": "^2.3.2",
"firebase-tools": "^13.15.4", "firebase-tools": "^13.15.4",
"lint": "^0.8.19",
"prettier": "^3.3.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"storybook": "^8.2.9", "storybook": "^8.2.9",
"storybook-addon-mock": "^5.0.0",
"webpack": "^5.94.0" "webpack": "^5.94.0"
}, },
"packageManager": "yarn@4.4.1" "packageManager": "yarn@4.4.1",
"homepage": "/"
} }

View File

@@ -1,89 +1,20 @@
<!DOCTYPE html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<title>Welcome to Firebase Hosting</title> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- update the version number as needed --> <meta
<script defer src="/__/firebase/10.13.0/firebase-app-compat.js"></script> name="description"
<!-- include only the Firebase features as you need --> content="Web site created using create-react-app"
<script defer src="/__/firebase/10.13.0/firebase-auth-compat.js"></script> />
<script defer src="/__/firebase/10.13.0/firebase-database-compat.js"></script> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<script defer src="/__/firebase/10.13.0/firebase-firestore-compat.js"></script> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<script defer src="/__/firebase/10.13.0/firebase-functions-compat.js"></script> <title>React App</title>
<script defer src="/__/firebase/10.13.0/firebase-messaging-compat.js"></script>
<script defer src="/__/firebase/10.13.0/firebase-storage-compat.js"></script>
<script defer src="/__/firebase/10.13.0/firebase-analytics-compat.js"></script>
<script defer src="/__/firebase/10.13.0/firebase-remote-config-compat.js"></script>
<script defer src="/__/firebase/10.13.0/firebase-performance-compat.js"></script>
<!--
initialize the SDK after all desired features are loaded, set useEmulator to false
to avoid connecting the SDK to running emulators.
-->
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head> </head>
<body> <body>
<div id="message"> <noscript>You need to enable JavaScript to run this app.</noscript>
<h2>Welcome</h2> <div id="root"></div>
<h1>Firebase Hosting Setup Complete</h1>
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
</div>
<p id="load">Firebase SDK Loading&hellip;</p>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loadEl = document.querySelector('#load');
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// // The Firebase SDK is initialized and available here!
//
// firebase.auth().onAuthStateChanged(user => { });
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
// firebase.firestore().doc('/foo/bar').get().then(() => { });
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
// firebase.messaging().requestPermission().then(() => { });
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
// firebase.analytics(); // call to activate
// firebase.analytics().logEvent('tutorial_completed');
// firebase.performance(); // call to activate
//
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
try {
let app = firebase.app();
let features = [
'auth',
'database',
'firestore',
'functions',
'messaging',
'storage',
'analytics',
'remoteConfig',
'performance',
].filter(feature => typeof app[feature] === 'function');
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
} catch (e) {
console.error(e);
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
}
});
</script>
</body> </body>
</html> </html>

11
pubsub-debug.log Normal file
View File

@@ -0,0 +1,11 @@
This is the Google Pub/Sub fake.
Implementation may be incomplete or differ from the real system.
Aug 31, 2024 1:39:41 AM com.google.cloud.pubsub.testing.v1.Main main
INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Aug 31, 2024 1:39:42 AM com.google.cloud.pubsub.testing.v1.Main main
INFO: Server started, listening on 8085
*** shutting down gRPC server since JVM is shutting down
*** server shut down

View File

@@ -1,49 +1,59 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import './App.css'; import {
import VoteCard from './components/VoteCard/VoteCard'; BrowserRouter as Router,
import VotingBoard from './components/VotingBoard/VotingBoard'; Routes,
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 the functions you need from the SDKs you need const App = () => {
import { initializeApp } from "firebase/app"; const [userId, setUserId] = useState('');
import { getAnalytics } from "firebase/analytics"; const [loading, setLoading] = useState(true);
// Your web app's Firebase configuration const handleJoinSession = (userId) => {
// For Firebase JS SDK v7.20.0 and later, measurementId is optional setUserId(userId);
const firebaseConfig = { return true;
apiKey: REACT_APP_FIREBASE_API_KEY,
authDomain: REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: REACT_APP_FIREBASE_APP_ID,
measurementId: REACT_APP_FIREBASE_DATABASE_URL,
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
function App() {
const [selectedVote, setSelectedVote] = useState(null);
const handleVoteClick = (number) => {
setSelectedVote(number);
}; };
return ( useEffect(() => {
<div className="App"> // Sign in the user anonymously
<header className="App-header"> signInAnonymously(auth).catch((error) => {
<h1>Fibonacci Voting App</h1> console.error('Anonymous sign-in error:', error);
</header> });
<div className="App-main"> // Handle user state changes
<h2>Pick Your Vote</h2> onAuthStateChanged(auth, (user) => {
<main className="vote-cards-container"> if (user) {
<VotingBoard /> console.log('User is signed in:', user);
</main> } else {
</div> console.log('User is signed out');
</div> }
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>
</Router>
); );
} };
export default App; export default App;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,22 @@
@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');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'cinzel';
src: url('/src/assets/fonts/Cinzel/Cinzel-VariableFont_wght.ttf')
format('opentype');
font-weight: normal;
font-style: normal;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

BIN
src/assets/images/card.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

BIN
src/assets/images/card.xcf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,4 +1,4 @@
<svg width="93" height="93" viewBox="0 0 93 93" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="93" height="93" viewBox="0 0 93 93" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.5 81.375H31C20.3166 81.375 11.625 72.6834 11.625 62V38.75H65.875V62C65.875 72.6834 57.1834 81.375 46.5 81.375ZM19.375 46.5V62C19.375 68.4093 24.5907 73.625 31 73.625H46.5C52.9093 73.625 58.125 68.4093 58.125 62V46.5H19.375Z" fill="#0094FF"/> <path d="M46.5 81.375H31C20.3166 81.375 11.625 72.6834 11.625 62V38.75H65.875V62C65.875 72.6834 57.1834 81.375 46.5 81.375ZM19.375 46.5V62C19.375 68.4093 24.5907 73.625 31 73.625H46.5C52.9093 73.625 58.125 68.4093 58.125 62V46.5H19.375Z" fill="currentColor"/>
<path d="M63.4725 71.7456L60.5275 64.5769L73.377 59.2991L73.5514 50.375H62V42.625H81.375V59.2061C81.375 62.5464 79.2437 65.503 76.074 66.557L63.4725 71.7456ZM38.3431 32.7321L31.4069 29.2679L33.5497 24.986C34.0922 23.901 34.0922 22.6067 33.5497 21.5179L33.1429 20.7002C32.34 19.0843 31.9222 17.3044 31.9222 15.5C31.9222 13.6956 32.34 11.9157 33.1429 10.2997L35.2857 6.01787L42.2181 9.486L40.0752 13.7679C39.5327 14.8529 39.5327 16.1471 40.0752 17.236L40.4821 18.0536C42.1096 21.3086 42.1096 25.1991 40.4821 28.4541L38.3431 32.7321ZM53.8431 32.7321L46.9107 29.264L49.0536 24.9821C49.5961 23.8971 49.5961 22.6029 49.0536 21.514L48.6467 20.6964C47.8439 19.0804 47.426 17.3005 47.426 15.4961C47.426 13.6917 47.8439 11.9118 48.6467 10.2959L50.7896 6.014L57.722 9.48212L55.5791 13.764C55.0366 14.849 55.0366 16.1432 55.5791 17.2321L55.986 18.0497C57.6135 21.3047 57.6135 25.1952 55.986 28.4502L53.8431 32.7321ZM22.8431 32.7321L15.9069 29.2679L18.0498 24.986C18.5923 23.901 18.5923 22.6067 18.0498 21.5179L17.6429 20.7002C16.84 19.0843 16.4222 17.3044 16.4222 15.5C16.4222 13.6956 16.84 11.9157 17.6429 10.2997L19.7858 6.01787L26.7181 9.486L24.5753 13.7679C24.0328 14.8529 24.0328 16.1471 24.5753 17.236L24.9821 18.0536C26.6096 21.3086 26.6096 25.1991 24.9821 28.4541L22.8431 32.7321Z" fill="#0094FF"/> <path d="M63.4725 71.7456L60.5275 64.5769L73.377 59.2991L73.5514 50.375H62V42.625H81.375V59.2061C81.375 62.5464 79.2437 65.503 76.074 66.557L63.4725 71.7456ZM38.3431 32.7321L31.4069 29.2679L33.5497 24.986C34.0922 23.901 34.0922 22.6067 33.5497 21.5179L33.1429 20.7002C32.34 19.0843 31.9222 17.3044 31.9222 15.5C31.9222 13.6956 32.34 11.9157 33.1429 10.2997L35.2857 6.01787L42.2181 9.486L40.0752 13.7679C39.5327 14.8529 39.5327 16.1471 40.0752 17.236L40.4821 18.0536C42.1096 21.3086 42.1096 25.1991 40.4821 28.4541L38.3431 32.7321ZM53.8431 32.7321L46.9107 29.264L49.0536 24.9821C49.5961 23.8971 49.5961 22.6029 49.0536 21.514L48.6467 20.6964C47.8439 19.0804 47.426 17.3005 47.426 15.4961C47.426 13.6917 47.8439 11.9118 48.6467 10.2959L50.7896 6.014L57.722 9.48212L55.5791 13.764C55.0366 14.849 55.0366 16.1432 55.5791 17.2321L55.986 18.0497C57.6135 21.3047 57.6135 25.1952 55.986 28.4502L53.8431 32.7321ZM22.8431 32.7321L15.9069 29.2679L18.0498 24.986C18.5923 23.901 18.5923 22.6067 18.0498 21.5179L17.6429 20.7002C16.84 19.0843 16.4222 17.3044 16.4222 15.5C16.4222 13.6956 16.84 11.9157 17.6429 10.2997L19.7858 6.01787L26.7181 9.486L24.5753 13.7679C24.0328 14.8529 24.0328 16.1471 24.5753 17.236L24.9821 18.0536C26.6096 21.3086 26.6096 25.1991 24.9821 28.4541L22.8431 32.7321Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="142" height="142" viewBox="0 0 142 142" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97 16H34C25.7157 16 19 22.7157 19 31V108C19 116.284 25.7157 123 34 123H97" stroke="currentColor" stroke-width="8" stroke-linecap="round"/>
<path d="M48 66C45.7909 66 44 67.7909 44 70C44 72.2091 45.7909 74 48 74L48 66ZM129.828 72.8284C131.391 71.2663 131.391 68.7337 129.828 67.1716L104.373 41.7157C102.81 40.1536 100.278 40.1536 98.7157 41.7157C97.1536 43.2778 97.1536 45.8105 98.7157 47.3726L121.343 70L98.7157 92.6274C97.1536 94.1895 97.1536 96.7222 98.7157 98.2843C100.278 99.8464 102.81 99.8464 104.373 98.2843L129.828 72.8284ZM48 74L127 74L127 66L48 66L48 74Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@@ -1,3 +1,3 @@
<svg width="100" height="46" viewBox="0 0 100 46" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="135" height="97" viewBox="0 0 135 97" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.868087 23.2669C0.470951 6.27752 19.011 -5.65518 35.2836 3.66982C40.6444 6.74186 45.1952 11.2917 50.0884 16.2609C50.3109 16.4869 50.4136 16.4635 50.6218 16.2537C58.2754 8.53838 69.3574 -3.22605 84.8798 1.35112C91.4123 3.27737 95.9515 7.51559 98.2771 13.902C106.06 35.276 83.7339 52.9721 64.5881 41.2414C59.7593 38.2827 55.5891 34.2003 50.6597 29.18C50.3966 28.912 50.2779 28.95 50.0373 29.1926C44.4371 34.8399 38.4089 40.9785 30.963 43.5483C15.8111 48.7776 1.28585 39.7487 0.868087 23.2669ZM10.3494 22.9375C10.8325 32.7514 18.3815 37.3264 26.4248 35.5867C33.3759 34.0832 39.2071 27.9698 44.1737 22.8643C44.341 22.6923 44.2472 22.6224 44.1322 22.5151C39.8185 18.4886 35.6058 13.5701 30.2966 11.039C20.0067 6.13343 10.3148 13.3276 10.3494 22.9375ZM90.3588 22.5228C90.0531 13.0384 82.5705 7.86993 73.9483 9.89237C67.1642 11.4836 61.3671 17.5682 56.5507 22.5168C56.3836 22.6885 56.4478 22.7687 56.5847 22.8964C62.6066 28.5163 69.713 37.3892 79.3141 35.7721C85.7095 34.6949 90.3731 29.1245 90.3588 22.5228Z" fill="#0094FF"/> <path d="M18.6919 48.2838C18.2948 31.2944 36.8348 19.3617 53.1074 28.6867C58.4682 31.7587 63.019 36.3086 67.9122 41.2778C68.1347 41.5038 68.2374 41.4804 68.4456 41.2706C76.0992 33.5553 87.1812 21.7908 102.704 26.368C109.236 28.2943 113.775 32.5325 116.101 38.9188C123.884 60.2929 101.558 77.989 82.412 66.2583C77.5831 63.2996 73.4129 59.2171 68.4836 54.1969C68.2204 53.9289 68.1017 53.9668 67.8612 54.2094C62.2609 59.8568 56.2327 65.9953 48.7868 68.5652C33.6349 73.7945 19.1097 64.7656 18.6919 48.2838ZM28.1732 47.9544C28.6563 57.7683 36.2053 62.3433 44.2486 60.6036C51.1997 59.1001 57.0309 52.9867 61.9975 47.8812C62.1648 47.7092 62.071 47.6393 61.956 47.532C57.6423 43.5055 53.4296 38.587 48.1204 36.0559C37.8305 31.1503 28.1386 38.3445 28.1732 47.9544ZM108.183 47.5397C107.877 38.0553 100.394 32.8868 91.7721 34.9093C84.988 36.5005 79.1909 42.585 74.3745 47.5337C74.2074 47.7054 74.2716 47.7855 74.4085 47.9133C80.4305 53.5331 87.5368 62.4061 97.138 60.789C103.533 59.7118 108.197 54.1414 108.183 47.5397Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

View File

@@ -0,0 +1,26 @@
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 styles from './BoardCard.module.css';
const BoardCard = ({ username, vote, isRevealed, isDimmed, isHighlighted }) => {
return (
<div className={`${styles.boardCard}`}>
<div className={styles.highlightIconContainer}>
{isHighlighted ? <BracketIcon /> : null}
</div>
<div
className={`${styles.card} ${isRevealed ? styles.flip : ''} ${isDimmed && styles.dimmed}`}
>
<img src={cardBack} alt="Card Back" />
<div className={styles.front}>
<FrontCard value={vote} />
</div>
</div>
<div className={styles.username}>{username}</div>
</div>
);
};
export default BoardCard;

View File

@@ -0,0 +1,73 @@
.boardCard {
perspective: 1000px;
max-width: 120px;
min-width: 60px;
padding: 0.1rem;
margin: 10px;
opacity: 1;
position: relative;
display: inline-flex;
flex-flow: column wrap;
align-items: center;
justify-content: flex-start;
}
.boardCard .username {
word-break: break-word;
}
.boardCard .dimmed {
opacity: 0.3;
}
.boardCard .highlight {
/* flex-wrap: nowrap; */
/* position: absolute; */
/* transform: translate(-0%, -100%); */
/* overflow: visible; */
/* height: 20%;
align-self: center;
justify-content: center; */
/* max-width: 115px; */
display: flex;
flex-wrap: nowrap;
max-height: 50px;
}
.boardCard .highlightIconContainer,
.boardCard .highlightIconContainer svg {
width: 50px;
height: 25px;
}
.card {
width: 50px;
height: 75px;
transition: transform 0.6s ease-in-out;
transform-style: preserve-3d;
position: relative;
margin: auto;
}
.card.flip {
transform: rotateY(180deg);
}
.card img,
.card .front {
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.card .front {
transform: rotateY(
180deg
); /* This makes the front face appear on the back initially */
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import BoardCard from './BoardCard';
export default {
title: 'Components/BoardCard',
component: BoardCard,
};
export const Default = () => <BoardCard />;

View File

@@ -1,10 +0,0 @@
import React from 'react';
import styles from './CardBack.module.css';
import cardBack from '../../assets/images/cardback.png';
const CardBack = () => (
<img className={styles.cardBack} src={cardBack} />
);
export default CardBack;

View File

@@ -1,9 +0,0 @@
import React from 'react';
import CardBack from './CardBack';
export default {
title: 'Components/CardBack',
component: CardBack,
};
export const Default = () => <CardBack />;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import card from '../../assets/images/card.png';
import VoteValue from '../VoteValue/VoteValue';
import styles from './FrontCard.module.css';
const FrontCard = ({ value, className }) => {
return (
<div className={`${styles.cardFrontContainer} ${className}`}>
<img className={styles.cardFront} src={card} alt="Card Front" />
<VoteValue
value={value}
className={`${styles.vote} ${styles.voteValue}`}
/>
</div>
);
};
export default FrontCard;

View File

@@ -0,0 +1,30 @@
.cardFrontContainer {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.cardFront {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.vote {
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
width: 80%;
}
.voteValue {
color: #1b191a;
text-shadow: none;
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import FrontCard from './FrontCard';
export default {
title: 'Components/FrontCard',
component: FrontCard,
};
const Template = (args) => (
<div style={args.parentStyle}>
<FrontCard {...args} />
</div>
);
export const Default = Template.bind({});
Default.args = {
value: 5,
parentStyle: { width: 50, height: 75 },
};
export const AlternateValue = Template.bind({});
AlternateValue.args = {
value: 8,
parentStyle: { width: 50, height: 75 },
};
export const EnlargedParent = Template.bind({});
EnlargedParent.args = {
value: 8,
parentStyle: { width: 100, height: 150 },
};

View File

@@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import { ref, onValue, set } from 'firebase/database';
import { realtimeDb } from '../../services/firebase';
import { generateUniqueId } from '../../utils/crypto';
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';
const Lobby = ({ onJoinSession = () => {} }) => {
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [users, setUsers] = useState([]);
const [isUsernameTaken, setIsUsernameTaken] = useState(false);
const { sessionId } = useParams();
useEffect(() => {
// 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}`);
}
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();
// Check if the uid is already part of the session
if (users.includes(userId)) {
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}`,
});
// Call the onJoinSession callback with the username
onJoinSession(userId);
// Navigate to the session
navigate(`/session/${sessionId}`);
}
};
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
handleJoinSession();
}
};
return (
<div className={styles.lobby}>
<img className={styles.appLogo} src={appLogo} alt="logo" />
<h2>Enter Your Username</h2>
<div className={styles.form}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
maxLength={24}
onKeyDown={handleKeyDown}
/>
<div className={styles.button} onClick={handleJoinSession}>
Join Session
</div>
</div>
{isUsernameTaken && (
<p>Username is already taken. Please choose another.</p>
)}
<br />
<h3>Current Users In Session</h3>
<section className={styles.list}>
{users.map(({ uid, username }) => (
<section key={uid}>{username}</section>
))}
</section>
</div>
);
};
export default Lobby;

View File

@@ -0,0 +1,67 @@
.lobby {
text-align: center;
padding: 20px;
}
.lobby .appLogo {
max-width: 500px;
width: calc(100% - 10px);
}
.lobby ul {
list-style-type: none;
padding: 0;
}
.lobby li {
margin: 10px 0;
font-size: 18px;
}
.lobby .form {
display: flex;
justify-content: center;
}
.lobby .form .button {
background-color: #f0dcb1;
border: 1px solid black;
border-style: double;
color: black;
text-shadow: none;
padding: 4px 8px;
cursor: pointer;
font-weight: 800;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
border-radius: 4px;
transition: background-color 0.3s ease;
}
.lobby .form .button:hover {
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
background-color: #cdba95;
}
.lobby .form .button:active {
transform: scale(0.95);
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
}
.lobby .form input {
margin-right: 8px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
}
.lobby .list {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.lobby .list section {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 10px;
font-size: 18px;
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import Lobby from './Lobby';
export default {
title: 'Components/Lobby',
component: Lobby,
};
export const Default = () => (
<MemoryRouter>
<Lobby onJoinSession={() => {}} />
</MemoryRouter>
);

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import Lobby from './Lobby';
test('renders voting Lobby', () => {
const mockOnJoinSession = jest.fn();
render(<Lobby onJoinSession={mockOnJoinSession} />);
expect(screen.getByText(/5: 2 vote\(s\)/i)).toBeInTheDocument();
});

View File

@@ -1,24 +1,31 @@
import React from 'react'; import React from 'react';
import styles from './Results.module.css'; import styles from './Results.module.css';
import VoteValue from '../VoteValue/VoteValue';
const Results = ({ votes }) => { const Results = ({ votes }) => {
const voteCounts = Object.values(votes).reduce((acc, vote) => { const voteCounts = Object.values(votes).reduce((acc, vote) => {
acc[vote] = (acc[vote] || 0) + 1; acc[vote] = (acc[vote] || 0) + 1;
return acc; return acc;
}, {}); }, {});
return ( return (
<div className={styles.results}> <div className={styles.results}>
<h2>Voting Results</h2> <h2>Voting Results</h2>
<ul> <div className={styles.list}>
{Object.keys(voteCounts).map((vote, index) => ( {Object.keys(voteCounts).map((vote, index) =>
<li key={index}> vote >= 0 ? (
{vote}: {voteCounts[vote]} vote(s) <div key={index} className={styles.pair}>
</li> <div className={styles.voteValueContainer}>
))} <VoteValue className={styles.voteValue} value={vote} />
</ul> </div>
</div> {' : '}
); {voteCounts[vote]} vote(s)
</div>
) : null
)}
</div>
</div>
);
}; };
export default Results; export default Results;

View File

@@ -1,14 +1,28 @@
.results { .results {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
} }
.results ul { .results .list {
list-style-type: none; display: flex;
padding: 0; justify-content: center;
flex-wrap: wrap;
} }
.results li { .results .list .pair {
margin: 10px 0; display: inline-flex;
font-size: 18px; align-items: center;
justify-content: center;
margin: 0 10px 10px;
font-size: 18px;
}
.results .voteValueContainer {
max-width: 2rem;
margin-right: 4px;
}
.results .voteValueContainer .voteValue {
transform: scale(0.8);
align-self: center;
} }

View File

@@ -2,8 +2,8 @@ import React from 'react';
import Results from './Results'; import Results from './Results';
export default { export default {
title: 'Components/Results', title: 'Components/Results',
component: Results, component: Results,
}; };
const mockVotes = { user1: 5, user2: 8, user3: 5 }; const mockVotes = { user1: 5, user2: 8, user3: 5 };

View File

@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
import Results from './Results'; import Results from './Results';
test('renders voting results', () => { test('renders voting results', () => {
const mockVotes = { user1: 5, user2: 8, user3: 5 }; const mockVotes = { user1: 5, user2: 8, user3: 5 };
render(<Results votes={mockVotes} />); render(<Results votes={mockVotes} />);
expect(screen.getByText(/5: 2 vote\(s\)/i)).toBeInTheDocument(); expect(screen.getByText(/5: 2 vote\(s\)/i)).toBeInTheDocument();
expect(screen.getByText(/8: 1 vote\(s\)/i)).toBeInTheDocument(); expect(screen.getByText(/8: 1 vote\(s\)/i)).toBeInTheDocument();
}); });

View File

@@ -0,0 +1,46 @@
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;

View File

@@ -1,20 +1,21 @@
import React from 'react'; import React from 'react';
import styles from './VoteCard.module.css'; import styles from './VoteCard.module.css';
import { ReactComponent as Coffee } from '../../assets/images/coffee.svg'; import FrontCard from '../FrontCard/FrontCard';
import { ReactComponent as Infinity } from '../../assets/images/infinity.svg';
const VoteCard = ({ number, isSelected, isCoffee, isInfinity, onClick }) => { const VoteCard = ({ number, isSelected, onClick, className }) => {
return ( return (
<button className={`${styles.voteCard} ${isSelected ? styles.selected : ""} ${styles["cardo-regular"]}`} onClick={() => onClick(number)}> <button
<section className={styles.voteCardInnerLayer}> className={`${styles.voteCard} ${isSelected ? styles.selected : ''} ${styles['cardo-regular']} ${className}`}
{isCoffee onClick={() => onClick(number)}
? <Coffee className={styles.coffee} /> >
: isInfinity ? <Infinity className={styles.infinity} /> : number <FrontCard
} className={`${styles.frontCard} ${isSelected && styles.selected}`}
</section> value={number}
</button> />
); <div className={`${styles.shadow} ${isSelected && styles.selected}`} />
</button>
);
}; };
export default VoteCard; export default VoteCard;

View File

@@ -1,52 +1,60 @@
.voteCard { .voteCard {
width: 50px; width: 50px;
height: 75px; height: 75px;
display: inline-block; position: relative;
padding: .1rem;
margin: 10px;
background-color: #f0f0f0;
cursor: pointer;
border: 2px solid #0095ff; padding: 0;
border-radius: .7rem; margin: 5px 10px;
transition: border 0.2s ease-in-out, margin 0.2s ease-in-out, background-color 0.2s ease-in-out; cursor: pointer;
margin-top: 0px;
border: 0;
background: transparent;
} }
.voteCardInnerLayer { .voteCard .frontCard {
border: 2px solid #0095ff; transition:
border-radius: .6rem; bottom 0.2s ease-in-out,
font-size: 26px; border 0.2s ease-in-out,
font-weight: 400; transform 0.2s ease-in-out;
color: #0095ff; bottom: 0px;
width: calc(100% - 4px);
height: calc(100% - 4px);
display: flex;
align-items: center;
justify-content: center;
} }
.voteCard:hover { .voteCard .frontCard:hover {
background-color: #e0e0e0; bottom: 10px;
margin-top: -6px; z-index: 1;
} }
.selected, .selected:hover { .voteCard .frontCard.selected,
background-color: #d4edda; .voteCard .frontCard.selected:hover {
/* border-color: #c3e6cb; */ bottom: 25px;
margin-top: -12px; transform: scale(1.2);
} z-index: 1;
.coffee {
width: 70%;
}
.infinity {
width: 70%;
} }
.cardo-regular { .cardo-regular {
font-family: "Cardo", serif; font-family: 'Cardo', serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
}
.shadow {
position: absolute;
width: 50px;
height: 10px;
background-color: darkgrey;
bottom: 0;
transition: transform 0.2s ease-in-out;
border-radius: 50%;
background: radial-gradient(ellipse at center, #00000000, #00000000);
}
.voteCard:hover .shadow {
background: radial-gradient(ellipse at center, #00000066, #00000000);
}
.voteCard.selected .shadow,
.voteCard.selected:hover .shadow {
background: radial-gradient(ellipse at center, #00000033, #00000000);
transform: scaleX(1.2);
} }

View File

@@ -2,21 +2,21 @@ import React from 'react';
import VoteCard from './VoteCard'; import VoteCard from './VoteCard';
export default { export default {
title: 'Components/VoteCard', title: 'Components/VoteCard',
component: VoteCard, component: VoteCard,
}; };
const Template = (args) => <VoteCard {...args} />; const Template = (args) => <VoteCard {...args} />;
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = { Default.args = {
number: 5, number: 5,
onClick: () => null, onClick: () => null,
}; };
export const Selected = Template.bind({}); export const Selected = Template.bind({});
Selected.args = { Selected.args = {
number: 8, number: 8,
isSelected: true, isSelected: true,
onClick: () => null, onClick: () => null,
}; };

View File

@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import VoteCard from './VoteCard'; import VoteCard from './VoteCard';
test('renders VoteCard component', () => { test('renders VoteCard component', () => {
render(<VoteCard number={5} />); render(<VoteCard number={5} />);
const voteElement = screen.getByText(/5/i); const voteElement = screen.getByText(/5/i);
expect(voteElement).toBeInTheDocument(); expect(voteElement).toBeInTheDocument();
}); });

View File

@@ -0,0 +1,23 @@
import React from 'react';
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 }) => {
const isInfinity = `${value}` === '9999';
const isCoffee = `${value}` === '0';
return (
<div className={`${styles.voteValueContainer} ${className}`}>
{isCoffee ? (
<Coffee className={styles.coffee} />
) : isInfinity ? (
<Infinity className={styles.infinity} />
) : (
<div className={styles.value}>{value >= 0 ? value : ''}</div>
)}
</div>
);
};
export default VoteValue;

View File

@@ -0,0 +1,30 @@
.voteValueContainer {
display: flex;
align-items: center;
justify-content: center;
width: 100%; /* or specify a specific width */
height: 100%; /* or specify a specific height */
padding: 0.25em;
}
.coffee,
.infinity {
width: 100%; /* Adjust this to the specific size you want */
height: auto; /* Maintains aspect ratio */
max-width: 100%;
max-height: 100%;
}
.value {
font-size: 2rem; /* Adjust this to fit your design */
font-weight: 800;
line-height: 1;
width: 100%; /* Makes the text conform to the container width */
height: auto;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
/* text-shadow: none; */
/* color: #1b191a; */
}

View File

@@ -1,35 +1,65 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ref, set, get } from 'firebase/database';
import { realtimeDb } from '../../services/firebase';
import VoteCard from '../VoteCard/VoteCard'; import VoteCard from '../VoteCard/VoteCard';
import styles from './VotingBoard.module.css'; import styles from './VotingBoard.module.css';
import { submitVote } from '../../services/voteService';
const fibonacciNumbers = [0, 1, 2, 3, 5, 8, 13, 21, 9999]; const fibonacciNumbers = [0, 1, 2, 3, 5, 8, 13, 21, 9999];
const VotingBoard = () => { const VotingBoard = ({ sessionId, userId }) => {
const [selectedVote, setSelectedVote] = useState(null); const [selectedVote, setSelectedVote] = useState(null);
const handleVote = (number) => { const handleVote = async (number) => {
setSelectedVote(number); setSelectedVote(number);
submitVote("user-id", number);
};
return ( const userVoteRef = ref(
<div className={styles.votingBoard}> realtimeDb,
<h2>Select Your Vote</h2> `sessions/${sessionId}/votes/${userId}`
<div className={styles.voteCards}>
{fibonacciNumbers.map(num => (
<VoteCard
key={num}
number={num}
onClick={() => handleVote(num)}
isSelected={selectedVote === num}
isCoffee={num === 0}
isInfinity={num === 9999}
/>
))}
</div>
</div>
); );
// 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; export default VotingBoard;

View File

@@ -1,10 +1,10 @@
.votingBoard { .votingBoard {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
} }
.voteCards { .voteCards {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
} }

View File

@@ -1,9 +1,8 @@
import React from 'react';
import VotingBoard from './VotingBoard'; import VotingBoard from './VotingBoard';
export default { export default {
title: 'Components/VotingBoard', title: 'Components/VotingBoard',
component: VotingBoard, component: VotingBoard,
}; };
export const Default = () => <VotingBoard />; export const Default = () => <VotingBoard />;

View File

@@ -2,10 +2,10 @@ import { render, screen, fireEvent } from '@testing-library/react';
import VotingBoard from './VotingBoard'; import VotingBoard from './VotingBoard';
test('renders voting board and allows selecting a vote', () => { test('renders voting board and allows selecting a vote', () => {
render(<VotingBoard />); render(<VotingBoard />);
const voteButton = screen.getByText(/5/i); const voteButton = screen.getByText(/5/i);
expect(voteButton).toBeInTheDocument(); expect(voteButton).toBeInTheDocument();
fireEvent.click(voteButton); fireEvent.click(voteButton);
expect(voteButton).toHaveClass('selected'); expect(voteButton).toHaveClass('selected');
}); });

View File

@@ -0,0 +1,294 @@
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;

View File

@@ -0,0 +1,125 @@
.votingTable {
text-align: center;
padding: 20px;
}
.votingTable .logout {
position: absolute;
right: 10px;
top: 10px;
color: #f0dcb1;
cursor: pointer;
width: 24px;
height: 24px;
padding: 6px;
border-radius: 50%;
user-select: none;
border: 1px solid #f0dcb1;
opacity: 0.5;
}
.votingTable .logout:hover {
color: #ddcba3;
border-color: #ddcba3;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
opacity: 1;
}
.votingTable .logout:active {
transform: scale(0.95);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
}
.votingTable .reveal {
margin-top: 8px;
}
.votingTable .restart {
margin-top: 8px;
}
.votingTable .titleCard {
max-width: 400px;
width: calc(100% - 150px);
}
.votingTable .tableCards {
display: flex;
align-items: center;
width: 100%;
justify-content: center;
margin-top: 8px;
flex-wrap: wrap;
}
.votingTable .tableCards .userCardWrapper {
position: relative;
display: inline-block;
height: 155px;
overflow: hidden;
}
.dropButton {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, 0%);
display: none;
background-color: #f0dcb1;
color: #1b191a;
border: 1px solid #1b191a;
padding: 5px;
cursor: pointer;
width: 20px;
height: 20px;
border-radius: 50%;
user-select: none;
transition: background-color 0.3s ease;
}
.userCardWrapper:hover .dropButton {
display: block;
box-shadow: 2px 2px 8px rgba(0, 0, 0, 1);
}
.dropButton:hover {
background-color: #ddcba3;
}
.dropButton:active {
background-color: #ddcba3;
}
.userCardWrapper:hover .dropButton:active {
transform: translate(-50%, 0%) scale(0.95);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
}
.votingTable .form {
display: flex;
justify-content: center;
}
.votingTable .form .button {
background-color: #f0dcb1;
border: 1px solid #1b191a;
border-radius: 4px;
color: #1b191a;
text-shadow: none;
padding: 4px;
cursor: pointer;
font-weight: 800;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
transition: background-color 0.3s ease;
}
.votingTable .form .button:hover {
background-color: #cdba95;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 1);
}
.votingTable .form .button:active {
background-color: #f0dcb1;
transform: scale(0.95);
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import VotingTable from './VotingTable';
import { generateUniqueId } from '../../utils/crypto';
export default {
title: 'Components/VotingTable',
component: VotingTable,
};
export const Default = () => {
const mockSessionId = generateUniqueId();
const mockUsername = 'testUser';
return (
<MemoryRouter initialEntries={[`/session/${mockSessionId}`]}>
<Routes>
<Route
path="/session/:sessionId"
element={<VotingTable username={mockUsername} />}
/>
</Routes>
</MemoryRouter>
);
};

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import Lobby from './VotingTable';
test('renders voting Lobby', () => {
const mockOnJoinSession = jest.fn();
render(<Lobby onJoinSession={mockOnJoinSession} />);
expect(screen.getByText(/5: 2 vote\(s\)/i)).toBeInTheDocument();
});

View File

@@ -2,15 +2,15 @@ import { useState, useEffect } from 'react';
import { fetchVotes } from '../services/voteService'; import { fetchVotes } from '../services/voteService';
export const useVotes = () => { export const useVotes = () => {
const [votes, setVotes] = useState([]); const [votes, setVotes] = useState([]);
useEffect(() => { useEffect(() => {
fetchVotes().then(snapshot => { fetchVotes().then((snapshot) => {
if (snapshot.exists()) { if (snapshot.exists()) {
setVotes(snapshot.val()); setVotes(snapshot.val());
} }
}); });
}, []); }, []);
return votes; return votes;
}; };

View File

@@ -1,10 +1,63 @@
html {
height: 100%;
}
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family:
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'cinzel',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #f0dcb1;
text-shadow: 2px 2px #1b191a;
background: radial-gradient(ellipse at center, #1c4926ff 50%, #0d2412 100%);
height: 100%;
}
input {
width: 150px;
}
input,
select,
textarea {
border: 1px solid #1b191a;
}
input:focus,
select:focus,
textarea:focus,
button:focus {
outline-color: #1b191a;
outline: none;
/* border: 1px solid #1b191a; */
}
button {
font-family:
'cinzel',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
} }
code { code {

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import './assets/fonts/fonts.css';
import '@fontsource/cardo/400.css'; import '@fontsource/cardo/400.css';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));

View File

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

View File

@@ -0,0 +1,64 @@
// src/services/firebase-mock.js
export const initializeApp = () =>
console.log('initializeApp was called!!!!!!') || {};
// Mock Firestore
export const getFirestore = () =>
console.log('getFirestore was called!!!!!!') || {
collection: () => ({
doc: () => ({
get: () => Promise.resolve({ data: () => ({}) }),
set: () => Promise.resolve(),
}),
}),
};
// Mock Auth
export const getAuth = () =>
console.log('getAuth was called!!!!!!') || {
signInAnonymously: () => Promise.resolve({}),
};
// Mock Realtime Database
export const getDatabase = () =>
console.log('getDatabase was called!!!!!!') || {
ref: () => {
console.log('getDatabase REF was called!!!!!!');
return {
on: (eventType, callback) => {
// Mock implementation of 'on'
console.log(`Mock on called with event type: ${eventType}`);
},
off: () => {
// Mock implementation of 'off'
console.log('Mock off called to remove listeners');
},
set: (value) => {
console.log(`Mock set called with value: ${JSON.stringify(value)}`);
return Promise.resolve();
},
once: (eventType) => {
console.log(`Mock once called with event type: ${eventType}`);
return Promise.resolve({ val: () => ({}) });
},
};
},
};
// Mock Analytics
export const getAnalytics = () =>
console.log('getAnalytics was called!!!!!!') || {
logEvent: (eventName, eventParams) => {
console.log(`Mock logEvent called with event: ${eventName}`, eventParams);
},
setCurrentScreen: (screenName) => {
console.log(`Mock setCurrentScreen called with screen: ${screenName}`);
},
setUserId: (userId) => {
console.log(`Mock setUserId called with user ID: ${userId}`);
},
setUserProperties: (properties) => {
console.log(`Mock setUserProperties called with properties:`, properties);
},
};

View File

@@ -1,19 +1,45 @@
import { initializeApp } from "firebase/app"; import { initializeApp } from 'firebase/app';
import { getFirestore } from "firebase/firestore"; import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { getAuth } from "firebase/auth"; import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { getDatabase } from "firebase/database"; import { getDatabase, connectDatabaseEmulator } from 'firebase/database';
import { getAnalytics } from 'firebase/analytics';
const firebaseConfig = { const firebaseConfig = {
apiKey: "YOUR_API_KEY", apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: "YOUR_PROJECT_ID.firebaseapp.com", authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: "YOUR_PROJECT_ID", projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: "YOUR_PROJECT_ID.appspot.com", storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: "YOUR_MESSAGING_SENDER_ID", messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: "YOUR_APP_ID", appId: process.env.REACT_APP_FIREBASE_APP_ID,
databaseURL: "YOUR_DATABASE_URL" measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
}; };
// Initialize Firebase
const app = initializeApp(firebaseConfig); const app = initializeApp(firebaseConfig);
// Initialize Firebase services
export const db = getFirestore(app); export const db = getFirestore(app);
export const auth = getAuth(app); export const auth = getAuth(app);
export const realtimeDb = getDatabase(app); export const realtimeDb = getDatabase(app);
export let analytics;
// Detect if running in Firebase Hosting Emulator
const isEmulator =
window.location.hostname === 'localhost' && window.location.port === '5000';
if (process.env.NODE_ENV === 'development' || isEmulator) {
// Connect to Firebase Emulators in development environment (including Storybook)
connectFirestoreEmulator(db, 'localhost', 8080);
connectAuthEmulator(auth, 'http://localhost:9099');
connectDatabaseEmulator(realtimeDb, 'localhost', 9000);
analytics = {
logEvent: (eventName, eventParams) => {
console.log(`Analytics event: ${eventName}`, eventParams);
},
};
} else {
analytics = getAnalytics(app);
}

View File

@@ -1,11 +1,11 @@
import { ref, set, get } from 'firebase/database'; import { ref, set, get } from 'firebase/database';
import { realtimeDb } from './firebase'; import { realtimeDb } from '../__mocks__/firebase';
export const submitVote = (userId, voteValue) => { export const submitVote = (userId, voteValue) => {
return set(ref(realtimeDb, `votes/${userId}`), voteValue); return set(ref(realtimeDb, `votes/${userId}`), voteValue);
}; };
export const fetchVotes = () => { export const fetchVotes = () => {
const votesRef = ref(realtimeDb, 'votes/'); const votesRef = ref(realtimeDb, 'votes/');
return get(votesRef); return get(votesRef);
}; };

View File

@@ -6,11 +6,15 @@ import './button.css';
* Primary UI component for user interaction * Primary UI component for user interaction
*/ */
export const Button = ({ primary, backgroundColor, size, label, ...props }) => { export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; const mode = primary
? 'storybook-button--primary'
: 'storybook-button--secondary';
return ( return (
<button <button
type="button" type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} className={['storybook-button', `storybook-button--${size}`, mode].join(
' '
)}
style={backgroundColor && { backgroundColor }} style={backgroundColor && { backgroundColor }}
{...props} {...props}
> >

View File

@@ -8,7 +8,12 @@ export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => (
<header> <header>
<div className="storybook-header"> <div className="storybook-header">
<div> <div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> <svg
width="32"
height="32"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd"> <g fill="none" fillRule="evenodd">
<path <path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
@@ -37,7 +42,12 @@ export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => (
) : ( ) : (
<> <>
<Button size="small" onClick={onLogin} label="Log in" /> <Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" /> <Button
primary
size="small"
onClick={onCreateAccount}
label="Sign up"
/>
</> </>
)} )}
</div> </div>

View File

@@ -19,40 +19,58 @@ export const Page = () => {
<h2>Pages in Storybook</h2> <h2>Pages in Storybook</h2>
<p> <p>
We recommend building UIs with a{' '} We recommend building UIs with a{' '}
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> <a
href="https://componentdriven.org"
target="_blank"
rel="noopener noreferrer"
>
<strong>component-driven</strong> <strong>component-driven</strong>
</a>{' '} </a>{' '}
process starting with atomic components and ending with pages. process starting with atomic components and ending with pages.
</p> </p>
<p> <p>
Render pages with mock data. This makes it easy to build and review page states without Render pages with mock data. This makes it easy to build and review
needing to navigate to them in your app. Here are some handy patterns for managing page page states without needing to navigate to them in your app. Here are
data in Storybook: some handy patterns for managing page data in Storybook:
</p> </p>
<ul> <ul>
<li> <li>
Use a higher-level connected component. Storybook helps you compose such data from the Use a higher-level connected component. Storybook helps you compose
"args" of child component stories such data from the "args" of child component stories
</li> </li>
<li> <li>
Assemble data in the page component from your services. You can mock these services out Assemble data in the page component from your services. You can mock
using Storybook. these services out using Storybook.
</li> </li>
</ul> </ul>
<p> <p>
Get a guided tutorial on component-driven development at{' '} Get a guided tutorial on component-driven development at{' '}
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> <a
href="https://storybook.js.org/tutorials/"
target="_blank"
rel="noopener noreferrer"
>
Storybook tutorials Storybook tutorials
</a> </a>
. Read more in the{' '} . Read more in the{' '}
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> <a
href="https://storybook.js.org/docs"
target="_blank"
rel="noopener noreferrer"
>
docs docs
</a> </a>
. .
</p> </p>
<div className="tip-wrapper"> <div className="tip-wrapper">
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '} <span className="tip">Tip</span> Adjust the width of the canvas with
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> the{' '}
<svg
width="10"
height="10"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd"> <g fill="none" fillRule="evenodd">
<path <path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"

9
src/utils/crypto.js Normal file
View File

@@ -0,0 +1,9 @@
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
};

110
src/utils/mockThis.js Normal file
View File

@@ -0,0 +1,110 @@
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;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
/*!
* isobject <https://github.com/jonschlinkert/isobject>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/**
* @license
* Lodash (Custom Build) <https://lodash.com/>
* Build: `lodash modularize exports="es" -o ./`
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkfibonacci_fold=self.webpackChunkfibonacci_fold||[]).push([[294],{"./node_modules/@mdx-js/react/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{MDXProvider:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.x,useMDXComponents:()=>_lib_index_js__WEBPACK_IMPORTED_MODULE_0__.R});var _lib_index_js__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@mdx-js/react/lib/index.js")},"./node_modules/@mdx-js/react/lib/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{R:()=>useMDXComponents,x:()=>MDXProvider});var react__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/react/index.js");const emptyComponents={},MDXContext=react__WEBPACK_IMPORTED_MODULE_0__.createContext(emptyComponents);function useMDXComponents(components){const contextComponents=react__WEBPACK_IMPORTED_MODULE_0__.useContext(MDXContext);return react__WEBPACK_IMPORTED_MODULE_0__.useMemo((function(){return"function"==typeof components?components(contextComponents):{...contextComponents,...components}}),[contextComponents,components])}function MDXProvider(properties){let allComponents;return allComponents=properties.disableParentContext?"function"==typeof properties.components?properties.components(emptyComponents):properties.components||emptyComponents:useMDXComponents(properties.components),react__WEBPACK_IMPORTED_MODULE_0__.createElement(MDXContext.Provider,{value:allComponents},properties.children)}}}]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkfibonacci_fold=self.webpackChunkfibonacci_fold||[]).push([[364],{"./node_modules/storybook/core/components/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{createCopyToClipboardFunction:()=>_storybook_core_components__WEBPACK_IMPORTED_MODULE_0__.zH});var _storybook_core_components__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@storybook/core/dist/components/index.js")}}]);

View File

@@ -0,0 +1 @@
(self.webpackChunkfibonacci_fold=self.webpackChunkfibonacci_fold||[]).push([[438],{"./node_modules/@storybook/addon-docs/dist/DocsRenderer-PKQXORMH.mjs":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{"use strict";__webpack_require__.d(__webpack_exports__,{DocsRenderer:()=>DocsRenderer});var react=__webpack_require__("./node_modules/react/index.js"),react_18=__webpack_require__("./node_modules/@storybook/react-dom-shim/dist/react-18.mjs"),dist=__webpack_require__("./node_modules/@storybook/blocks/dist/index.mjs"),defaultComponents={code:dist.XA,a:dist.zE,...dist.Sw},ErrorBoundary=class extends react.Component{constructor(){super(...arguments),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(err){let{showException}=this.props;showException(err)}render(){let{hasError}=this.state,{children}=this.props;return hasError?null:react.createElement(react.Fragment,null,children)}},DocsRenderer=class{constructor(){this.render=async(context,docsParameter,element)=>{let components={...defaultComponents,...docsParameter?.components},TDocs=dist.kQ;return new Promise(((resolve,reject)=>{__webpack_require__.e(294).then(__webpack_require__.bind(__webpack_require__,"./node_modules/@mdx-js/react/index.js")).then((({MDXProvider})=>(0,react_18.d)(react.createElement(ErrorBoundary,{showException:reject,key:Math.random()},react.createElement(MDXProvider,{components},react.createElement(TDocs,{context,docsParameter}))),element))).then((()=>resolve()))}))},this.unmount=element=>{(0,react_18.H)(element)}}};__webpack_require__("./node_modules/@storybook/addon-docs/dist/chunk-H6MOWX77.mjs")},"./node_modules/@storybook/core/dist/components sync recursive":module=>{function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=()=>[],webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/core/dist/components sync recursive",module.exports=webpackEmptyContext},"./node_modules/@storybook/core/dist/theming sync recursive":module=>{function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=()=>[],webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/@storybook/core/dist/theming sync recursive",module.exports=webpackEmptyContext},"./node_modules/memoizerific sync recursive":module=>{function webpackEmptyContext(req){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}webpackEmptyContext.keys=()=>[],webpackEmptyContext.resolve=webpackEmptyContext,webpackEmptyContext.id="./node_modules/memoizerific sync recursive",module.exports=webpackEmptyContext}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Some files were not shown because too many files have changed in this diff Show More