MVP
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
16
.eslintrc.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
.firebase/hosting.YnVpbGQ.cache
Normal 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
|
||||||
@@ -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
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
9
database-debug.log
Normal 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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
31
package.json
@@ -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": "/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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…</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
@@ -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
|
||||||
92
src/App.jsx
@@ -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;
|
||||||
|
|||||||
BIN
src/assets/fonts/Blackmild Regular/Blackmild Regular.otf
Executable file
BIN
src/assets/fonts/Cardo/Cardo-Bold.ttf
Normal file
BIN
src/assets/fonts/Cardo/Cardo-Italic.ttf
Normal file
BIN
src/assets/fonts/Cardo/Cardo-Regular.ttf
Normal file
BIN
src/assets/fonts/Cinzel/Cinzel-VariableFont_wght.ttf
Normal file
22
src/assets/fonts/fonts.css
Normal 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;
|
||||||
|
}
|
||||||
BIN
src/assets/images/2cards.xcf
Normal file
BIN
src/assets/images/applogo.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
src/assets/images/applogo.xcf
Normal file
BIN
src/assets/images/card.png
Normal file
|
After Width: | Height: | Size: 379 KiB |
BIN
src/assets/images/card.xcf
Normal file
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/images/cardback.xcf
Normal file
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -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 |
4
src/assets/images/exit.svg
Normal 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 |
@@ -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 |
BIN
src/assets/images/roundtable.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
src/assets/images/roundtable.xcf
Normal file
BIN
src/assets/images/titlecard.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/titlecard.xcf
Normal file
26
src/components/BoardCard/BoardCard.jsx
Normal 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;
|
||||||
73
src/components/BoardCard/BoardCard.module.css
Normal 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 */
|
||||||
|
}
|
||||||
9
src/components/BoardCard/BoardCard.stories.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import BoardCard from './BoardCard';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/BoardCard',
|
||||||
|
component: BoardCard,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => <BoardCard />;
|
||||||
@@ -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;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import CardBack from './CardBack';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/CardBack',
|
|
||||||
component: CardBack,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default = () => <CardBack />;
|
|
||||||
18
src/components/FrontCard/FrontCard.jsx
Normal 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;
|
||||||
30
src/components/FrontCard/FrontCard.module.css
Normal 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;
|
||||||
|
}
|
||||||
31
src/components/FrontCard/FrontCard.stories.js
Normal 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 },
|
||||||
|
};
|
||||||
114
src/components/Lobby/Lobby.jsx
Normal 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;
|
||||||
67
src/components/Lobby/Lobby.module.css
Normal 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;
|
||||||
|
}
|
||||||
14
src/components/Lobby/Lobby.stories.js
Normal 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>
|
||||||
|
);
|
||||||
8
src/components/Lobby/Lobby.test.js
Normal 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();
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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) => {
|
||||||
@@ -10,13 +11,19 @@ const Results = ({ votes }) => {
|
|||||||
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>
|
||||||
|
{' : '}
|
||||||
|
{voteCounts[vote]} vote(s)
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,26 @@
|
|||||||
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;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 10px 10px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results .voteValueContainer {
|
||||||
|
max-width: 2rem;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results .voteValueContainer .voteValue {
|
||||||
|
transform: scale(0.8);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|||||||
46
src/components/UserSessionListener/UserSessionListener.jsx
Normal 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;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
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}
|
||||||
|
/>
|
||||||
|
<div className={`${styles.shadow} ${isSelected && styles.selected}`} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,60 @@
|
|||||||
.voteCard {
|
.voteCard {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 75px;
|
height: 75px;
|
||||||
display: inline-block;
|
position: relative;
|
||||||
padding: .1rem;
|
|
||||||
margin: 10px;
|
padding: 0;
|
||||||
background-color: #f0f0f0;
|
margin: 5px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border: 2px solid #0095ff;
|
border: 0;
|
||||||
border-radius: .7rem;
|
background: transparent;
|
||||||
transition: border 0.2s ease-in-out, margin 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
|
}
|
||||||
|
|||||||
23
src/components/VoteValue/VoteValue.jsx
Normal 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;
|
||||||
30
src/components/VoteValue/VoteValue.module.css
Normal 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; */
|
||||||
|
}
|
||||||
@@ -1,30 +1,60 @@
|
|||||||
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);
|
|
||||||
|
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 (
|
return (
|
||||||
<div className={styles.votingBoard}>
|
<div className={styles.votingBoard}>
|
||||||
<h2>Select Your Vote</h2>
|
<h2>Select Your Vote</h2>
|
||||||
<div className={styles.voteCards}>
|
<div className={styles.voteCards}>
|
||||||
{fibonacciNumbers.map(num => (
|
{fibonacciNumbers.map((num) => (
|
||||||
<VoteCard
|
<VoteCard
|
||||||
key={num}
|
key={num}
|
||||||
|
className={styles.voteCard}
|
||||||
number={num}
|
number={num}
|
||||||
onClick={() => handleVote(num)}
|
onClick={() => handleVote(num)}
|
||||||
isSelected={selectedVote === num}
|
isSelected={selectedVote === num}
|
||||||
isCoffee={num === 0}
|
|
||||||
isInfinity={num === 9999}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import VotingBoard from './VotingBoard';
|
import VotingBoard from './VotingBoard';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
294
src/components/VotingTable/VotingTable.jsx
Normal 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;
|
||||||
125
src/components/VotingTable/VotingTable.module.css
Normal 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);
|
||||||
|
}
|
||||||
25
src/components/VotingTable/VotingTable.stories.js
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
src/components/VotingTable/VotingTable.test.js
Normal 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();
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ 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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
64
src/services/firebase-mock.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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);
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
95
storybook-static/126.25a96a85.iframe.bundle.js
Normal file
16
storybook-static/126.25a96a85.iframe.bundle.js.LICENSE.txt
Normal 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
|
||||||
|
*/
|
||||||
1
storybook-static/126.25a96a85.iframe.bundle.js.map
Normal file
BIN
storybook-static/161528bb6c25115b3f83.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
1
storybook-static/294.e1ff8b2a.iframe.bundle.js
Normal 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)}}}]);
|
||||||
BIN
storybook-static/2dbc69731c3f9930a271.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
1
storybook-static/364.19a0c543.iframe.bundle.js
Normal 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")}}]);
|
||||||
1
storybook-static/438.5ee504b7.iframe.bundle.js
Normal 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}}]);
|
||||||
1
storybook-static/461.ebf4beba.iframe.bundle.js
Normal file
2
storybook-static/481.8b61055c.iframe.bundle.js
Normal file
77
storybook-static/481.8b61055c.iframe.bundle.js.LICENSE.txt
Normal 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.
|
||||||
|
*/
|
||||||
6
storybook-static/7.d4d44a12.iframe.bundle.js
Normal file
9
storybook-static/7.d4d44a12.iframe.bundle.js.LICENSE.txt
Normal 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.
|
||||||
|
*/
|
||||||
1
storybook-static/7.d4d44a12.iframe.bundle.js.map
Normal file
BIN
storybook-static/7a58d2cb9a6358f85e9b.png
Normal file
|
After Width: | Height: | Size: 456 KiB |
402
storybook-static/850.58361dab.iframe.bundle.js
Normal file
46
storybook-static/850.58361dab.iframe.bundle.js.LICENSE.txt
Normal 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.
|
||||||
|
*/
|
||||||
1
storybook-static/850.58361dab.iframe.bundle.js.map
Normal file
BIN
storybook-static/9335a1a91b80ad4fec70.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
storybook-static/c720ced26387af8a9cb2.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
@@ -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.
|
||||||
|
*/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/*! For license information please see components-Results-Results-stories.d034fb3c.iframe.bundle.js.LICENSE.txt */
|
||||||
|
"use strict";(self.webpackChunkfibonacci_fold=self.webpackChunkfibonacci_fold||[]).push([[571],{"./src/components/Results/Results.stories.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{Default:()=>Default,__namedExportsOrder:()=>__namedExportsOrder,default:()=>Results_stories});__webpack_require__("./node_modules/react/index.js");const Results_module_results="Results_results__nJhmZ";var jsx_runtime=__webpack_require__("./node_modules/react/jsx-runtime.js");const Results=_ref=>{let{votes}=_ref;const voteCounts=Object.values(votes).reduce(((acc,vote)=>(acc[vote]=(acc[vote]||0)+1,acc)),{});return(0,jsx_runtime.jsxs)("div",{className:Results_module_results,children:[(0,jsx_runtime.jsx)("h2",{children:"Voting Results"}),(0,jsx_runtime.jsx)("ul",{children:Object.keys(voteCounts).map(((vote,index)=>(0,jsx_runtime.jsxs)("li",{children:[vote,": ",voteCounts[vote]," vote(s)"]},index)))})]})},Results_Results=Results;Results.__docgenInfo={description:"",methods:[],displayName:"Results"};const Results_stories={title:"Components/Results",component:Results_Results},mockVotes={user1:5,user2:8,user3:5},Default=()=>(0,jsx_runtime.jsx)(Results_Results,{votes:mockVotes}),__namedExportsOrder=["Default"];Default.parameters={...Default.parameters,docs:{...Default.parameters?.docs,source:{originalSource:"() => <Results votes={mockVotes} />",...Default.parameters?.docs?.source}}}},"./node_modules/react/cjs/react-jsx-runtime.production.min.js":(__unused_webpack_module,exports,__webpack_require__)=>{var f=__webpack_require__("./node_modules/react/index.js"),k=Symbol.for("react.element"),l=Symbol.for("react.fragment"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};function q(c,a,g){var b,d={},e=null,h=null;for(b in void 0!==g&&(e=""+g),void 0!==a.key&&(e=""+a.key),void 0!==a.ref&&(h=a.ref),a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l,exports.jsx=q,exports.jsxs=q},"./node_modules/react/jsx-runtime.js":(module,__unused_webpack_exports,__webpack_require__)=>{module.exports=__webpack_require__("./node_modules/react/cjs/react-jsx-runtime.production.min.js")}}]);
|
||||||
@@ -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.
|
||||||
|
*/
|
||||||
@@ -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.
|
||||||
|
*/
|
||||||