├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── iPad-Pro.png ├── iPad-Retina.png ├── older-iPhone.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── iPhone-6-Plus.png ├── meta_og_image.png ├── manifest.json └── index.html ├── .prettierrc ├── src ├── setupTests.js ├── constants.js ├── index.css ├── index.js ├── components │ ├── BlackCardDrop.js │ ├── BlankCard.js │ ├── CardWrap.js │ ├── BlankPlayerCard.js │ ├── ChatButton.js │ ├── EmptyPage.js │ ├── App.js │ ├── ChooseADeck.js │ ├── PlayerInfo.js │ ├── PrivacyCheck.js │ ├── InputWithLabel.js │ ├── Table.js │ ├── PublicGames.js │ ├── PlayerDrop.js │ ├── HowToPlay.js │ ├── NamePopup.js │ ├── Admin.js │ ├── CreateADeck.js │ ├── DraggableCard.js │ ├── ChatBox.js │ ├── Landing.js │ ├── CreateGame.js │ ├── MyCardsDropZone.js │ ├── Game.css │ └── EditADeck.js ├── card.js └── serviceWorker.js ├── .gitignore ├── README.md ├── .github └── FUNDING.yml ├── package.json └── LICENSE.txt /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/iPad-Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/iPad-Pro.png -------------------------------------------------------------------------------- /public/iPad-Retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/iPad-Retina.png -------------------------------------------------------------------------------- /public/older-iPhone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/older-iPhone.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/iPhone-6-Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/iPhone-6-Plus.png -------------------------------------------------------------------------------- /public/meta_og_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdennett55/cards-of-personality-frontend/HEAD/public/meta_og_image.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "bracketSpacing": false, 5 | "tabWidth": 2, 6 | "proseWrap": "always", 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const SERVER_URL = 2 | process.env.NODE_ENV === "development" 3 | ? "http://localhost:3001" 4 | : "https://cards-of-personality-backend.onrender.com"; 5 | 6 | export const CLIENT_URL = 7 | process.env.NODE_ENV === "development" 8 | ? "http://localhost:3000" 9 | : "https://cardsofpersonality.com"; 10 | 11 | export const MAX_PLAYERS = 8; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cards of Personality (frontend) 2 | A mobile-friendly online multiplayer web game inspired by the popular Cards Against Humanity game. 3 | 4 | ## Try it out! 5 | https://cardsofpersonality.com 6 | 7 | ## Backend 8 | https://github.com/sdennett55/cards-of-personality-backend 9 | 10 | Buy Me A Coffee 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | font-family: 'Roboto Condensed', Arial, sans-serif; 9 | box-sizing: border-box; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Cards of Personality", 3 | "name": "Cards of Personality", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import "typeface-roboto-condensed"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /src/components/BlackCardDrop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { useDrop } from 'react-dnd'; 4 | 5 | const BlackCardDropElem = styled.div` 6 | position: absolute; 7 | width: 100%; 8 | height: 100%; 9 | `; 10 | 11 | const BlackCardDrop = ({addBlackCardBackToPile, children}) => { 12 | const [, drop] = useDrop({ 13 | accept: 'blackCardFromPlayer', 14 | drop: (item) => { 15 | addBlackCardBackToPile(item); 16 | }, 17 | }); 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | export default BlackCardDrop; 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: cardsofpersonality 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /src/components/BlankCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const CardElement = styled.div` 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | border-radius: 8px; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | padding: 1em; 15 | border: 2px dashed #fff; 16 | color: #fff; 17 | `; 18 | 19 | const Wrapper = styled.div` 20 | position: relative; 21 | padding-bottom: calc(140% + 4px); 22 | `; 23 | 24 | const BlankCard = ({children}) => { 25 | 26 | return ( 27 |
28 | 29 | 30 | {children} 31 | 32 | 33 |
34 | 35 | ) 36 | } 37 | 38 | export default BlankCard; 39 | -------------------------------------------------------------------------------- /src/card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const CardElement = styled.div` 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | border-radius: 8px; 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | padding: 1em; 15 | border: 2px solid #000; 16 | 17 | .MyCardsContainer & { 18 | border-color: #2cce9f; 19 | } 20 | `; 21 | 22 | const Wrapper = styled.div` 23 | position: relative; 24 | padding-bottom: calc(140% + 4px); 25 | `; 26 | 27 | const Card = ({ bgColor, color, text }) => { 28 | 29 | return ( 30 |
31 | 32 | 33 | {text} 34 | 35 | 36 |
37 | 38 | ) 39 | } 40 | 41 | export default Card; 42 | -------------------------------------------------------------------------------- /src/components/CardWrap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrap = styled.div` 5 | position: relative; 6 | padding-bottom: 140%; 7 | color: black; 8 | border-radius: 8px; 9 | border: 2px dashed #fff; 10 | `; 11 | 12 | const PickUpPileWrap = styled.div` 13 | position: relative; 14 | padding-bottom: 140%; 15 | color: black; 16 | border-radius: 8px; 17 | background: #fff; 18 | font-size: 13px; 19 | `; 20 | 21 | const PickUpPileWrapper = styled.div` 22 | position: relative; 23 | width: calc(50% - .5em); 24 | max-width: calc(100vh - 50px - 6em); 25 | 26 | @media (max-width: 500px) and (orientation: portrait) { 27 | max-width: 25vh; 28 | } 29 | `; 30 | 31 | const DefaultWrapper = styled.div` 32 | position: relative; 33 | width: 150px; 34 | margin: .5em; 35 | `; 36 | 37 | const CardWrap = React.memo(({ children, innerRef, isPickUpPile, className }) => { 38 | const WrappingElement = isPickUpPile ? PickUpPileWrap : Wrap; 39 | const Wrapper = isPickUpPile ? PickUpPileWrapper : DefaultWrapper; 40 | return ( 41 | 42 | 43 | {children} 44 | 45 | 46 | ); 47 | }); 48 | 49 | export default CardWrap 50 | -------------------------------------------------------------------------------- /src/components/BlankPlayerCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { MAX_PLAYERS } from "../constants"; 4 | 5 | const CardElement = styled.div` 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | bottom: 0; 11 | border-radius: 8px; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | padding: 1em; 16 | border: 2px dashed #000; 17 | color: #000; 18 | @media screen and (min-width: 1100px) { 19 | font-size: 16px; 20 | } 21 | `; 22 | 23 | const Wrapper = styled.div` 24 | position: relative; 25 | padding-bottom: 140%; 26 | `; 27 | const BlankCard = styled.div` 28 | position: relative; 29 | width: calc(25% - 1em); 30 | margin: .5em; 31 | 32 | @media (max-width: 500px) and (orientation: portrait) { 33 | max-width: calc(25vh - 1em - 50px); 34 | } 35 | 36 | @media (orientation: landscape) { 37 | max-width: calc(50vh - 50px - 4em); 38 | } 39 | `; 40 | 41 | const BlankPlayerCard = ({count, index, className}) => { 42 | 43 | return ( 44 | 45 | 46 | 47 | Player {count < MAX_PLAYERS ? count + index + 1 : index + 1} 48 | 49 | 50 | 51 | 52 | ) 53 | } 54 | 55 | export default BlankPlayerCard; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cards-fun", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.2", 10 | "classnames": "^2.2.6", 11 | "query-string": "^6.12.1", 12 | "react": "^16.13.1", 13 | "react-dnd": "^10.0.2", 14 | "react-dnd-touch-backend": "^10.0.2", 15 | "react-dom": "^16.13.1", 16 | "react-ga": "^3.0.0", 17 | "react-helmet": "^6.0.0", 18 | "react-router": "^5.2.0", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "3.4.1", 21 | "react-swipeable": "^5.5.1", 22 | "react-toastify": "^6.0.6", 23 | "reactour": "^1.18.0", 24 | "socket.io-client": "^2.3.0", 25 | "styled-components": "^5.0.1", 26 | "typeface-roboto-condensed": "^0.0.75" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "CI=false react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": "react-app" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ChatButton.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { ChatIcon } from "../icons"; 3 | import styled from "styled-components"; 4 | 5 | const pageTitle = document.title; 6 | 7 | const ChatButton = ({socket, myName, chatOpen, setChatOpen, unreadCount, setUnreadCount}) => { 8 | useEffect(() => { 9 | if (unreadCount) { 10 | document.title = `(${unreadCount}) ${pageTitle}`; 11 | } else { 12 | document.title = pageTitle; 13 | } 14 | }, [unreadCount]) 15 | return ( 16 | <> 17 | setChatOpen(true)}> 18 | 19 | {unreadCount > 0 && {unreadCount}} 20 | 21 | 22 | ); 23 | }; 24 | 25 | const ChatIconWrap = styled.button` 26 | position: relative; 27 | appearance: none; 28 | border: 0; 29 | background: #000; 30 | padding: 0.5em; 31 | width: 50px; 32 | height: 50px; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | transition: opacity 0.25s; 37 | color: #fff; 38 | 39 | &:hover, 40 | &:focus { 41 | opacity: 0.5; 42 | } 43 | &:focus { 44 | outline: 0; 45 | } 46 | 47 | @media (min-width: 1600px) { 48 | border-radius: 0 8px 0 0; 49 | } 50 | `; 51 | 52 | const Notification = styled.div` 53 | position: absolute; 54 | top: 8px; 55 | right: 8px; 56 | border-radius: 50%; 57 | background: #ff0080; 58 | width: 16px; 59 | height: 16px; 60 | font-size: 0.75rem; 61 | color: #fff; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | `; 66 | 67 | export default ChatButton; 68 | -------------------------------------------------------------------------------- /src/components/EmptyPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled, {createGlobalStyle} from "styled-components"; 4 | 5 | const EmptyPage = () => { 6 | return ( 7 | 8 | 9 | Not sure how you got here, but this page doesn't exist. 10 | 11 | Back to homepage. 12 | 13 | 14 | ); 15 | }; 16 | 17 | const GlobalStyle = createGlobalStyle` 18 | body { 19 | text-align: center; 20 | padding: 2em; 21 | background: #000; 22 | color: #fff; 23 | border: 1em solid; 24 | border-image: linear-gradient(90deg, rgb(64,224,208), rgb(255,140,0), rgb(255,0,128) ) 1; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | } 29 | `; 30 | const Page = styled.div` 31 | font-size: 2em; 32 | ` 33 | const Text = styled.p` 34 | margin: .5em 0; 35 | `; 36 | const LinkElem = styled(Link)` 37 | position: relative; 38 | display: inline-block; 39 | color: #fff; 40 | background: linear-gradient( 41 | to right, 42 | rgb(64, 224, 208), 43 | rgb(255, 140, 0), 44 | rgb(255, 0, 128) 45 | ); 46 | -webkit-text-fill-color: transparent; 47 | -webkit-background-clip: text; 48 | 49 | &::after { 50 | content: ""; 51 | width: 100%; 52 | position: absolute; 53 | bottom: -3px; 54 | left: 0; 55 | height: 2px; 56 | background: linear-gradient( 57 | to right, 58 | rgb(64, 224, 208), 59 | rgb(255, 140, 0), 60 | rgb(255, 0, 128) 61 | ); 62 | transition: opacity .25s; 63 | } 64 | 65 | &:hover, 66 | &:focus { 67 | &::after { 68 | opacity: 0; 69 | } 70 | } 71 | `; 72 | 73 | export default EmptyPage; 74 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Landing from "./Landing"; 3 | import Game from "./Game"; 4 | import CreateADeck from "./CreateADeck"; 5 | import EditADeck from "./EditADeck"; 6 | import Admin from "./Admin"; 7 | import PlayerInfo from "./PlayerInfo"; 8 | import EmptyPage from "./EmptyPage"; 9 | import CreateGame from "./CreateGame"; 10 | import PublicGames from "./PublicGames"; 11 | import HowToPlay from "./HowToPlay"; 12 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 13 | import ReactGA from "react-ga"; 14 | 15 | class App extends React.Component { 16 | componentDidMount() { 17 | // initialize analytics 18 | if (process.env.NODE_ENV === "production") { 19 | this.initializeReactGA(); 20 | } 21 | } 22 | 23 | initializeReactGA = () => { 24 | ReactGA.initialize("UA-171045081-1"); 25 | ReactGA.pageview("/"); 26 | }; 27 | 28 | render() { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | 68 | export default App; 69 | -------------------------------------------------------------------------------- /src/components/ChooseADeck.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from "styled-components"; 3 | 4 | const ChooseADeck = ({ setDeck, title, loading, deck, toggle }) => { 5 | const handleSFWClick = () => { 6 | if (toggle) { 7 | setDeck(deck === 'safe-for-work' ? '' : 'safe-for-work'); 8 | return; 9 | } 10 | 11 | setDeck('safe-for-work'); 12 | } 13 | 14 | const handleNSFWClick = () => { 15 | if (toggle) { 16 | setDeck(deck === 'not-safe-for-work' ? '' : 'not-safe-for-work'); 17 | return; 18 | } 19 | 20 | setDeck('not-safe-for-work'); 21 | } 22 | 23 | return ( 24 | <> 25 | {title} 26 | 27 | 33 | Safe for Work 34 | 35 | 41 | Not Safe for Work 42 | 43 | 44 | 45 | ) 46 | }; 47 | 48 | const Flex = styled.div` 49 | display: flex; 50 | `; 51 | const StartTitle = styled.h2` 52 | color: #fff; 53 | margin: 0.5em 0 0; 54 | font-weight: normal; 55 | font-size: 1.5em; 56 | `; 57 | const PinkButton = styled.button` 58 | display: block; 59 | appearance: none; 60 | background: rgb(255, 0, 128); 61 | color: #000; 62 | font-size: 1em; 63 | border: 0; 64 | padding: 0.7em 1em; 65 | border-radius: 8px; 66 | margin: 1em 0.5em; 67 | font-weight: bold; 68 | transition: opacity 0.25s; 69 | 70 | &:hover, 71 | &:focus, 72 | &:disabled { 73 | opacity: 1; 74 | outline: 0; 75 | } 76 | `; 77 | const BlueButton = styled.button` 78 | display: block; 79 | appearance: none; 80 | background: rgb(64, 224, 208); 81 | color: #000; 82 | font-size: 1em; 83 | border: 0; 84 | padding: 0.7em 1em; 85 | border-radius: 8px; 86 | margin: 1em 0.5em; 87 | font-weight: bold; 88 | transition: opacity 0.25s; 89 | 90 | &:hover, 91 | &:focus, 92 | &:disabled { 93 | opacity: 1; 94 | outline: 0; 95 | } 96 | `; 97 | 98 | export default ChooseADeck; -------------------------------------------------------------------------------- /src/components/PlayerInfo.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import { SERVER_URL } from "../constants"; 4 | import styled from "styled-components"; 5 | import queryString from "query-string"; 6 | import axios from "axios"; 7 | 8 | const PlayerInfo = () => { 9 | const [data, setData] = useState({}); 10 | const [playerName, setPlayerName] = useState(""); 11 | const location = useLocation(); 12 | 13 | useEffect(() => { 14 | const { roomName, id } = queryString.parse(location.search); 15 | axios 16 | .get(`${SERVER_URL}/api/getPlayerInfo?roomName=${roomName}&id=${id}`) 17 | .then((res) => { 18 | console.log(res.data); 19 | setData(res.data); 20 | setPlayerName(res.data.name); 21 | }); 22 | }, [location]); 23 | 24 | return ( 25 | 26 | {playerName || "Unknown Player"}'s Stats 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | ))} 44 | 45 | 50 | 51 | 52 | ))} 53 | 54 | 55 | 56 |
White CardsBlack Cards
37 | {Object.keys(data).length > 0 && 38 | data.whiteCards && 39 | data.whiteCards.map((whiteCard) => ( 40 |
{whiteCard.text}
46 | {Object.keys(data).length > 0 && 47 | data.blackCards && 48 | data.blackCards.map((blackCard) => ( 49 |
{blackCard.text}
57 |
58 | ); 59 | }; 60 | 61 | const Table = styled.table` 62 | width: 100%; 63 | text-align: left; 64 | border-collapse: collapse; 65 | 66 | thead { 67 | background: #828282; 68 | border-radius: 8px 8px 0 0; 69 | color: white; 70 | th { 71 | padding: 0.5em; 72 | } 73 | } 74 | tbody > tr > td:first-child tr:nth-child(2n + 1) td { 75 | background: #dcdcdc; 76 | } 77 | 78 | tbody > tr > td:last-child tr:nth-child(2n + 2) td { 79 | background: #dcdcdc; 80 | } 81 | td { 82 | vertical-align: top; 83 | padding: 0.5em; 84 | width: 50%; 85 | } 86 | tbody > tr > td { 87 | padding: 0; 88 | } 89 | `; 90 | 91 | const Title = styled.h1` 92 | text-align: center; 93 | text-transform: uppercase; 94 | `; 95 | 96 | const Wrapper = styled.div` 97 | max-width: 1000px; 98 | margin: 0 auto; 99 | `; 100 | 101 | export default PlayerInfo; 102 | -------------------------------------------------------------------------------- /src/components/PrivacyCheck.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToastContainer, toast, Slide } from "react-toastify"; 3 | import { HelpIcon } from "../icons"; 4 | import styled, { createGlobalStyle } from "styled-components"; 5 | 6 | const PrivacyCheck = ({ setIsPrivate, isPrivate, title, toastText }) => { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | setIsPrivate((bool) => !bool)} 16 | checked={isPrivate} 17 | /> 18 | 19 | Make {title} private{" "} 20 | 23 | toast.info( 24 | toastText, 25 | { 26 | toastId: "private-toast", 27 | position: toast.POSITION.TOP_CENTER, 28 | } 29 | ) 30 | } 31 | > 32 | 33 | 34 | 35 | 36 | 37 | 45 | 46 | ) 47 | }; 48 | 49 | const GlobalStyle = createGlobalStyle` 50 | .Toastify__toast--info { 51 | background: #2cce9f; 52 | border-radius: 8px; 53 | color: #000; 54 | margin: 2em; 55 | font: inherit; 56 | } 57 | .Toastify__close-button { 58 | color: #000; 59 | } 60 | `; 61 | 62 | const Divider = styled.div` 63 | margin: 1em 0; 64 | `; 65 | 66 | const Flex = styled.div` 67 | display: flex; 68 | `; 69 | 70 | const PrivateLabel = styled.label` 71 | display: flex; 72 | align-items: center; 73 | color: #fff; 74 | font-size: 1.5em; 75 | `; 76 | 77 | const PublicCheckbox = styled.input` 78 | display: block; 79 | background: #fff; 80 | width: 20px; 81 | height: 20px; 82 | border-radius: 4px; 83 | border: 2px solid #fff; 84 | transition: background 0.25s; 85 | margin-right: 0.5em; 86 | 87 | &:focus { 88 | outline: 0; 89 | } 90 | &:checked { 91 | background: #2cce9f; 92 | } 93 | `; 94 | 95 | const IconWrap = styled.button` 96 | appearance: none; 97 | background: transparent; 98 | color: #fff; 99 | display: flex; 100 | align-items: center; 101 | margin: 0 0 0 0.25em; 102 | transition: opacity 0.25s, color 0.25s; 103 | padding: 0; 104 | 105 | &:hover, 106 | &:focus, 107 | &:disabled { 108 | color: #2cce9f; 109 | opacity: 0.5; 110 | outline: 0; 111 | } 112 | `; 113 | 114 | export default PrivacyCheck; 115 | -------------------------------------------------------------------------------- /src/components/InputWithLabel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Input = styled.input` 5 | appearance: none; 6 | font-size: 1em; 7 | border: 0; 8 | margin: 0 0 1em; 9 | padding: .5em 0 .3em; 10 | background: transparent; 11 | border-bottom: 1px solid #fff; 12 | transition: border-color .25s; 13 | border-radius: 0; 14 | color: #fff; 15 | 16 | ::placeholder { 17 | color: rgba(255, 255, 255, .8); 18 | } 19 | 20 | &:hover, 21 | &:focus { 22 | outline: 0; 23 | border-color:#2cce9f; 24 | } 25 | `; 26 | 27 | const Label = styled.label` 28 | text-align: left; 29 | text-transform: uppercase; 30 | font-size: .813em; 31 | display: block; 32 | font-weight: bold; 33 | color: #2cce9f; 34 | `; 35 | 36 | const BlackButton = styled.button` 37 | display: block; 38 | appearance: none; 39 | color: #000; 40 | font-size: 1em; 41 | border: 0; 42 | padding: .7em 1em; 43 | border-radius: 8px; 44 | margin: 0 auto; 45 | font-weight: bold; 46 | background: #000; 47 | border: 2px solid #2cce9f; 48 | color: #2cce9f; 49 | 50 | &:hover, 51 | &:focus, 52 | &:disabled { 53 | opacity: .5; 54 | outline:0; 55 | } 56 | `; 57 | 58 | const WhiteButton = styled.button` 59 | display: block; 60 | appearance: none; 61 | background: #fff; 62 | color: #000; 63 | font-size: 1em; 64 | border: 0; 65 | padding: .7em 1em; 66 | border-radius: 8px; 67 | margin: 0 auto; 68 | font-weight: bold; 69 | 70 | &:hover, 71 | &:focus, 72 | &:disabled { 73 | opacity: .5; 74 | outline:0; 75 | } 76 | `; 77 | 78 | const Wrapper = styled.div` 79 | display: flex; 80 | flex-direction: column; 81 | width: 100%; 82 | max-width: 270px; 83 | justify-content: center; 84 | margin: 2em auto; 85 | `; 86 | 87 | const InputWithLabel = ({ buttonText, labelText, onChange, type, placeholderText, blackCard, whiteCard, isLoading }) => { 88 | const inputRef = React.useRef(null); 89 | const hasMounted = React.useRef(false); 90 | const Button = type === 'white' ? WhiteButton : BlackButton; 91 | 92 | React.useEffect(() => { 93 | if (hasMounted.current && inputRef) { 94 | if (type === 'black' && blackCard === '') { 95 | inputRef.current.focus(); 96 | } else if (type === 'white' && whiteCard === '') { 97 | inputRef.current.focus(); 98 | } 99 | } 100 | hasMounted.current = true; 101 | }, [blackCard, whiteCard, type]); 102 | return ( 103 | 104 | 105 | onChange(e.target.value)} value={type === 'black' ? blackCard : whiteCard} /> 106 | 107 | 108 | ) 109 | } 110 | 111 | export default InputWithLabel; -------------------------------------------------------------------------------- /src/components/Table.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {PlusIcon, MinusIcon} from '../icons'; 4 | 5 | const Table = ({children, headers, color, isCollapsible}) => { 6 | const [isOpen, setIsOpen] = useState(false); 7 | const TableElem = color === 'white' ? WhiteTable : GreenTable; 8 | return ( 9 | 10 | 11 | 12 | {headers.map((header, index) => ( 13 | {header} 14 | ))} 15 | {isCollapsible && ( 16 | 17 | 24 | 25 | )} 26 | 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default Table; 36 | 37 | const Button = styled.button` 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 42px; 43 | appearance: none; 44 | text-align: right; 45 | transition: opacity 0.25s; 46 | background: transparent; 47 | padding: 0 1em; 48 | 49 | @media (hover) { 50 | &:hover, 51 | &:focus { 52 | outline: 0; 53 | opacity: 0.5; 54 | } 55 | } 56 | `; 57 | 58 | const WhiteTable = styled.table` 59 | position: relative; 60 | width: 100%; 61 | max-width: 600px; 62 | text-align: left; 63 | border-collapse: collapse; 64 | color: #000; 65 | border: 1px solid #fff; 66 | 67 | thead { 68 | color: #000; 69 | th { 70 | padding: 0.5em 1em; 71 | background: #fff; 72 | 73 | &:first-child { 74 | border-radius: 4px 0 0 0; 75 | } 76 | &:last-child { 77 | border-radius: 0 4px 0 0; 78 | } 79 | } 80 | } 81 | tbody tr:not(:first-child) { 82 | border-top: 1px solid #fff; 83 | } 84 | td { 85 | color: #fff; 86 | vertical-align: middle; 87 | } 88 | p { 89 | margin: 0.25em 0; 90 | } 91 | svg { 92 | display: inline-block; 93 | vertical-align: middle; 94 | } 95 | `; 96 | 97 | const GreenTable = styled.table` 98 | position: relative; 99 | width: 100%; 100 | max-width: 600px; 101 | text-align: left; 102 | border-collapse: collapse; 103 | color: #fff; 104 | border: 1px solid #2cce9f; 105 | 106 | thead { 107 | color: #000; 108 | th { 109 | padding: 0.5em 1em; 110 | background: #2cce9f; 111 | 112 | &:first-child { 113 | border-radius: 4px 0 0 0; 114 | } 115 | &:last-child { 116 | border-radius: 0 4px 0 0; 117 | } 118 | } 119 | } 120 | tbody tr:not(:first-child) { 121 | border-top: 1px solid #fff; 122 | } 123 | td { 124 | vertical-align: middle; 125 | } 126 | p { 127 | margin: 0.25em 0; 128 | } 129 | svg { 130 | display: inline-block; 131 | vertical-align: middle; 132 | } 133 | `; 134 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 42 | Cards of Personality 43 | 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/components/PublicGames.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import { SERVER_URL } from "../constants"; 4 | import { Link } from "react-router-dom"; 5 | import Table from "./Table"; 6 | import styled, { createGlobalStyle } from "styled-components"; 7 | 8 | const PublicGames = () => { 9 | const [data, setData] = useState({}); 10 | const [error, setError] = useState(""); 11 | useEffect(() => { 12 | axios 13 | .get(`${SERVER_URL}/api/getPublicRooms`) 14 | .then((res) => { 15 | setData(res.data); 16 | setError(""); 17 | }) 18 | .catch((err) => { 19 | setError("There was an error on the server. Please try again."); 20 | console.error(err); 21 | }); 22 | }, []); 23 | return ( 24 | 25 | 26 | Public Games 27 | 28 | {Object.keys(data).length > 0 && 29 | Object.entries(data).map(([roomName, { players }]) => ( 30 | 31 | 32 | 33 | 34 | Join Game 35 | 36 | 37 | ))} 38 | {Object.keys(data).length === 0 && ( 39 | 40 | 43 | 44 | )} 45 |
{roomName}{`${players.length}/8`}
41 | No games available. 42 |
46 | {error && {error}} 47 | Back 48 |
49 | ); 50 | }; 51 | 52 | const GlobalStyle = createGlobalStyle` 53 | body { 54 | text-align: center; 55 | background: #000; 56 | border: 1em solid; 57 | border-image: linear-gradient(90deg,rgb(64,224,208),rgb(255,140,0),rgb(255,0,128) ) 1; 58 | padding: 2em; 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: center; 62 | } 63 | button, 64 | input { 65 | appearance: none; 66 | border: 0; 67 | } 68 | `; 69 | 70 | const ErrorText = styled.p` 71 | color: red; 72 | font-size: 0.8rem; 73 | `; 74 | 75 | const Page = styled.div` 76 | min-height: 100%; 77 | overflow: auto; 78 | display: flex; 79 | flex-direction: column; 80 | align-items: center; 81 | justify-content: center; 82 | `; 83 | 84 | const WhiteButton = styled(Link)` 85 | display: block; 86 | appearance: none; 87 | background: #fff; 88 | color: #000; 89 | font-size: 1em; 90 | border: 0; 91 | padding: 0.7em 1em; 92 | border-radius: 8px; 93 | margin: 1em 0.5em; 94 | font-weight: bold; 95 | transition: opacity 0.25s; 96 | text-decoration: none; 97 | 98 | &:hover, 99 | &:focus, 100 | &:disabled { 101 | opacity: 0.5; 102 | outline: 0; 103 | } 104 | `; 105 | 106 | const GreenButton = styled(Link)` 107 | display: inline-block; 108 | appearance: none; 109 | background: #2cce9f; 110 | color: #000; 111 | font-size: 1em; 112 | border: 0; 113 | padding: 0.5em; 114 | margin: 0 auto; 115 | border-radius: 8px; 116 | font-weight: bold; 117 | transition: opacity 0.25s; 118 | text-decoration: none; 119 | text-align: center; 120 | 121 | &:hover, 122 | &:focus, 123 | &:disabled { 124 | opacity: 0.5; 125 | outline: 0; 126 | } 127 | `; 128 | const FlexCell = styled.td` 129 | display: flex; 130 | `; 131 | const StartTitle = styled.h2` 132 | color: #fff; 133 | margin: 1em; 134 | font-weight: normal; 135 | font-size: 2em; 136 | `; 137 | 138 | export default PublicGames; 139 | -------------------------------------------------------------------------------- /src/components/PlayerDrop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import {useDrop} from 'react-dnd'; 4 | import {MAX_PLAYERS} from '../constants'; 5 | import DraggableCard from './DraggableCard'; 6 | import {Confetti} from '../icons'; 7 | 8 | const PlayerName = styled.p` 9 | margin: 0px; 10 | position: absolute; 11 | top: 0; 12 | text-align: left; 13 | transform: translateY(-100%); 14 | color: black; 15 | font-weight: bold; 16 | width: 100%; 17 | padding: 0 0.5em 0.1em; 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | overflow: hidden; 21 | z-index: 1; 22 | @media screen and (min-width: 1100px) { 23 | font-size: 16px; 24 | } 25 | `; 26 | 27 | const CardElement = styled.div` 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | border-radius: 8px; 34 | color: black; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | padding: 1em; 39 | border: 2px dashed #000; 40 | transition: background 0.25s, transform 0.25s; 41 | `; 42 | 43 | const Wrap = styled.div` 44 | position: relative; 45 | padding-bottom: 140%; 46 | `; 47 | 48 | const PlayerDropWrap = styled.div` 49 | position: relative; 50 | width: calc(25% - 1em); 51 | margin: 0.5em; 52 | 53 | @media (max-width: 500px) and (orientation: portrait) { 54 | max-width: calc(25vh - 50px - 1em); 55 | } 56 | 57 | @media (orientation: landscape) { 58 | max-width: calc(50vh - 50px - 4em); 59 | } 60 | 61 | &:nth-child(1n + ${MAX_PLAYERS / 2 + 1}) ${PlayerName} { 62 | transform: translateY(100%); 63 | bottom: 0; 64 | top: auto; 65 | padding-top: 0.1em; 66 | } 67 | `; 68 | 69 | const getPlayerName = ({index, myName, players, socket}) => { 70 | if (players[index].id === socket.id) { 71 | return myName; 72 | } 73 | if (players[index].name) { 74 | return players[index].name; 75 | } 76 | 77 | return `NEW USER`; 78 | }; 79 | 80 | const getBlackCardLength = ({players, index}) => { 81 | if (players[index].blackCards && players[index].blackCards.length) { 82 | return `(${players[index].blackCards.length})`; 83 | } 84 | 85 | return ''; 86 | }; 87 | 88 | const PlayerDrop = ({ 89 | index, 90 | winningPlayerIndex, 91 | myName, 92 | players, 93 | socket, 94 | addCardToPlayer, 95 | userIsDragging, 96 | setUserIsDragging, 97 | className, 98 | isMyCardsOpen, 99 | isSubmittedTableOpen, 100 | }) => { 101 | const [{isOver}, drop] = useDrop({ 102 | accept: ['blackCard', 'blackCardFromPlayer'], 103 | drop: (item) => { 104 | addCardToPlayer(item, players[index]); 105 | }, 106 | collect: (monitor) => ({ 107 | isOver: !!monitor.isOver(), 108 | }), 109 | }); 110 | 111 | return ( 112 | 113 | 114 | 124 | {`${getBlackCardLength({ 125 | players, 126 | index, 127 | })} ${getPlayerName({myName, players, index, socket})}`} 128 | 129 | {index === winningPlayerIndex && } 130 | 131 | {players && 132 | players[index] && 133 | players[index].blackCards && 134 | players[index].blackCards.map((blackCard) => ( 135 |
145 | 156 |
157 | ))} 158 |
159 | ); 160 | }; 161 | 162 | export default PlayerDrop; 163 | -------------------------------------------------------------------------------- /src/components/HowToPlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import styled, {createGlobalStyle} from 'styled-components'; 4 | 5 | const HowToPlay = ({isPage, setIsVisible}) => { 6 | return ( 7 | 8 | 9 | How to play 10 | 11 | Note: This game works best with friends on audio/video chat. 12 | Also, there's an interactive tutorial you can run once you join 13 | a game. 14 | 15 | 16 | TL;DR: Drag and drop white cards on the bottom bar. Tapping 17 | bottom bar advances screens. 18 | 19 | 20 |
  • 21 | The first player to enter the room is the judge. They flip the big 22 | black card and read it out loud to start the round. 23 |
  • 24 |
  • 25 | Tap the bottom bar to view your deck. Everyone but the judge submits a 26 | single white card that fits the black card the best. 27 |
  • 28 |
  • 29 | Once everyone has submitted a card, everyone goes to the "Submitted 30 | Cards" screen by tapping the bottom bar within their deck where the 31 | judge proceeds to re-read the black card and then taps on each 32 | submitted card one at a time for all to admire or condemn.{' '} 33 |
  • 34 |
  • 35 | The judge announces a winner whereby all submitted white cards should 36 | be discarded and the winner can drag the big black card on the main 37 | screen to their player slot{' '} 38 |
  • 39 |
  • 40 | Each player that submitted a card must may another white card. Players 41 | can have 7 white cards in their deck at all times. 42 |
  • 43 |
  • 44 | The next judge then flips the black card to start the next round. 45 | First player to have 7 black cards in their player slot wins!{' '} 46 |
  • 47 |
    48 | {isPage ? ( 49 | Got it! 50 | ) : ( 51 | 54 | )} 55 |
    56 | ); 57 | }; 58 | 59 | const GlobalStyle = createGlobalStyle` 60 | body { 61 | text-align: center; 62 | background: #000; 63 | color: #fff; 64 | border: 1em solid; 65 | border-image: linear-gradient(90deg,rgb(64,224,208),rgb(255,140,0),rgb(255,0,128) ) 1; 66 | padding: 2em; 67 | display: flex; 68 | flex-direction: column; 69 | justify-content: center; 70 | } 71 | `; 72 | 73 | const TLDR = styled.p` 74 | margin: 2rem auto; 75 | font-size: 1.25rem; 76 | max-width: 350px; 77 | `; 78 | 79 | const Note = styled.p` 80 | margin: 1em 2em 0; 81 | font-style: italic; 82 | font-size: 1rem; 83 | `; 84 | 85 | const TLDRText = styled.span` 86 | color: rgb(255, 0, 128); 87 | font-weight: bold; 88 | `; 89 | 90 | const Wrapper = styled.div` 91 | display: flex; 92 | min-height: 100%; 93 | justify-content: center; 94 | align-items: center; 95 | flex-direction: column; 96 | margin: 0 auto; 97 | max-width: 600px; 98 | overflow: auto; 99 | text-align: center; 100 | `; 101 | 102 | const Title = styled.h2` 103 | margin: 0; 104 | font-weight: normal; 105 | color: rgb(255, 0, 128); 106 | font-size: 2em; 107 | `; 108 | 109 | const List = styled.ol` 110 | text-align: left; 111 | padding-left: 1.25em; 112 | margin-top: 0; 113 | 114 | li { 115 | margin: 1em 0; 116 | &:first-child { 117 | margin-top: 0; 118 | } 119 | } 120 | `; 121 | const Button = styled.button` 122 | appearance: none; 123 | font-weight: bold; 124 | background: #2cce9f; 125 | color: #000; 126 | font-size: 1em; 127 | border: 0; 128 | padding: 0.7em 1em; 129 | border-radius: 8px; 130 | margin: 0 auto; 131 | transition: opacity 0.25s; 132 | margin-top: 1em; 133 | background-color: rgb(255, 0, 128); 134 | 135 | &:hover, 136 | &:focus { 137 | opacity: 0.5; 138 | } 139 | &:focus { 140 | outline: 0; 141 | border-color: #2cce9f; 142 | } 143 | `; 144 | const LinkButton = styled(Link)` 145 | display: inline-block; 146 | font-weight: bold; 147 | appearance: none; 148 | background: #2cce9f; 149 | color: #000; 150 | font-size: 1em; 151 | border: 0; 152 | padding: 0.7em 1em; 153 | border-radius: 8px; 154 | transition: opacity 0.25s; 155 | margin: 1em 0 0; 156 | background-color: rgb(255, 0, 128); 157 | text-decoration: none; 158 | 159 | &:hover, 160 | &:focus { 161 | opacity: 0.5; 162 | } 163 | &:focus { 164 | outline: 0; 165 | border-color: #2cce9f; 166 | } 167 | `; 168 | 169 | export default HowToPlay; 170 | -------------------------------------------------------------------------------- /src/components/NamePopup.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { CLIENT_URL } from "../constants"; 3 | import { CopyIcon } from "../icons"; 4 | import HowToPlay from "./HowToPlay"; 5 | import styled from "styled-components"; 6 | 7 | const NamePopup = ({ 8 | handleSubmit, 9 | inviteInputRef, 10 | roomId, 11 | copyLink, 12 | updateMyName, 13 | myName, 14 | nameError, 15 | reactGA, 16 | }) => { 17 | const [isVisible, setIsVisible] = useState(false); 18 | const PopupElement = isVisible ? HelpPopup : Popup; 19 | 20 | return ( 21 | 22 | {isVisible ? ( 23 | 24 | ) : ( 25 | <> 26 |
    handleSubmit(e)}> 27 | 28 | Invite a friend 29 | 30 | 37 | 38 | 39 | 40 | 41 | Enter your name 42 | updateMyName(e)} 47 | defaultValue={myName} 48 | /> 49 | {nameError && {nameError}} 50 | JOIN GAME 51 | 52 |
    53 | 54 | )} 55 |
    56 | ); 57 | }; 58 | 59 | const Popup = styled.div` 60 | position: absolute; 61 | top: 0; 62 | left: 0; 63 | width: 100%; 64 | height: 100%; 65 | background: rgba(0, 0, 0, 0.8); 66 | color: #fff; 67 | z-index: 9999; 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | justify-content: center; 72 | transition: background 0.25s; 73 | `; 74 | 75 | const HelpPopup = styled.div` 76 | position: absolute; 77 | top: 0; 78 | left: 0; 79 | width: 100%; 80 | height: 100%; 81 | background: rgba(0, 0, 0, 0.95); 82 | color: #fff; 83 | z-index: 9999; 84 | display: flex; 85 | flex-direction: column; 86 | align-items: center; 87 | justify-content: center; 88 | transition: background 0.25s; 89 | `; 90 | 91 | const PopupInnerWrap = styled.div` 92 | display: flex; 93 | flex-direction: column; 94 | max-width: 218px; 95 | `; 96 | 97 | const ErrorMsg = styled.p` 98 | margin-top: 0; 99 | color: #cc2e2e; 100 | `; 101 | 102 | const IconWrap = styled.button` 103 | appearance: none; 104 | background: #2cce9f; 105 | color: #000; 106 | font-size: 1em; 107 | border: 0; 108 | padding: 0.7em 1em; 109 | border-radius: 0 8px 8px 0; 110 | margin: 0 auto; 111 | transition: opacity 0.25s; 112 | 113 | &:hover, 114 | &:focus { 115 | opacity: 0.5; 116 | } 117 | &:focus { 118 | outline: 0; 119 | border-color: #2cce9f; 120 | } 121 | & svg { 122 | display: block; 123 | } 124 | `; 125 | 126 | const Flex = styled.div` 127 | display: flex; 128 | margin-bottom: 2em; 129 | border-radius: 8px; 130 | `; 131 | 132 | const NameInput = styled.input` 133 | appearance: none; 134 | font-size: 1em; 135 | border: 0; 136 | margin: 0 0 1em; 137 | padding: 0.25em 0 0.5em; 138 | background: transparent; 139 | border-bottom: 1px solid white; 140 | color: #fff; 141 | transition: border-color 0.25s; 142 | border-radius: 0; 143 | 144 | &:focus { 145 | outline: 0; 146 | border-color: #2cce9f; 147 | } 148 | `; 149 | 150 | const NameLabel = styled.label` 151 | text-align: left; 152 | text-transform: uppercase; 153 | font-size: 0.813em; 154 | color: #c1bdbd; 155 | `; 156 | 157 | const InviteInput = styled.input` 158 | appearance: none; 159 | font-size: 1em; 160 | border: 0; 161 | background: white; 162 | border-radius: 8px 0 0 8px; 163 | color: #000; 164 | padding: 0.25em 0.5em; 165 | margin: 0; 166 | direction: rtl; 167 | 168 | &:focus { 169 | outline: 0; 170 | border-color: #2cce9f; 171 | } 172 | `; 173 | 174 | const InviteLabel = styled.label` 175 | text-align: left; 176 | text-transform: uppercase; 177 | font-size: 0.813em; 178 | color: #c1bdbd; 179 | margin-bottom: 0.5em; 180 | `; 181 | 182 | const JoinGameButton = styled.button` 183 | appearance: none; 184 | background: #2cce9f; 185 | color: #000; 186 | font-size: 1em; 187 | border: 0; 188 | padding: 0.7em 1em; 189 | border-radius: 8px; 190 | margin: 0 auto; 191 | transition: opacity 0.25s; 192 | 193 | &:hover, 194 | &:focus { 195 | opacity: 0.5; 196 | } 197 | &:focus { 198 | outline: 0; 199 | border-color: #2cce9f; 200 | } 201 | `; 202 | 203 | export default NamePopup; 204 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/Admin.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { SERVER_URL } from "../constants"; 4 | import styled from "styled-components"; 5 | import axios from "axios"; 6 | 7 | const Admin = () => { 8 | const [toggleWhiteCards, setToggleWhiteCards] = useState(false); 9 | const [toggleBlackCards, setToggleBlackCards] = useState(false); 10 | const [data, setData] = useState({}); 11 | 12 | useEffect(() => { 13 | axios.get(`${SERVER_URL}/api/getActiveRooms`).then((res) => { 14 | console.log(res.data); 15 | setData(res.data); 16 | }); 17 | }, []); 18 | 19 | const getSlice = (type, cards) => { 20 | const check = type === "white" ? toggleWhiteCards : toggleBlackCards; 21 | return check ? cards.length : 1; 22 | }; 23 | 24 | console.table(data); 25 | 26 | return ( 27 | 28 | Active Rooms 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {Object.keys(data).length > 0 && 43 | Object.entries(data).map( 44 | ([ 45 | roomName, 46 | { 47 | players, 48 | whiteCards, 49 | blackCards, 50 | playersThatLeft, 51 | submittedCards, 52 | isPrivate, 53 | }, 54 | ]) => ( 55 | 0 ? "1" : ".5" }}> 56 | 61 | 64 | {whiteCards.length > 0 && 65 | whiteCards 66 | .slice(0, getSlice("white", whiteCards)) 67 | .map((whiteCard, index) => ( 68 | 69 | 81 | 82 | ))} 83 | 84 | 85 | 86 | 89 | {blackCards.length > 0 && 90 | blackCards 91 | .slice(0, getSlice("black", blackCards)) 92 | .map((blackCard, index) => ( 93 | 94 | 106 | 107 | ))} 108 | 109 | 110 | 111 | 114 | {submittedCards.length > 0 && 115 | submittedCards.map((submittedCard) => ( 116 | 117 | 118 | 119 | ))} 120 | 121 | 122 | 123 | 129 | 141 | 142 | 143 | ) 144 | )} 145 | 146 |
    Room NameWhite CardsBlack CardsSubmitted CardsPast PlayersCurrent PlayersIs Private
    57 | 58 | {roomName} 59 | 60 | 62 | 63 |
    70 | {whiteCard.text ? whiteCard.text : whiteCard}{" "} 71 | {index === 0 && ( 72 | 79 | )} 80 |
    87 | 88 |
    95 | {blackCard.text ? blackCard.text : blackCard}{" "} 96 | {index === 0 && ( 97 | 104 | )} 105 |
    112 | 113 |
    {submittedCard.text}
    124 | {playersThatLeft.length > 0 && 125 | playersThatLeft.map((player) => ( 126 |

    {player.name}

    127 | ))} 128 |
    130 | {players.length > 0 && 131 | players.map((player) => ( 132 | 137 | {player.name} 138 | 139 | ))} 140 | {isPrivate ? "true" : "false"}
    147 |
    148 | ); 149 | }; 150 | 151 | const SimpleTable = styled.table` 152 | border-collapse: collapse; 153 | `; 154 | 155 | const Table = styled.table` 156 | width: 100%; 157 | text-align: left; 158 | border-collapse: collapse; 159 | 160 | thead { 161 | background: #828282; 162 | border-radius: 8px 8px 0 0; 163 | color: white; 164 | th { 165 | padding: 0.5em; 166 | } 167 | } 168 | tbody tr:nth-child(2n + 2) { 169 | background: #dcdcdc; 170 | } 171 | td { 172 | vertical-align: top; 173 | } 174 | tbody > tr > td { 175 | padding: 0.5em; 176 | } 177 | p { 178 | margin: 0.25em 0; 179 | } 180 | `; 181 | 182 | const Title = styled.h1` 183 | text-align: center; 184 | text-transform: uppercase; 185 | `; 186 | 187 | const Wrapper = styled.div` 188 | max-width: 1000px; 189 | margin: 0 auto; 190 | `; 191 | 192 | const LinkElement = styled(Link)` 193 | display: block; 194 | color: #2cce9f; 195 | font-weight: bold; 196 | 197 | & + & { 198 | margin-top: 0.25em; 199 | } 200 | 201 | &:hover, 202 | &:focus { 203 | text-decoration: none; 204 | } 205 | `; 206 | 207 | export default Admin; 208 | -------------------------------------------------------------------------------- /src/components/CreateADeck.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import {Helmet} from 'react-helmet'; 4 | import {SERVER_URL} from '../constants'; 5 | import axios from 'axios'; 6 | import PrivacyCheck from './PrivacyCheck'; 7 | import ChooseADeck from './ChooseADeck'; 8 | import styled, {createGlobalStyle} from 'styled-components'; 9 | import {Link} from 'react-router-dom'; 10 | 11 | const handleKeyUp = ({e, setNameOfDeck}) => { 12 | const val = e.target.value.trim().toLowerCase().replace(/\s+/g, '-'); 13 | setNameOfDeck(val); 14 | }; 15 | 16 | const handleSubmit = ({ 17 | e, 18 | nameOfDeck, 19 | setError, 20 | isPrivate, 21 | deck, 22 | setSecret, 23 | reactGA, 24 | setLoading, 25 | }) => { 26 | e.preventDefault(); 27 | setLoading(true); 28 | if (nameOfDeck.trim().length === 0) { 29 | return setError('Error: Please enter the name of your deck.'); 30 | } 31 | axios 32 | .post(`${SERVER_URL}/api/createDeck`, { 33 | deckName: nameOfDeck.trim(), 34 | isPrivate, 35 | hasSFWCards: deck === 'safe-for-work', 36 | hasNSFWCards: deck === 'not-safe-for-work', 37 | approved: false, 38 | }) 39 | .then((res) => { 40 | setLoading(false); 41 | if (res.data.includes('Error')) { 42 | return setError(res.data); 43 | } 44 | 45 | reactGA.event({ 46 | category: "Deck", 47 | action: "Created a new deck", 48 | label: nameOfDeck.trim(), 49 | }); 50 | 51 | // redirect on success 52 | setSecret(res.data); 53 | setError(''); 54 | }); 55 | }; 56 | 57 | const CreateADeck = ({title, reactGA}) => { 58 | const [nameOfDeck, setNameOfDeck] = useState(''); 59 | const [isSuccess, setSuccess] = useState(false); 60 | const [error, setError] = useState(''); 61 | const [isPrivate, setIsPrivate] = useState(true); 62 | const [deck, setDeck] = useState(''); 63 | const [secret, setSecret] = useState(''); 64 | const [loading, setLoading] = useState(false); 65 | 66 | useEffect(() => { 67 | if (secret) { 68 | setSuccess(true); 69 | } 70 | }, [secret]); 71 | 72 | return ( 73 | 74 | 75 | 76 | {title} 77 | 78 |
    80 | handleSubmit({ 81 | e, 82 | setSuccess, 83 | nameOfDeck, 84 | setError, 85 | isPrivate, 86 | deck, 87 | setSecret, 88 | reactGA, 89 | setLoading, 90 | }) 91 | } 92 | > 93 | 94 | Create a deck 95 | 96 | handleKeyUp({e, setNameOfDeck})} 100 | maxLength="20" 101 | /> 102 | {error} 103 | 104 | 111 | 117 | 118 | Back 119 | 122 | 123 | 124 | {isSuccess && ( 125 | 126 | )} 127 |
    128 | ); 129 | }; 130 | 131 | const GlobalStyle = createGlobalStyle` 132 | body { 133 | text-align: center; 134 | padding: 2em; 135 | background: #000; 136 | color: #fff; 137 | border: 1em solid; 138 | border-image: linear-gradient(90deg, rgb(64,224,208), rgb(255,140,0), rgb(255,0,128) ) 1; 139 | display: flex; 140 | flex-direction: column; 141 | justify-content: center; 142 | } 143 | button, 144 | input { 145 | appearance: none; 146 | border: 0; 147 | } 148 | `; 149 | const MainHeading = styled.h1` 150 | color: #fff; 151 | margin: 0; 152 | font-weight: normal; 153 | font-size: 2em; 154 | margin-bottom: 1em; 155 | `; 156 | const Page = styled.div` 157 | display: flex; 158 | flex-direction: column; 159 | width: 100%; 160 | justify-content: center; 161 | align-items: center; 162 | color: #000; 163 | background: #000; 164 | min-height: 100%; 165 | `; 166 | const Form = styled.form` 167 | display: flex; 168 | flex-direction: column; 169 | justify-content: center; 170 | align-items: center; 171 | `; 172 | const ErrorText = styled.p` 173 | color: red; 174 | font-size: 0.8rem; 175 | `; 176 | const Input = styled.input` 177 | appearance: none; 178 | font-size: 1em; 179 | border: 0; 180 | margin: 0; 181 | padding: 0.5em 0 0.3em; 182 | background: transparent; 183 | border-bottom: 1px solid #fff; 184 | transition: border-color 0.25s; 185 | border-radius: 0; 186 | color: #fff; 187 | 188 | &:-webkit-autofill, 189 | &:-webkit-autofill:hover, 190 | &:-webkit-autofill:focus, 191 | &:-webkit-autofill:active { 192 | -webkit-text-fill-color: #fff; 193 | -webkit-box-shadow: 0 0 0px 1000px #000 inset; 194 | transition: background-color 5000s ease-in-out 0s; 195 | } 196 | 197 | &:hover, 198 | &:focus { 199 | outline: 0; 200 | border-color: #2cce9f; 201 | } 202 | `; 203 | 204 | const Label = styled.label` 205 | text-align: left; 206 | text-transform: uppercase; 207 | font-size: 0.813em; 208 | display: block; 209 | font-weight: bold; 210 | color: #fff; 211 | `; 212 | 213 | const Button = styled.button` 214 | display: block; 215 | appearance: none; 216 | background: #2cce9f; 217 | color: #000; 218 | font-size: 1em; 219 | border: 0; 220 | padding: 0.7em 1em; 221 | border-radius: 8px; 222 | margin: 1em 0.5em; 223 | font-weight: bold; 224 | margin-top: 1em; 225 | 226 | &:hover, 227 | &:focus, 228 | &:disabled { 229 | opacity: 0.5; 230 | outline: 0; 231 | } 232 | `; 233 | 234 | const Wrapper = styled.div` 235 | display: flex; 236 | flex-direction: column; 237 | width: 100%; 238 | max-width: 270px; 239 | justify-content: center; 240 | margin: auto; 241 | `; 242 | 243 | const Flex = styled.div` 244 | display: flex; 245 | align-items: center; 246 | `; 247 | const WhiteButton = styled(Link)` 248 | display: block; 249 | appearance: none; 250 | background: #fff; 251 | color: #000; 252 | font-size: 1em; 253 | border: 0; 254 | padding: 0.7em 1em; 255 | border-radius: 8px; 256 | margin: 1em 0.5em; 257 | font-weight: bold; 258 | transition: opacity 0.25s; 259 | text-decoration: none; 260 | 261 | &:hover, 262 | &:focus, 263 | &:disabled { 264 | opacity: 0.5; 265 | outline: 0; 266 | } 267 | `; 268 | 269 | export default CreateADeck; 270 | -------------------------------------------------------------------------------- /src/components/DraggableCard.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import {useDrag} from 'react-dnd'; 4 | import {TouchIcon} from '../icons'; 5 | 6 | const CardElement = styled.div` 7 | transition: transform 0.35s, z-index 0s 0.35s; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | border-radius: 8px; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | padding: 1.25em; 18 | overflow: hidden; 19 | user-select: none; 20 | cursor: pointer; 21 | @media screen and (min-width: 1100px) { 22 | font-size: 16px; 23 | } 24 | 25 | &.is-dragging { 26 | background: red; 27 | transition: none; 28 | } 29 | `; 30 | 31 | const TouchIconWrap = styled.div` 32 | color: #fff; 33 | width: 50px; 34 | max-width: 30%; 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | transform: translate(-50%, -50%); 39 | transition: color 0.25s; 40 | 41 | ${CardElement}:hover & { 42 | color: #2cce9f; 43 | } 44 | `; 45 | 46 | const DraggableCard = ({ 47 | bgColor, 48 | isBroadcastingDrag = true, 49 | isFlipBroadcasted, 50 | color, 51 | socket, 52 | text, 53 | type, 54 | setUserIsDragging, 55 | flippedByDefault = false, 56 | isFlippable = true, 57 | screen = '', 58 | isMyCardsOpen, 59 | isSubmittedTableOpen, 60 | }) => { 61 | const [ghostCard, setGhostCard] = useState({}); 62 | const [isFlipped, setFlipped] = useState(flippedByDefault); 63 | const [{isDragging, getDifferenceFromInitialOffset}, drag] = useDrag({ 64 | item: { 65 | type, 66 | id: 0, 67 | text, 68 | bgColor, 69 | color, 70 | isFlipped, 71 | }, 72 | collect: (monitor) => ({ 73 | isDragging: !!monitor.isDragging() && !Object.keys(ghostCard).length, 74 | getDifferenceFromInitialOffset: 75 | !!monitor.isDragging() && monitor.getDifferenceFromInitialOffset(), 76 | }), 77 | }); 78 | 79 | if (isDragging && getDifferenceFromInitialOffset) { 80 | const {x, y} = getDifferenceFromInitialOffset; 81 | 82 | if (isBroadcastingDrag) { 83 | // send dragged card to server 84 | socket.emit('dragged card', {type, text, x, y}); 85 | } 86 | } 87 | 88 | useEffect(() => { 89 | setUserIsDragging(type); 90 | 91 | if (isBroadcastingDrag) { 92 | if (!isDragging) { 93 | // send card that was let go to server 94 | socket.emit('let go card', {ghostDragging: false, type, text}); 95 | } 96 | } 97 | 98 | if (!isDragging) { 99 | setUserIsDragging(null); 100 | } 101 | 102 | return () => { 103 | setUserIsDragging(null); 104 | }; 105 | }, [isBroadcastingDrag, setUserIsDragging, socket, text, type, isDragging]); 106 | 107 | useEffect(() => { 108 | let isMounted = true; 109 | if (isBroadcastingDrag) { 110 | // on everyones client but the sender, show the card being returned to deck if let go prematurely 111 | socket.on('let go card', ({text: otherText}) => { 112 | if (isMounted && text === otherText) { 113 | setGhostCard({}); 114 | } 115 | }); 116 | 117 | // on everyones client but the sender, show the card being dragged 118 | socket.on('dragged card', ({text: otherText, x, y, type}) => { 119 | if (isMounted && text === otherText) { 120 | // if i'm looking at my cards, never drag cards on other screens 121 | if (isMyCardsOpen && !isSubmittedTableOpen) { 122 | return; 123 | } 124 | // if the card that is being dragged is on the submitted cards screen 125 | // and the submitted cards screen isn't open, then don't set state 126 | if (screen === 'submittedCards' && !isSubmittedTableOpen) { 127 | return; 128 | } 129 | // if the card that is being dragged is on the main screen 130 | // and the submitted cards screen is open, then don't set state 131 | if (screen === 'main' && isSubmittedTableOpen) { 132 | return; 133 | } 134 | 135 | setGhostCard({x, y, text}); 136 | } 137 | }); 138 | } 139 | 140 | if (isFlipBroadcasted) { 141 | socket.on('card is flipped', function ({isFlipped, text: otherText}) { 142 | if (isMounted && text === otherText) { 143 | setFlipped(isFlipped); 144 | } 145 | }); 146 | } 147 | 148 | return () => { 149 | // socket.off('let go card'); 150 | // socket.off('dragged cards'); 151 | isMounted = false; 152 | }; 153 | }, [ 154 | isBroadcastingDrag, 155 | setUserIsDragging, 156 | socket, 157 | text, 158 | type, 159 | isFlipBroadcasted, 160 | isMyCardsOpen, 161 | isSubmittedTableOpen, 162 | screen 163 | ]); 164 | 165 | const getTransform = () => { 166 | if (isBroadcastingDrag) { 167 | // any cards being dragged by someone else 168 | if (Object.keys(ghostCard).length) { 169 | if (ghostCard.text === text) { 170 | return { 171 | pointerEvents: 'none', 172 | opacity: '1', 173 | transform: `translate3d(${ghostCard.x}px, ${ghostCard.y}px, 0)`, 174 | zIndex: '999', 175 | }; 176 | } else { 177 | return {pointerEvents: 'none', transform: 'translate3d(0, 0, 0)'}; 178 | } 179 | } 180 | } 181 | 182 | // on the client that's actually dragging the card 183 | if (isDragging && getDifferenceFromInitialOffset) { 184 | return { 185 | pointerEvents: 'none', 186 | transform: `translate3d(${getDifferenceFromInitialOffset.x}px, ${getDifferenceFromInitialOffset.y}px, 0)`, 187 | }; 188 | } 189 | 190 | return {transform: 'translate3d(0, 0, 0)'}; 191 | }; 192 | 193 | const getClassName = () => { 194 | if (isBroadcastingDrag) { 195 | // any cards being dragged by someone else 196 | if (Object.keys(ghostCard).length) { 197 | if (ghostCard.text === text) { 198 | return 'is-dragging'; 199 | } else { 200 | return null; 201 | } 202 | } 203 | } 204 | 205 | // on the client that's actually dragging the card 206 | if (isDragging && getDifferenceFromInitialOffset) { 207 | return 'is-dragging'; 208 | } 209 | 210 | return null; 211 | }; 212 | 213 | return ( 214 | { 217 | if (isFlippable) { 218 | setFlipped((isFlipped) => { 219 | socket.emit('card is flipped', {isFlipped: !isFlipped, text}); 220 | return !isFlipped; 221 | }); 222 | } 223 | }} 224 | ref={drag} 225 | style={{ 226 | zIndex: isDragging ? 999 : '0', 227 | ...getTransform(), 228 | backgroundColor: bgColor, 229 | color, 230 | justifyContent: isFlipped ? '' : 'flex-start', 231 | }} 232 | > 233 | {isFlipped ? ( 234 | text 235 | ) : ( 236 | <> 237 |
    242 | {type.includes('black') && ( 243 | 244 | 245 | 246 | )} 247 | 248 | )} 249 | 250 | ); 251 | }; 252 | 253 | export default DraggableCard; 254 | -------------------------------------------------------------------------------- /src/components/ChatBox.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { SendIcon, BackIcon } from "../icons"; 3 | import { Swipeable } from "react-swipeable"; 4 | import styled from "styled-components"; 5 | 6 | function handleSubmit({ e, inputRef, socket, myName, setMessages, reactGA, roomId }) { 7 | e.preventDefault(); 8 | 9 | const msg = inputRef.current.value; 10 | if (!msg.trim()) { 11 | return; 12 | } 13 | 14 | reactGA.event({ 15 | category: `Game ${roomId}`, 16 | action: "Sent a chat message", 17 | label: msg, 18 | }); 19 | 20 | setMessages((oldMessages) => [...oldMessages, { msg, from: myName }]); 21 | 22 | socket.emit("sent message to chat", { 23 | msg: inputRef.current.value, 24 | from: myName, 25 | }); 26 | 27 | inputRef.current.value = ""; 28 | } 29 | 30 | function getMyStyles() { 31 | return { 32 | background: "#40E0D0", 33 | }; 34 | } 35 | 36 | function getBubbleWrapStyles() { 37 | return { 38 | alignSelf: "flex-end", 39 | textAlign: "right", 40 | alignItems: "flex-end", 41 | }; 42 | } 43 | 44 | const handleClick = ({ e, wrapperRef, setChatOpen }) => { 45 | if (wrapperRef?.current?.contains(e.target)) { 46 | // inside click 47 | return; 48 | } 49 | setChatOpen(false); 50 | // outside click 51 | // ... do whatever on click outside here ... 52 | }; 53 | 54 | const getOverlayStyles = ({ chatOpen }) => { 55 | if (chatOpen) { 56 | return { 57 | pointerEvents: "auto", 58 | opacity: "1", 59 | }; 60 | } 61 | 62 | return null; 63 | }; 64 | 65 | const ChatBox = ({ chatOpen, setChatOpen, socket, myName, setUnreadCount, reactGA, roomId }) => { 66 | const inputRef = useRef(null); 67 | const wrapperRef = useRef(null); 68 | const scrollRef = useRef(null); 69 | const [messages, setMessages] = useState([]); 70 | useEffect(() => { 71 | if (socket) { 72 | socket.on("receive message from chat", function ({ msg, from }) { 73 | setMessages((oldMessages) => [...oldMessages, { msg, from }]); 74 | 75 | setUnreadCount(1); 76 | }); 77 | } 78 | }, [socket, setUnreadCount]); 79 | 80 | useEffect(() => { 81 | if (chatOpen) { 82 | inputRef.current.focus(); 83 | } 84 | }, [chatOpen, inputRef]); 85 | 86 | useEffect(() => { 87 | if (chatOpen && document && !document.hidden) { 88 | setUnreadCount(0); 89 | } 90 | }, [chatOpen, messages, setUnreadCount]); 91 | 92 | useEffect(() => { 93 | const xH = scrollRef.current.scrollHeight; 94 | scrollRef.current.scrollTop = xH; 95 | }, [messages]); 96 | 97 | useEffect(() => { 98 | // add when mounted 99 | document.addEventListener("mousedown", (e) => 100 | handleClick({ e, wrapperRef, setChatOpen }) 101 | ); 102 | // return function to be called when unmounted 103 | return () => { 104 | document.removeEventListener("mousedown", (e) => 105 | handleClick({ e, wrapperRef, setChatOpen }) 106 | ); 107 | }; 108 | }, [setChatOpen]); 109 | 110 | return ( 111 | <> 112 | 113 | (wrapperRef.current = el)} 115 | onSwipedRight={(eventData) => { 116 | setChatOpen(false); 117 | }} 118 | > 119 | 122 |
    123 | setChatOpen(false)}> 124 | 125 | 126 | Party Chat 127 |
    128 | 129 | 130 | {messages && 131 | messages.length > 0 && 132 | messages.map(({ msg, from }, index) => ( 133 | 137 | {from} 138 | 139 | {msg} 140 | 141 | 142 | ))} 143 | 144 | 145 |
    147 | handleSubmit({ e, inputRef, socket, myName, setMessages, reactGA, roomId }) 148 | } 149 | > 150 | 151 | 152 | 153 | 154 |
    155 |
    156 |
    157 | 158 | ); 159 | }; 160 | 161 | const Overlay = styled.div` 162 | position: absolute; 163 | top: 0; 164 | left: 0; 165 | width: 100%; 166 | height: 100%; 167 | background: rgba(0, 0, 0, 0.9); 168 | z-index: 999; 169 | pointer-events: none; 170 | opacity: 0; 171 | transition: opacity 0.25s; 172 | `; 173 | 174 | const Header = styled.div` 175 | position: relative; 176 | background: #fff; 177 | height: 40px; 178 | `; 179 | 180 | const BackButton = styled.button` 181 | position: absolute; 182 | top: 0; 183 | left: 0; 184 | width: 50px; 185 | height: 40px; 186 | appearance: none; 187 | background: 0; 188 | border: 0; 189 | display: flex; 190 | justify-content: center; 191 | align-items: center; 192 | transition: background 0.25s; 193 | 194 | &:hover, 195 | &:focus { 196 | background: rgb(64, 224, 208); 197 | outline: 0; 198 | } 199 | `; 200 | 201 | const Title = styled.h3` 202 | font-size: 0.75em; 203 | text-transform: uppercase; 204 | `; 205 | 206 | const MessageList = styled.ul` 207 | list-style: none; 208 | margin: 0; 209 | padding: 0; 210 | text-align: right; 211 | display: flex; 212 | flex-direction: column; 213 | padding: 1em; 214 | flex-grow: 1; 215 | `; 216 | 217 | const MessageGroup = styled.div` 218 | position: relative; 219 | overflow: auto; 220 | margin-top: auto; 221 | `; 222 | 223 | const ChatBubbleWrap = styled.li` 224 | display: inline-flex; 225 | align-self: flex-start; 226 | align-items: flex-start; 227 | flex-direction: column; 228 | margin: 0.5em 0; 229 | `; 230 | 231 | const PlayerName = styled.p` 232 | font-size: 0.75em; 233 | text-transform: uppercase; 234 | margin: 0; 235 | `; 236 | 237 | const ChatBubble = styled.div` 238 | background: #bbbbbb; 239 | border-radius: 12px; 240 | padding: 0.5em; 241 | text-align: left; 242 | word-break: break-word; 243 | `; 244 | 245 | const Form = styled.form` 246 | width: 100%; 247 | background: #e8e8e8; 248 | height: 50px; 249 | `; 250 | 251 | const Input = styled.input` 252 | display: inline-block; 253 | vertical-align: top; 254 | appearance: none; 255 | width: calc(100% - 50px); 256 | height: 50px; 257 | background: #2cce9f; 258 | padding: 0 1em; 259 | background: #fff; 260 | border-radius: 8px 0 0 0; 261 | border: 0; 262 | border-top: 2px solid transparent; 263 | border-left: 2px solid transparent; 264 | font-size: 1em; 265 | 266 | &:hover, 267 | &:focus { 268 | outline: 0; 269 | border-color rgb(64, 224, 208); 270 | 271 | & + button { 272 | border-color: rgb(64,224,208); 273 | } 274 | } 275 | `; 276 | 277 | const SendButton = styled.button` 278 | appearance: none; 279 | width: 50px; 280 | height: 50px; 281 | background: #fff; 282 | border: 0; 283 | border-radius: 0 8px 0 0; 284 | display: inline-block; 285 | border-top: 2px solid transparent; 286 | border-right: 2px solid transparent; 287 | transition: background 0.25s; 288 | 289 | &:hover, 290 | &:focus { 291 | outline: 0; 292 | background: rgb(64, 224, 208); 293 | } 294 | `; 295 | 296 | const Wrapper = styled.div` 297 | background: #dcdbdb; 298 | display: flex; 299 | flex-direction: column; 300 | justify-content: space-between; 301 | position: absolute; 302 | top: 0; 303 | right: 0; 304 | width: 80%; 305 | max-width: 400px; 306 | height: 100%; 307 | transform: translateX(100%) translateZ(0); 308 | transition: transform 0.4s, z-index 0s 0.4s; 309 | overflow: hidden; 310 | z-index: 999; 311 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 312 | `; 313 | 314 | export default ChatBox; 315 | -------------------------------------------------------------------------------- /src/components/Landing.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { useHistory, Link } from "react-router-dom"; 3 | import { Helmet } from "react-helmet"; 4 | import axios from "axios"; 5 | import { SERVER_URL } from "../constants"; 6 | import { LogoIcon } from "../icons"; 7 | import styled, { createGlobalStyle } from "styled-components"; 8 | 9 | const MIN_ROOM_NAME_CHARS = 2; 10 | const MAX_ROOM_NAME_CHARS = 16; 11 | 12 | function handleJoinGame({ 13 | e, 14 | setLoading, 15 | joinGameInputRef, 16 | history, 17 | setErrorMsg, 18 | }) { 19 | e.preventDefault(); 20 | if (joinGameInputRef.current.value.length < MIN_ROOM_NAME_CHARS) { 21 | setErrorMsg({ 22 | type: "join", 23 | message: `Room name must be at least ${MIN_ROOM_NAME_CHARS} characters long.`, 24 | }); 25 | return; 26 | } 27 | if (joinGameInputRef.current.value.length > MAX_ROOM_NAME_CHARS) { 28 | setErrorMsg({ 29 | type: "join", 30 | message: `Room name must be no longer than ${MAX_ROOM_NAME_CHARS} characters.`, 31 | }); 32 | return; 33 | } 34 | setLoading("join"); 35 | axios 36 | .post(`${SERVER_URL}/api/checkAvailableRooms`, { 37 | roomName: joinGameInputRef.current.value, 38 | }) 39 | .then((res) => { 40 | setLoading(""); 41 | 42 | // if no response, game doesn't exist, so ask if they want to create it 43 | if (!res.data) { 44 | setErrorMsg({ type: "join" }); 45 | } else { 46 | history.push(`/g/${joinGameInputRef.current.value}`); 47 | } 48 | }) 49 | .catch((err) => { 50 | setErrorMsg({ 51 | type: "join", 52 | message: "There was an error on the server. Please try again.", 53 | }); 54 | console.error(err); 55 | }); 56 | } 57 | 58 | const Landing = ({ title }) => { 59 | const history = useHistory(); 60 | const joinGameInputRef = useRef(null); 61 | const [errorMsg, setErrorMsg] = useState({}); 62 | const [loading, setLoading] = useState(""); 63 | return ( 64 | 65 | 66 | 67 | {title} 68 | 69 | 70 | 71 | 72 |
    74 | handleJoinGame({ 75 | e, 76 | setLoading, 77 | joinGameInputRef, 78 | history, 79 | setErrorMsg, 80 | }) 81 | } 82 | > 83 | GOT THE GAME CODE? 84 | 92 | 93 | Join Game 94 | 95 | {errorMsg.type === "join" && !errorMsg.message ? ( 96 | 97 | Game doesn't exist. Would you like to{" "} 98 | create it? 99 | 100 | ) : ( 101 | errorMsg.type === "join" && 102 | errorMsg.message && {errorMsg.message} 103 | )} 104 | 105 | OR 106 | 107 | 108 | Public Games 109 | 110 | 111 | Create Game 112 | How To Play 113 | 114 | Create Deck BETA 115 | 116 | {/* history.push('/create-deck')}>Create Deck 117 | history.push('/edit-deck')}>Edit Deck */} 118 |
    119 | 120 | Completely free and open sourced. No ads, accounts, or 121 | subscriptions. If you enjoyed the game and want to say thanks you can{" "} 122 | buy me a beer! 123 | 124 |
    125 |
    126 | ); 127 | }; 128 | 129 | const GlobalStyle = createGlobalStyle` 130 | body { 131 | text-align: center; 132 | border: 1em solid; 133 | border-image: linear-gradient(90deg, rgb(64,224,208), rgb(255,140,0), rgb(255,0,128) ) 1; 134 | background: #000; 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: center; 138 | padding: 0; 139 | } 140 | button, 141 | input { 142 | appearance: none; 143 | border: 0; 144 | } 145 | `; 146 | const FooterText = styled.p` 147 | color: #fff; 148 | font-size: .8em; 149 | font-style: italic; 150 | margin: 2em 0 0; 151 | max-width: 360px; 152 | line-height: 1.3; 153 | ` 154 | const InlineLink = styled.a` 155 | color: #2cce9f; 156 | 157 | &:hover, 158 | &:focus { 159 | text-decoration: none; 160 | } 161 | `; 162 | const BETAText = styled.sup` 163 | color: #2cce9f; 164 | font-size: 0.7em; 165 | font-weight: bold; 166 | `; 167 | const LandingWrapper = styled.div` 168 | display: flex; 169 | flex-direction: column; 170 | justify-content: center; 171 | align-items: center; 172 | min-height: 100%; 173 | background-color: #000; 174 | padding: 2em; 175 | `; 176 | const Heading = styled.h1` 177 | width: 100%; 178 | position: relative; 179 | margin: 0 0 1rem; 180 | padding: 0 1rem; 181 | `; 182 | 183 | const ErrorText = styled.p` 184 | color: red; 185 | font-size: 0.8rem; 186 | `; 187 | 188 | const GameExistsMessage = styled.p` 189 | color: #fff; 190 | 191 | a { 192 | color: #2cce9f; 193 | } 194 | `; 195 | 196 | const OrangeButton = styled(Link)` 197 | display: block; 198 | background: rgb(255, 140, 0); 199 | appearance: none; 200 | color: #000; 201 | font-size: 1em; 202 | border: 0; 203 | padding: 0.7em 1em; 204 | border-radius: 8px; 205 | margin: .75em 0; 206 | font-weight: bold; 207 | transition: opacity 0.25s; 208 | text-decoration: none; 209 | 210 | &:hover, 211 | &:focus, 212 | &:disabled { 213 | opacity: 0.5; 214 | outline: 0; 215 | } 216 | `; 217 | const PinkButton = styled(Link)` 218 | appearance: none; 219 | background: rgb(255, 0, 128); 220 | color: #000; 221 | font-size: 1em; 222 | border: 0; 223 | padding: 0.7em 1em; 224 | border-radius: 8px; 225 | margin: .75em 0; 226 | font-weight: bold; 227 | transition: opacity 0.25s; 228 | text-decoration: none; 229 | 230 | @media screen and (max-width: 501px) and (orientation: portrait) { 231 | position: absolute; 232 | top: -16px; 233 | left: -140px; 234 | width: 360px; 235 | transform: rotate(-25deg); 236 | } 237 | 238 | @media screen and (max-height: 501px) { 239 | position: absolute; 240 | top: -16px; 241 | left: -140px; 242 | width: 360px; 243 | transform: rotate(-25deg); 244 | } 245 | 246 | &:hover, 247 | &:focus, 248 | &:disabled { 249 | opacity: 0.5; 250 | outline: 0; 251 | } 252 | `; 253 | const GreenButton = styled.button` 254 | display: block; 255 | appearance: none; 256 | background: #2cce9f; 257 | color: #000; 258 | font-size: 1em; 259 | border: 0; 260 | padding: 0.7em 1em; 261 | border-radius: 8px; 262 | margin: 1em 0.5em; 263 | font-weight: bold; 264 | transition: opacity 0.25s; 265 | 266 | &:hover, 267 | &:focus, 268 | &:disabled { 269 | opacity: 0.5; 270 | outline: 0; 271 | } 272 | `; 273 | const WhiteButton = styled(Link)` 274 | display: block; 275 | appearance: none; 276 | background: #fff; 277 | color: #000; 278 | font-size: 1em; 279 | border: 0; 280 | padding: 0.7em 1em; 281 | border-radius: 8px; 282 | margin: .75em 0.5em; 283 | font-weight: bold; 284 | transition: opacity 0.25s; 285 | text-decoration: none; 286 | 287 | &:hover, 288 | &:focus, 289 | &:disabled { 290 | opacity: 0.5; 291 | outline: 0; 292 | } 293 | `; 294 | const BlueButton = styled(Link)` 295 | display: block; 296 | appearance: none; 297 | background: rgb(64, 224, 208); 298 | color: #000; 299 | font-size: 1em; 300 | border: 0; 301 | padding: 0.7em 1em; 302 | border-radius: 8px; 303 | margin: 1em 0 .75em; 304 | font-weight: bold; 305 | transition: opacity 0.25s; 306 | text-decoration: none; 307 | 308 | &:hover, 309 | &:focus, 310 | &:disabled { 311 | opacity: 0.5; 312 | outline: 0; 313 | } 314 | `; 315 | const OrTextWrap = styled.p` 316 | position: relative; 317 | font-style: italic; 318 | color: #fff; 319 | 320 | &::before { 321 | content: ""; 322 | position: absolute; 323 | top: 50%; 324 | left: 50%; 325 | transform: translate(-50%, -50%); 326 | border-top: 1px solid #fff; 327 | width: 70px; 328 | height: 1px; 329 | } 330 | `; 331 | 332 | const OrText = styled.span` 333 | position: relative; 334 | background: #000; 335 | padding: 0 0.5em; 336 | `; 337 | 338 | const JoinGameInput = styled.input` 339 | appearance: none; 340 | border-radius: 8px; 341 | padding: 0.35em 0.25em; 342 | border: 2px solid transparent; 343 | text-align: center; 344 | transition: border-color 0.25s; 345 | max-width: 120px; 346 | font-size: 1em; 347 | 348 | &:focus { 349 | outline: 0; 350 | border-color: #2cce9f; 351 | } 352 | `; 353 | const JoinGameLabel = styled.label` 354 | display: block; 355 | text-align: left; 356 | text-transform: uppercase; 357 | font-size: 0.813em; 358 | color: #fff; 359 | margin-bottom: 0.5em; 360 | `; 361 | 362 | const Form = styled.form` 363 | text-align: center; 364 | display: flex; 365 | flex-direction: column; 366 | align-items: center; 367 | `; 368 | 369 | export default Landing; 370 | -------------------------------------------------------------------------------- /src/components/CreateGame.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {Link, useHistory} from 'react-router-dom'; 3 | import axios from 'axios'; 4 | import {SERVER_URL} from '../constants'; 5 | import PrivacyCheck from './PrivacyCheck'; 6 | import ChooseADeck from './ChooseADeck'; 7 | import styled, {createGlobalStyle} from 'styled-components'; 8 | 9 | function handleCreateGame({ 10 | e, 11 | history, 12 | deck, 13 | setError, 14 | setLoading, 15 | isPrivate, 16 | reactGA, 17 | }) { 18 | e.preventDefault(); 19 | setLoading('createGame'); 20 | 21 | axios 22 | .post(`${SERVER_URL}/api/getDeck`, {deck}) 23 | .then((res) => { 24 | if (res.data) { 25 | setLoading(false); 26 | setError(''); 27 | createRandomRoom({ 28 | history, 29 | deck, 30 | setError, 31 | setLoading, 32 | isPrivate, 33 | reactGA, 34 | }); 35 | } else { 36 | setError('This deck could not be found.'); 37 | } 38 | }) 39 | .catch((err) => { 40 | setLoading(false); 41 | setError('This deck does not exist.'); 42 | console.error(err); 43 | }); 44 | } 45 | 46 | function getQueries({deck, isPrivate}) { 47 | let queryString = ''; 48 | 49 | if (deck) { 50 | queryString += `?deck=${deck}`; 51 | } 52 | if (isPrivate) { 53 | if (deck) { 54 | queryString += '&private=1'; 55 | } else { 56 | queryString += '?private=1'; 57 | } 58 | } 59 | 60 | return queryString; 61 | } 62 | 63 | function createRandomRoom({ 64 | history, 65 | deck, 66 | setError, 67 | setLoading, 68 | isPrivate, 69 | reactGA, 70 | }) { 71 | const random = ( 72 | Math.random().toString(36).substring(2, 15) + 73 | Math.random().toString(36).substring(2, 15) 74 | ).substr(0, 5); 75 | 76 | // check server to make sure random room doesn't already exist 77 | axios 78 | .post(`${SERVER_URL}/api/checkAvailableRooms`, {roomName: random}) 79 | .then((res) => { 80 | setLoading(false); 81 | setError(''); 82 | 83 | if (!res.data) { 84 | reactGA.event({ 85 | category: 'Game', 86 | action: `Created a new game`, 87 | label: random, 88 | }); 89 | 90 | history.push(`/g/${random}${getQueries({deck, isPrivate})}`); 91 | } else { 92 | createRandomRoom({ 93 | history, 94 | deck, 95 | setError, 96 | setLoading, 97 | isPrivate, 98 | reactGA, 99 | }); 100 | } 101 | }) 102 | .catch((err) => { 103 | setError('There was an error on the server. Please try again.'); 104 | console.error(err); 105 | }); 106 | } 107 | 108 | const handlePublicDeckClick = ({name, deck, setDeck}) => { 109 | if (deck === name) { 110 | return setDeck(''); 111 | } 112 | setDeck(name); 113 | }; 114 | 115 | const handleKeyUp = ({e, setDeck}) => { 116 | const val = e.target.value.trim().toLowerCase().replace(/\s+/g, '-'); 117 | setDeck(val); 118 | }; 119 | 120 | const CreateGame = ({reactGA}) => { 121 | const history = useHistory(); 122 | const [deck, setDeck] = useState('safe-for-work'); 123 | const [isPrivate, setIsPrivate] = useState(false); 124 | const [loading, setLoading] = useState(false); 125 | const [error, setError] = useState(''); 126 | const [publicDecks, setPublicDecks] = useState([]); 127 | useEffect(() => { 128 | axios.get(`${SERVER_URL}/api/getApprovedPublicDecks`).then((res) => { 129 | setPublicDecks(res.data); 130 | }); 131 | }, []); 132 | return ( 133 | 134 | 135 | Create game 136 |
    138 | handleCreateGame({ 139 | e, 140 | history, 141 | deck, 142 | setError, 143 | setLoading, 144 | isPrivate, 145 | reactGA, 146 | }) 147 | } 148 | > 149 | 156 | 157 | or choose a{' '} 158 | 159 | community deckBETA 160 | 161 | 162 | {publicDecks && ( 163 | 164 | {publicDecks.map(({name, isNSFW}) => ( 165 | 166 | handlePublicDeckClick({name, deck, setDeck})} 169 | style={{color: name === deck ? '#2cce9f' : null}} 170 | > 171 | {name.replace(/-/g, ' ')} 172 | {isNSFW && NSFW} 173 | 174 | 175 | ))} 176 | 177 | )} 178 | 179 | or choose a{' '} 180 | 181 | private deckBETA 182 | 183 | 184 | 185 | 186 | handleKeyUp({e, setDeck})} 190 | /> 191 | 192 | {error && {error}} 193 | 199 | 200 | Back 201 | Create 202 | 203 | 204 |
    205 | ); 206 | }; 207 | 208 | const GlobalStyle = createGlobalStyle` 209 | html { 210 | position: static; 211 | height: 100%; 212 | min-height: auto; 213 | overflow: visible; 214 | } 215 | body { 216 | text-align: center; 217 | background: #000; 218 | border: 1em solid; 219 | border-image: linear-gradient(90deg,rgb(64,224,208),rgb(255,140,0),rgb(255,0,128) ) 1; 220 | padding: 2em; 221 | display: flex; 222 | flex-direction: column; 223 | justify-content: center; 224 | height: auto; 225 | } 226 | button, 227 | input { 228 | appearance: none; 229 | border: 0; 230 | } 231 | `; 232 | 233 | const Form = styled.form` 234 | display: flex; 235 | flex-direction: column; 236 | justify-content: center; 237 | align-items: center; 238 | `; 239 | 240 | const Input = styled.input` 241 | appearance: none; 242 | font-size: 1em; 243 | border: 0; 244 | margin: 0; 245 | padding: 0.5em 0 0.3em; 246 | background: transparent; 247 | border-bottom: 1px solid #fff; 248 | transition: border-color 0.25s; 249 | border-radius: 0; 250 | color: #fff; 251 | 252 | &:-webkit-autofill, 253 | &:-webkit-autofill:hover, 254 | &:-webkit-autofill:focus, 255 | &:-webkit-autofill:active { 256 | -webkit-text-fill-color: #fff; 257 | -webkit-box-shadow: 0 0 0px 1000px #000 inset; 258 | transition: background-color 5000s ease-in-out 0s; 259 | } 260 | 261 | &:hover, 262 | &:focus { 263 | outline: 0; 264 | border-color: #2cce9f; 265 | } 266 | `; 267 | 268 | const Divider = styled.div` 269 | margin-bottom: 1em; 270 | `; 271 | 272 | const Label = styled.label` 273 | text-align: left; 274 | text-transform: uppercase; 275 | font-size: 0.813em; 276 | display: block; 277 | font-weight: bold; 278 | color: #fff; 279 | `; 280 | 281 | const NoWrap = styled.span` 282 | white-space: nowrap; 283 | `; 284 | 285 | const List = styled.ul` 286 | list-style: none; 287 | padding: 0; 288 | width: 100%; 289 | max-width: 300px; 290 | max-height: 175px; 291 | overflow: auto; 292 | margin: 0; 293 | border: 1px solid #2cce9f; 294 | border-radius: 8px; 295 | max-height: 139px; 296 | overflow: auto; 297 | margin-bottom: 1em; 298 | min-height: 145px; 299 | `; 300 | 301 | const BETAText = styled.sup` 302 | color: #2cce9f; 303 | font-size: 0.5em; 304 | font-weight: bold; 305 | margin-left: 0.25em; 306 | `; 307 | 308 | const NSFWText = styled.sup` 309 | color: #2cce9f; 310 | font-size: 0.7em; 311 | font-weight: bold; 312 | margin-left: 0.25em; 313 | `; 314 | 315 | const ListItem = styled.li` 316 | color: #fff; 317 | border-bottom: 1px solid rgb(44, 206, 159); 318 | `; 319 | 320 | const PublicDeckButton = styled.button` 321 | apperance: none; 322 | font-size: 1em; 323 | background: 0; 324 | color: #fff; 325 | padding: 0.5em 0; 326 | text-transform: capitalize; 327 | transition: color 0.25s; 328 | width: 100%; 329 | 330 | &:hover, 331 | &:focus { 332 | outline: 0; 333 | color: #2cce9f; 334 | } 335 | `; 336 | 337 | const Subtitle = styled.p` 338 | font-size: 1.5em; 339 | color: #fff; 340 | `; 341 | 342 | const MainHeading = styled.h1` 343 | color: #fff; 344 | margin: 0 0 1em; 345 | font-weight: normal; 346 | font-size: 2em; 347 | `; 348 | 349 | const ErrorText = styled.p` 350 | color: red; 351 | font-size: 0.8rem; 352 | `; 353 | 354 | const Wrapper = styled.div` 355 | min-height: 100%; 356 | background: #000; 357 | display: flex; 358 | flex-direction: column; 359 | justify-content: center; 360 | align-items: center; 361 | `; 362 | 363 | const WhiteButton = styled(Link)` 364 | display: block; 365 | appearance: none; 366 | background: #fff; 367 | color: #000; 368 | font-size: 1em; 369 | border: 0; 370 | padding: 0.7em 1em; 371 | border-radius: 8px; 372 | margin: 1em 0.5em; 373 | font-weight: bold; 374 | transition: opacity 0.25s; 375 | text-decoration: none; 376 | 377 | &:hover, 378 | &:focus, 379 | &:disabled { 380 | opacity: 0.5; 381 | outline: 0; 382 | } 383 | `; 384 | const GreenButton = styled.button` 385 | display: block; 386 | appearance: none; 387 | background: #2cce9f; 388 | color: #000; 389 | font-size: 1em; 390 | border: 0; 391 | padding: 0.7em 1em; 392 | border-radius: 8px; 393 | margin: 1em 0.5em; 394 | font-weight: bold; 395 | transition: opacity 0.25s; 396 | 397 | &:hover, 398 | &:focus, 399 | &:disabled { 400 | opacity: 0.5; 401 | outline: 0; 402 | } 403 | `; 404 | 405 | const Flex = styled.div` 406 | display: flex; 407 | align-items: center; 408 | `; 409 | 410 | export default CreateGame; 411 | -------------------------------------------------------------------------------- /src/components/MyCardsDropZone.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import styled from 'styled-components'; 3 | import cx from 'classnames'; 4 | import {useDrop} from 'react-dnd'; 5 | import Card from '../card'; 6 | import {BackIcon} from '../icons'; 7 | import DraggableCard from './DraggableCard'; 8 | import BlankCard from './BlankCard'; 9 | import CardWrap from './CardWrap'; 10 | import ChatButton from './ChatButton'; 11 | import {MAX_PLAYERS} from '../constants'; 12 | 13 | const MyCards = styled.button` 14 | width: calc(100% - 50px); 15 | height: 50px; 16 | line-height: 50px; 17 | font-weight: bold; 18 | background-color: #fff; 19 | color: #000; 20 | border: 0; 21 | padding: 0; 22 | transition: background 0.25s, color 0.25s; 23 | 24 | &:hover, 25 | &:focus { 26 | background: #2cce9f; 27 | outline: 0; 28 | } 29 | 30 | @media (min-width: 1600px) { 31 | border-radius: 8px 0 0 0; 32 | } 33 | `; 34 | 35 | const Wrapper = styled.div` 36 | width: 100%; 37 | position: relative; 38 | overflow: hidden; 39 | flex-grow: 1; 40 | overflow-x: auto; 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | justify-content: center; 45 | padding-left: 2em; 46 | padding-bottom: 50px; 47 | background-color: #000; 48 | `; 49 | 50 | const Flex = styled.div` 51 | display: flex; 52 | `; 53 | 54 | const WrapperCentered = styled.div` 55 | width: 100%; 56 | position: relative; 57 | overflow: hidden; 58 | flex-grow: 1; 59 | overflow-x: auto; 60 | display: flex; 61 | flex-direction: column; 62 | align-items: center; 63 | justify-content: center; 64 | padding-left: 2em; 65 | padding-bottom: 50px; 66 | background-color: rgba(35, 139, 179, 0.34); 67 | `; 68 | 69 | const Scrolling = styled.div` 70 | display: flex; 71 | position: absolute; 72 | padding-right: 1.5em; 73 | `; 74 | 75 | const BackToTableButton = styled.button` 76 | width: 100px; 77 | background: #000; 78 | color: #fff; 79 | height: 50px; 80 | appearance: none; 81 | border: 0; 82 | padding: 0; 83 | margin: 0; 84 | border-top: 1px solid #fff; 85 | transition: color 0.25s; 86 | 87 | &:hover, 88 | &:focus { 89 | color: #2cce9f; 90 | outline: 0; 91 | } 92 | `; 93 | 94 | const SubmittedCardsButton = styled.button` 95 | width: calc(100% - 100px); 96 | text-transform: uppercase; 97 | background: #fff; 98 | color: #000; 99 | height: 50px; 100 | appearance: none; 101 | border: 0; 102 | padding: 0; 103 | margin: 0; 104 | font-size: 16px; 105 | font-weight: bold; 106 | transition: color 0.25s, background 0.25s; 107 | 108 | &:hover, 109 | &:focus { 110 | background: #2cce9f; 111 | outline: 0; 112 | } 113 | `; 114 | 115 | const DiscardButton = styled.div` 116 | width: calc(100% - 100px); 117 | text-transform: uppercase; 118 | background: #fff; 119 | color: #000; 120 | height: 50px; 121 | border: 0; 122 | padding: 0; 123 | margin: 0; 124 | font-size: 16px; 125 | font-weight: bold; 126 | line-height: 50px; 127 | transition: background 0.25s; 128 | `; 129 | 130 | const MenuTitle = styled.h2` 131 | color: #fff; 132 | font-size: 2.5rem; 133 | opacity: 0.5; 134 | text-transform: uppercase; 135 | text-align: left; 136 | margin: 0; 137 | line-height: 1; 138 | width: 100%; 139 | padding-left: 0.25em; 140 | font-style: italic; 141 | 142 | @media (min-width: 1384px) { 143 | display: flex; 144 | justify-content: center; 145 | } 146 | `; 147 | 148 | const ScrollingWrap = styled.div` 149 | position: relative; 150 | height: 226px; 151 | width: 100%; 152 | 153 | @media (min-width: 1384px) { 154 | display: flex; 155 | justify-content: center; 156 | } 157 | `; 158 | 159 | const ButtonWrapper = styled.div` 160 | position: absolute; 161 | bottom: 0; 162 | left: 0; 163 | width: 100%; 164 | display: flex; 165 | align-items: center; 166 | transform: translateZ(0); 167 | `; 168 | 169 | const DropZoneWrap = styled.div` 170 | position: absolute; 171 | bottom: 0; 172 | left: 50%; 173 | width: 100%; 174 | transform: translateX(-50%); 175 | display: flex; 176 | `; 177 | 178 | function getBlankCards(myCards) { 179 | const length = 7 - myCards.length; 180 | const arr = Array.from({length}, (_, i) => i); 181 | 182 | return arr; 183 | } 184 | 185 | function getBlankSubmittedCards(cards) { 186 | const length = MAX_PLAYERS - 1 - cards.length; 187 | const arr = Array.from({length}, (_, i) => i); 188 | 189 | return arr; 190 | } 191 | 192 | function getMyNameCards({myCards, userIsDragging, myName, isOver}) { 193 | if (isOver && myCards.length !== 7) { 194 | return 'DROP IT!'; 195 | } 196 | if (myCards.length === 7 && userIsDragging === 'whiteCard') { 197 | return 'YOU ALREADY HAVE 7 CARDS'; 198 | } 199 | if (userIsDragging === 'whiteCard') { 200 | const emptyCardsLength = 7 - myCards.length; 201 | if (emptyCardsLength === 1) { 202 | return `DROP ${emptyCardsLength} WHITE CARD HERE`; 203 | } 204 | return `DROP ${emptyCardsLength} WHITE CARDS HERE`; 205 | } 206 | 207 | return `${myName}'S CARDS (${myCards.length})`; 208 | } 209 | 210 | function getMyNameCardsStyle({myCards, userIsDragging, isOver}) { 211 | if (isOver && myCards.length !== 7) { 212 | return { 213 | background: '#2cce9f', 214 | }; 215 | } 216 | if (myCards.length === 7 && userIsDragging === 'whiteCard') { 217 | return { 218 | background: '#ff2d55', 219 | }; 220 | } 221 | 222 | if (userIsDragging === 'whiteCard') { 223 | return { 224 | background: 'rgb(64,224,208)', 225 | }; 226 | } 227 | } 228 | 229 | function getBottomBarText({submittedCards, userIsDragging, isOverSubmit}) { 230 | if (isOverSubmit && submittedCards.length !== 7) { 231 | return 'DROP IT!'; 232 | } 233 | if (submittedCards.length === 7 && userIsDragging === 'whiteCard') { 234 | return 'CARDS ARE FULL'; 235 | } 236 | 237 | return userIsDragging === 'whiteCard' 238 | ? 'DROP TO SUBMIT CARD HERE' 239 | : 'Submitted Cards'; 240 | } 241 | 242 | function getBottomBarStyles({submittedCards, userIsDragging, isOverSubmit}) { 243 | if (isOverSubmit && submittedCards.length !== 7) { 244 | return { 245 | background: '#2cce9f', 246 | }; 247 | } 248 | if (submittedCards.length === 7 && userIsDragging === 'whiteCard') { 249 | return { 250 | background: userIsDragging === 'whiteCard' ? '#ff2d55' : null, 251 | }; 252 | } 253 | 254 | return { 255 | background: userIsDragging === 'whiteCard' ? 'rgb(64,224,208)' : null, 256 | }; 257 | } 258 | 259 | function getDiscardStyles({userIsDragging, isOverDiscard}) { 260 | if (isOverDiscard) { 261 | return { 262 | background: '#2cce9f', 263 | }; 264 | } 265 | if (userIsDragging === 'whiteCard') { 266 | return { 267 | background: 'rgb(64,224,208)', 268 | }; 269 | } 270 | } 271 | 272 | const MyCardsDropZone = ({ 273 | addCardToMyCards, 274 | submittedCards, 275 | discardACard, 276 | myCards, 277 | myName, 278 | socket, 279 | setUserIsDragging, 280 | userIsDragging, 281 | submitACard, 282 | blackCards, 283 | setChatOpen, 284 | unreadCount, 285 | isMyCardsOpen, 286 | setMyCardsOpen, 287 | isSubmittedTableOpen, 288 | setSubmittedTableOpen, 289 | }) => { 290 | const [{isOver}, drop] = useDrop({ 291 | accept: 'whiteCard', 292 | drop: (item) => { 293 | addCardToMyCards(item); 294 | }, 295 | collect: (monitor) => ({ 296 | isOver: !!monitor.isOver(), 297 | }), 298 | }); 299 | const [{isOverSubmit}, submitDropRef] = useDrop({ 300 | accept: 'whiteCard', 301 | drop: (item) => { 302 | submitACard(item); 303 | }, 304 | collect: (monitor) => ({ 305 | isOverSubmit: !!monitor.isOver(), 306 | }), 307 | }); 308 | const [{isOverDiscard}, discardDropRef] = useDrop({ 309 | accept: 'whiteCard', 310 | drop: (item) => { 311 | discardACard(item); 312 | }, 313 | collect: (monitor) => ({ 314 | isOverDiscard: !!monitor.isOver(), 315 | }), 316 | }); 317 | 318 | return ( 319 | <> 320 | 321 | setMyCardsOpen(true)} 323 | ref={drop} 324 | style={getMyNameCardsStyle({myCards, userIsDragging, isOver})} 325 | className="MyCardsDropBar" 326 | > 327 | {getMyNameCards({myCards, userIsDragging, myName, isOver})} 328 | 329 | 335 | 336 |
    337 | 338 | {`${myName}'s Cards`} 339 | 340 | 341 | 350 | {myCards.map((card) => ( 351 | 352 | 363 | 364 | ))} 365 | {getBlankCards(myCards).map((num) => ( 366 | Draw a card 367 | ))} 368 | 369 | 370 | 371 | 372 | setMyCardsOpen(false)}> 373 | 374 | 375 | setSubmittedTableOpen(true)} 378 | style={getBottomBarStyles({ 379 | submittedCards, 380 | userIsDragging, 381 | isOverSubmit, 382 | })} 383 | className="SubmittedCardsBar" 384 | > 385 | {getBottomBarText({submittedCards, userIsDragging, isOverSubmit})} 386 | 387 | 388 |
    389 |
    394 | 395 | SUBMITTED CARDS 396 | 397 | 398 | 407 | {submittedCards.map((card) => ( 408 | 409 | 419 | 420 | ))} 421 | {getBlankSubmittedCards(submittedCards).map((num) => ( 422 | 423 | ))} 424 | 425 | 426 | 427 | 428 | setSubmittedTableOpen(false)}> 429 | 430 | 431 | 432 | 437 | {isOverDiscard ? 'DROP IT!' : 'DROP TO DISCARD HERE'} 438 | 439 | 440 |
    441 | 442 | ); 443 | }; 444 | 445 | export default MyCardsDropZone; 446 | -------------------------------------------------------------------------------- /src/components/Game.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | .Game { 5 | text-align: center; 6 | max-width: 1600px; 7 | margin: 0 auto; 8 | } 9 | 10 | svg { 11 | fill: currentColor; 12 | pointer-events: none; 13 | } 14 | 15 | html, 16 | #root, 17 | .Game { 18 | height: 100%; 19 | } 20 | body { 21 | min-height: 100%; 22 | } 23 | 24 | body { 25 | background: #dcdbdb; 26 | width: 100%; 27 | position: relative; 28 | font-size: 16px; 29 | } 30 | button { 31 | cursor: pointer; 32 | font-size: 16px; 33 | } 34 | button::-moz-focus-inner { 35 | border: 0; 36 | } 37 | button, input { 38 | font-family: 'Roboto Condensed', Arial, sans-serif; 39 | } 40 | 41 | ::-moz-selection { background: #2cce9f; color: #000;} 42 | ::selection { background: #2cce9f; color: #000; } 43 | 44 | .MyCardsContainer, 45 | .SubmittedCardsTable { 46 | display: flex; 47 | flex-direction: column; 48 | justify-content: space-between; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | min-width: 100%; 53 | height: 100%; 54 | background: rgba(0, 0, 0, 1); 55 | transform: translateX(100%) translateZ(0); 56 | overflow: hidden; 57 | z-index: 1; 58 | } 59 | .MyCardsContainer.is-open, 60 | .SubmittedCardsTable.is-open { 61 | transform: translateX(0) translateZ(0); 62 | z-index: 999; /* sit on top of cards being dragged on table screen */ 63 | } 64 | .Game:not(.is-tourActive) .MyCardsContainer, 65 | .Game:not(.is-tourActive) .SubmittedCardsTable { 66 | transition: transform .4s, z-index 0s .4s; 67 | } 68 | .Game:not(.is-tourActive) .MyCardsContainer.is-open, 69 | .Game:not(.is-tourActive) .SubmittedCardsTable.is-open { 70 | transition: transform .4s; 71 | } 72 | 73 | .MaskOverlay { 74 | opacity: .8; 75 | } 76 | .reactour__helper .reactour__close { 77 | width: 11px; 78 | height: auto; 79 | top: 15px; 80 | right: 15px; 81 | } 82 | .reactour__helper .reactour__close::after { 83 | content: ''; 84 | position: absolute; 85 | top: 0; 86 | left: 0; 87 | width: 40px; 88 | height: 40px; 89 | transform: translate(-16px, -16px); 90 | } 91 | .reactour__helper span[data-tour-elem="badge"] { 92 | color: inherit; 93 | } 94 | 95 | /* Give prev/next arrows in tutorial more hit area */ 96 | button[data-tour-elem="right-arrow"], 97 | button[data-tour-elem="left-arrow"] { 98 | position: relative; 99 | } 100 | button[data-tour-elem="right-arrow"]::after, 101 | button[data-tour-elem="left-arrow"]::after { 102 | content: ''; 103 | position: absolute; 104 | top: 0; 105 | left: 0; 106 | width: 40px; 107 | height: 40px; 108 | transform: translate(-12px, -12px); 109 | } 110 | 111 | .LogoInCard { 112 | background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' x='0' y='0' viewBox='0 0 50.26 15.34' xml:space='preserve'%3E%3Cstyle%3E.st11%7Bfill:%23fff%7D%3C/style%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M.06 15.22c-.04 0-.06-.02-.06-.06l.02-8.24c0-.03.02-.05.05-.05h2.3c.74 0 1.32.22 1.75.67.43.45.64 1.06.64 1.82 0 .57-.11 1.06-.34 1.48-.22.42-.52.74-.88.96-.36.22-.75.34-1.18.34h-.9v3.02c0 .04-.02.06-.06.06H.06zm1.4-4.53h.91c.26 0 .48-.12.67-.37s.28-.56.28-.95c0-.31-.08-.57-.25-.77-.17-.2-.4-.3-.7-.3l-.91.01v2.38z' fill='url(%23SVGID_1_)'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M5.06 15.22c-.03 0-.05-.02-.05-.06l.01-8.24c0-.03.02-.05.05-.05h3.85c.03 0 .05.02.05.06v1.34c0 .03-.02.05-.05.05H6.47v1.87h2.46c.03 0 .05.02.05.05l.01 1.36c0 .03-.02.05-.05.05H6.47v2.1h2.47c.03 0 .05.02.05.06v1.37c0 .03-.02.05-.05.05H5.06z' fill='url(%23SVGID_2_)'/%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M9.35 15.22c-.03 0-.05-.02-.05-.06l.02-8.24c0-.03.02-.05.05-.05h2.42c.43 0 .83.11 1.19.32.36.21.65.5.87.85.22.36.32.76.32 1.21 0 .3-.04.56-.13.8s-.19.44-.31.6-.23.29-.32.37c.43.48.65 1.04.65 1.69l.01 2.46c0 .04-.02.06-.06.06h-1.36c-.03 0-.05-.01-.05-.04V12.7c0-.29-.1-.54-.31-.75a.997.997 0 00-.75-.32h-.79l-.01 3.53c0 .04-.02.06-.05.06H9.35zm1.41-5.03h1.03a.9.9 0 00.66-.28.9.9 0 00.29-.67.87.87 0 00-.28-.66.947.947 0 00-.67-.28h-1.03v1.89z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M16.82 15.34c-.43 0-.83-.11-1.18-.33-.36-.22-.64-.51-.85-.88s-.32-.77-.32-1.21v-.55c0-.04.02-.06.06-.06h1.34c.03 0 .05.02.05.06v.55c0 .26.09.49.26.68.18.19.39.28.64.28s.46-.1.64-.29c.18-.19.26-.42.26-.67 0-.3-.19-.55-.58-.77-.13-.07-.33-.18-.6-.34-.27-.15-.53-.3-.77-.43-.44-.26-.77-.58-.98-.97a2.73 2.73 0 01-.32-1.31c0-.45.11-.85.32-1.21.22-.36.5-.64.86-.85.36-.21.74-.31 1.16-.31.42 0 .81.11 1.17.32s.64.5.85.85c.21.36.32.75.32 1.19v.98c0 .03-.02.05-.05.05h-1.34c-.03 0-.05-.02-.05-.05l-.01-.98c0-.28-.09-.51-.26-.68a.832.832 0 00-.62-.26c-.25 0-.46.09-.64.28-.18.19-.26.41-.26.67 0 .26.06.48.17.66.11.18.32.34.61.5.04.02.12.07.23.13.12.06.24.13.38.2.14.08.26.14.37.2s.17.09.2.11c.4.22.72.5.95.82s.35.72.35 1.19A2.399 2.399 0 0118.01 15c-.36.23-.75.34-1.19.34z' fill='url(%23SVGID_4_)'/%3E%3ClinearGradient id='SVGID_5_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M21.9 15.34c-.43 0-.83-.11-1.18-.33a2.5 2.5 0 01-.86-.88c-.22-.36-.32-.77-.32-1.21l.01-3.8a2.364 2.364 0 011.18-2.06c.36-.22.75-.32 1.18-.32.43 0 .82.11 1.18.32.35.22.63.5.85.86.21.36.32.76.32 1.2l.01 3.8c0 .44-.11.84-.32 1.21-.21.36-.5.66-.85.88-.37.22-.77.33-1.2.33zm0-1.45c.24 0 .45-.1.63-.29s.27-.42.27-.67l-.01-3.8c0-.26-.08-.49-.25-.67a.847.847 0 00-.64-.28.87.87 0 00-.64.27c-.17.18-.26.4-.26.67v3.8c0 .26.09.49.26.68.18.2.39.29.64.29z' fill='url(%23SVGID_5_)'/%3E%3ClinearGradient id='SVGID_6_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M24.77 15.22c-.06 0-.1-.03-.1-.08l-.01-8.17c0-.06.03-.1.1-.1h1.08l2.03 4.73-.07-4.64c0-.06.04-.1.11-.1h1.19c.05 0 .07.03.07.1l.01 8.18c0 .05-.02.07-.06.07h-1.06l-2.08-4.42.08 4.32c0 .06-.04.1-.11.1h-1.18z' fill='url(%23SVGID_6_)'/%3E%3ClinearGradient id='SVGID_7_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M29.52 15.16l1.49-8.24c.01-.03.03-.05.06-.05h1.74c.03 0 .05.02.06.05l1.43 8.24c.01.04-.01.06-.05.06h-1.33c-.03 0-.05-.02-.06-.06l-.13-.88H31.1l-.13.88c-.01.04-.03.06-.06.06h-1.33c-.04 0-.06-.02-.06-.06zM31.33 13h1.15l-.49-3.37-.07-.44-.05.44-.54 3.37z' fill='url(%23SVGID_7_)'/%3E%3ClinearGradient id='SVGID_8_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M34.67 15.22c-.03 0-.05-.02-.05-.06l.01-8.23c0-.04.02-.06.06-.06h1.33c.04 0 .06.02.06.06l-.01 6.82h2.47c.04 0 .06.02.06.06v1.36c0 .04-.02.06-.06.06h-3.87z' fill='url(%23SVGID_8_)'/%3E%3ClinearGradient id='SVGID_9_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M39.01 15.22c-.04 0-.06-.02-.06-.06l.01-8.24c0-.03.02-.05.05-.05h1.34c.03 0 .05.02.05.05l.01 8.24c0 .04-.02.06-.05.06h-1.35z' fill='url(%23SVGID_9_)'/%3E%3ClinearGradient id='SVGID_10_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M42.38 15.22c-.03 0-.05-.02-.05-.06V8.32h-1.56c-.04 0-.06-.02-.06-.06l.01-1.34c0-.03.02-.05.05-.05h4.56c.04 0 .06.02.06.05v1.34c0 .04-.02.06-.05.06h-1.57l.01 6.84c0 .04-.02.06-.05.06h-1.35z' fill='url(%23SVGID_10_)'/%3E%3ClinearGradient id='SVGID_11_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M47.22 15.22c-.02 0-.04-.02-.04-.05l.01-3.41-1.61-4.85c-.01-.03 0-.05.04-.05h1.33c.04 0 .06.02.07.05l.89 3.23.9-3.23c.01-.03.03-.05.06-.05h1.34c.03 0 .04.02.04.05l-1.62 4.8.01 3.46c0 .03-.02.05-.05.05h-1.37z' fill='url(%23SVGID_11_)'/%3E%3Cg%3E%3Cpath class='st11' d='M3.46 3.71c-.03.57-.19 1-.48 1.31-.29.3-.7.46-1.23.46s-.96-.2-1.27-.61C.16 4.46 0 3.91 0 3.21v-.96C0 1.55.16 1.01.48.6.81.2 1.25 0 1.81 0c.51 0 .91.15 1.19.46.28.31.43.75.46 1.32h-.68c-.03-.43-.12-.74-.27-.93-.15-.18-.39-.28-.7-.28-.37 0-.65.15-.84.43s-.29.7-.29 1.25v.98c0 .54.09.95.27 1.24.18.28.45.43.79.43.35 0 .6-.09.75-.26.15-.17.25-.48.28-.93h.69zM6.85 4h-1.8l-.41 1.4h-.69L5.67.07h.57L7.96 5.4h-.69L6.85 4zm-1.62-.57h1.45l-.73-2.42-.72 2.42zM10.29 3.24h-.94V5.4h-.67V.07h1.49c.52 0 .92.14 1.19.41.27.27.4.67.4 1.19 0 .33-.07.62-.22.86s-.35.43-.62.55l1.03 2.26v.06h-.72l-.94-2.16zm-.94-.57h.81c.28 0 .5-.09.67-.27.17-.18.25-.42.25-.73 0-.68-.31-1.03-.93-1.03h-.8v2.03zM12.66 5.4V.07h1.27c.62 0 1.1.19 1.45.58.34.39.52.94.52 1.64v.89c0 .7-.17 1.25-.52 1.63-.35.38-.85.58-1.52.58h-1.2zm.67-4.75v4.18h.54c.47 0 .81-.13 1.03-.4.22-.27.33-.67.33-1.2v-.95c0-.56-.11-.97-.32-1.24-.22-.26-.54-.39-.98-.39h-.6zM19.26 4.05c0-.27-.07-.47-.22-.61-.14-.14-.4-.28-.78-.41-.38-.13-.66-.27-.86-.42-.2-.15-.35-.32-.45-.5-.1-.19-.15-.41-.15-.65 0-.42.14-.77.42-1.04.29-.28.66-.42 1.12-.42.31 0 .59.07.83.21.24.14.43.33.56.58.13.25.2.52.2.82h-.67c0-.33-.08-.58-.24-.76-.16-.18-.39-.27-.68-.27-.27 0-.48.08-.63.23-.15.15-.22.36-.22.64 0 .23.08.41.24.56.16.15.41.29.75.41.52.16.89.38 1.12.63.23.25.34.59.34 1 0 .43-.14.78-.42 1.04-.28.26-.66.39-1.14.39-.31 0-.6-.07-.86-.2s-.48-.34-.62-.58c-.15-.25-.23-.53-.23-.84h.67c0 .33.09.58.28.77.18.18.43.27.75.27.29 0 .52-.08.67-.23.14-.15.22-.36.22-.62zM25.99 3.2c0 .73-.16 1.29-.46 1.68-.31.39-.75.59-1.32.59-.55 0-.98-.19-1.3-.57-.32-.38-.48-.92-.5-1.62v-1c0-.71.16-1.27.47-1.67C23.2.2 23.64 0 24.2 0s1 .19 1.31.58c.31.39.47.94.48 1.65v.97zm-.67-.93c0-.56-.09-.98-.28-1.26S24.58.6 24.2.6c-.37 0-.65.14-.84.42-.19.28-.28.69-.29 1.23v.95c0 .54.09.96.28 1.24.19.28.47.43.85.43s.65-.13.83-.39c.18-.26.27-.67.28-1.21v-1zM29.5 3.05h-1.8V5.4h-.67V.07h2.77v.57h-2.1v1.83h1.8v.58z'/%3E%3C/g%3E%3C/svg%3E"); 113 | background-repeat: no-repeat; 114 | background-position: bottom left; 115 | width: 100%; 116 | max-width: 200px; 117 | align-self: flex-end; 118 | padding-bottom: 30.38%; 119 | display: flex; 120 | } 121 | .LogoInCard--whiteCard { 122 | background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' x='0' y='0' viewBox='0 0 50.26 15.34' xml:space='preserve'%3E%3Cstyle%3E.st11%7Bfill:%23000%7D%3C/style%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M.06 15.22c-.04 0-.06-.02-.06-.06l.02-8.24c0-.03.02-.05.05-.05h2.3c.74 0 1.32.22 1.75.67.43.45.64 1.06.64 1.82 0 .57-.11 1.06-.34 1.48-.22.42-.52.74-.88.96-.36.22-.75.34-1.18.34h-.9v3.02c0 .04-.02.06-.06.06H.06zm1.4-4.53h.91c.26 0 .48-.12.67-.37s.28-.56.28-.95c0-.31-.08-.57-.25-.77-.17-.2-.4-.3-.7-.3l-.91.01v2.38z' fill='url(%23SVGID_1_)'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M5.06 15.22c-.03 0-.05-.02-.05-.06l.01-8.24c0-.03.02-.05.05-.05h3.85c.03 0 .05.02.05.06v1.34c0 .03-.02.05-.05.05H6.47v1.87h2.46c.03 0 .05.02.05.05l.01 1.36c0 .03-.02.05-.05.05H6.47v2.1h2.47c.03 0 .05.02.05.06v1.37c0 .03-.02.05-.05.05H5.06z' fill='url(%23SVGID_2_)'/%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M9.35 15.22c-.03 0-.05-.02-.05-.06l.02-8.24c0-.03.02-.05.05-.05h2.42c.43 0 .83.11 1.19.32.36.21.65.5.87.85.22.36.32.76.32 1.21 0 .3-.04.56-.13.8s-.19.44-.31.6-.23.29-.32.37c.43.48.65 1.04.65 1.69l.01 2.46c0 .04-.02.06-.06.06h-1.36c-.03 0-.05-.01-.05-.04V12.7c0-.29-.1-.54-.31-.75a.997.997 0 00-.75-.32h-.79l-.01 3.53c0 .04-.02.06-.05.06H9.35zm1.41-5.03h1.03a.9.9 0 00.66-.28.9.9 0 00.29-.67.87.87 0 00-.28-.66.947.947 0 00-.67-.28h-1.03v1.89z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M16.82 15.34c-.43 0-.83-.11-1.18-.33-.36-.22-.64-.51-.85-.88s-.32-.77-.32-1.21v-.55c0-.04.02-.06.06-.06h1.34c.03 0 .05.02.05.06v.55c0 .26.09.49.26.68.18.19.39.28.64.28s.46-.1.64-.29c.18-.19.26-.42.26-.67 0-.3-.19-.55-.58-.77-.13-.07-.33-.18-.6-.34-.27-.15-.53-.3-.77-.43-.44-.26-.77-.58-.98-.97a2.73 2.73 0 01-.32-1.31c0-.45.11-.85.32-1.21.22-.36.5-.64.86-.85.36-.21.74-.31 1.16-.31.42 0 .81.11 1.17.32s.64.5.85.85c.21.36.32.75.32 1.19v.98c0 .03-.02.05-.05.05h-1.34c-.03 0-.05-.02-.05-.05l-.01-.98c0-.28-.09-.51-.26-.68a.832.832 0 00-.62-.26c-.25 0-.46.09-.64.28-.18.19-.26.41-.26.67 0 .26.06.48.17.66.11.18.32.34.61.5.04.02.12.07.23.13.12.06.24.13.38.2.14.08.26.14.37.2s.17.09.2.11c.4.22.72.5.95.82s.35.72.35 1.19A2.399 2.399 0 0118.01 15c-.36.23-.75.34-1.19.34z' fill='url(%23SVGID_4_)'/%3E%3ClinearGradient id='SVGID_5_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M21.9 15.34c-.43 0-.83-.11-1.18-.33a2.5 2.5 0 01-.86-.88c-.22-.36-.32-.77-.32-1.21l.01-3.8a2.364 2.364 0 011.18-2.06c.36-.22.75-.32 1.18-.32.43 0 .82.11 1.18.32.35.22.63.5.85.86.21.36.32.76.32 1.2l.01 3.8c0 .44-.11.84-.32 1.21-.21.36-.5.66-.85.88-.37.22-.77.33-1.2.33zm0-1.45c.24 0 .45-.1.63-.29s.27-.42.27-.67l-.01-3.8c0-.26-.08-.49-.25-.67a.847.847 0 00-.64-.28.87.87 0 00-.64.27c-.17.18-.26.4-.26.67v3.8c0 .26.09.49.26.68.18.2.39.29.64.29z' fill='url(%23SVGID_5_)'/%3E%3ClinearGradient id='SVGID_6_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M24.77 15.22c-.06 0-.1-.03-.1-.08l-.01-8.17c0-.06.03-.1.1-.1h1.08l2.03 4.73-.07-4.64c0-.06.04-.1.11-.1h1.19c.05 0 .07.03.07.1l.01 8.18c0 .05-.02.07-.06.07h-1.06l-2.08-4.42.08 4.32c0 .06-.04.1-.11.1h-1.18z' fill='url(%23SVGID_6_)'/%3E%3ClinearGradient id='SVGID_7_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M29.52 15.16l1.49-8.24c.01-.03.03-.05.06-.05h1.74c.03 0 .05.02.06.05l1.43 8.24c.01.04-.01.06-.05.06h-1.33c-.03 0-.05-.02-.06-.06l-.13-.88H31.1l-.13.88c-.01.04-.03.06-.06.06h-1.33c-.04 0-.06-.02-.06-.06zM31.33 13h1.15l-.49-3.37-.07-.44-.05.44-.54 3.37z' fill='url(%23SVGID_7_)'/%3E%3ClinearGradient id='SVGID_8_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M34.67 15.22c-.03 0-.05-.02-.05-.06l.01-8.23c0-.04.02-.06.06-.06h1.33c.04 0 .06.02.06.06l-.01 6.82h2.47c.04 0 .06.02.06.06v1.36c0 .04-.02.06-.06.06h-3.87z' fill='url(%23SVGID_8_)'/%3E%3ClinearGradient id='SVGID_9_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M39.01 15.22c-.04 0-.06-.02-.06-.06l.01-8.24c0-.03.02-.05.05-.05h1.34c.03 0 .05.02.05.05l.01 8.24c0 .04-.02.06-.05.06h-1.35z' fill='url(%23SVGID_9_)'/%3E%3ClinearGradient id='SVGID_10_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M42.38 15.22c-.03 0-.05-.02-.05-.06V8.32h-1.56c-.04 0-.06-.02-.06-.06l.01-1.34c0-.03.02-.05.05-.05h4.56c.04 0 .06.02.06.05v1.34c0 .04-.02.06-.05.06h-1.57l.01 6.84c0 .04-.02.06-.05.06h-1.35z' fill='url(%23SVGID_10_)'/%3E%3ClinearGradient id='SVGID_11_' gradientUnits='userSpaceOnUse' x1='0' y1='11.039' x2='50.257' y2='11.039'%3E%3Cstop offset='0' stop-color='%2340e0d0'/%3E%3Cstop offset='.255' stop-color='%23a3b464'/%3E%3Cstop offset='.5' stop-color='%23ff8c00'/%3E%3Cstop offset='.664' stop-color='%23ff5a2e'/%3E%3Cstop offset='.892' stop-color='%23ff1969'/%3E%3Cstop offset='1' stop-color='%23ff0080'/%3E%3C/linearGradient%3E%3Cpath d='M47.22 15.22c-.02 0-.04-.02-.04-.05l.01-3.41-1.61-4.85c-.01-.03 0-.05.04-.05h1.33c.04 0 .06.02.07.05l.89 3.23.9-3.23c.01-.03.03-.05.06-.05h1.34c.03 0 .04.02.04.05l-1.62 4.8.01 3.46c0 .03-.02.05-.05.05h-1.37z' fill='url(%23SVGID_11_)'/%3E%3Cg%3E%3Cpath class='st11' d='M3.46 3.71c-.03.57-.19 1-.48 1.31-.29.3-.7.46-1.23.46s-.96-.2-1.27-.61C.16 4.46 0 3.91 0 3.21v-.96C0 1.55.16 1.01.48.6.81.2 1.25 0 1.81 0c.51 0 .91.15 1.19.46.28.31.43.75.46 1.32h-.68c-.03-.43-.12-.74-.27-.93-.15-.18-.39-.28-.7-.28-.37 0-.65.15-.84.43s-.29.7-.29 1.25v.98c0 .54.09.95.27 1.24.18.28.45.43.79.43.35 0 .6-.09.75-.26.15-.17.25-.48.28-.93h.69zM6.85 4h-1.8l-.41 1.4h-.69L5.67.07h.57L7.96 5.4h-.69L6.85 4zm-1.62-.57h1.45l-.73-2.42-.72 2.42zM10.29 3.24h-.94V5.4h-.67V.07h1.49c.52 0 .92.14 1.19.41.27.27.4.67.4 1.19 0 .33-.07.62-.22.86s-.35.43-.62.55l1.03 2.26v.06h-.72l-.94-2.16zm-.94-.57h.81c.28 0 .5-.09.67-.27.17-.18.25-.42.25-.73 0-.68-.31-1.03-.93-1.03h-.8v2.03zM12.66 5.4V.07h1.27c.62 0 1.1.19 1.45.58.34.39.52.94.52 1.64v.89c0 .7-.17 1.25-.52 1.63-.35.38-.85.58-1.52.58h-1.2zm.67-4.75v4.18h.54c.47 0 .81-.13 1.03-.4.22-.27.33-.67.33-1.2v-.95c0-.56-.11-.97-.32-1.24-.22-.26-.54-.39-.98-.39h-.6zM19.26 4.05c0-.27-.07-.47-.22-.61-.14-.14-.4-.28-.78-.41-.38-.13-.66-.27-.86-.42-.2-.15-.35-.32-.45-.5-.1-.19-.15-.41-.15-.65 0-.42.14-.77.42-1.04.29-.28.66-.42 1.12-.42.31 0 .59.07.83.21.24.14.43.33.56.58.13.25.2.52.2.82h-.67c0-.33-.08-.58-.24-.76-.16-.18-.39-.27-.68-.27-.27 0-.48.08-.63.23-.15.15-.22.36-.22.64 0 .23.08.41.24.56.16.15.41.29.75.41.52.16.89.38 1.12.63.23.25.34.59.34 1 0 .43-.14.78-.42 1.04-.28.26-.66.39-1.14.39-.31 0-.6-.07-.86-.2s-.48-.34-.62-.58c-.15-.25-.23-.53-.23-.84h.67c0 .33.09.58.28.77.18.18.43.27.75.27.29 0 .52-.08.67-.23.14-.15.22-.36.22-.62zM25.99 3.2c0 .73-.16 1.29-.46 1.68-.31.39-.75.59-1.32.59-.55 0-.98-.19-1.3-.57-.32-.38-.48-.92-.5-1.62v-1c0-.71.16-1.27.47-1.67C23.2.2 23.64 0 24.2 0s1 .19 1.31.58c.31.39.47.94.48 1.65v.97zm-.67-.93c0-.56-.09-.98-.28-1.26S24.58.6 24.2.6c-.37 0-.65.14-.84.42-.19.28-.28.69-.29 1.23v.95c0 .54.09.96.28 1.24.19.28.47.43.85.43s.65-.13.83-.39c.18-.26.27-.67.28-1.21v-1zM29.5 3.05h-1.8V5.4h-.67V.07h2.77v.57h-2.1v1.83h1.8v.58z'/%3E%3C/g%3E%3C/svg%3E"); 123 | } 124 | -------------------------------------------------------------------------------- /src/components/EditADeck.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import {useLocation, Link} from 'react-router-dom'; 3 | import {Helmet} from 'react-helmet'; 4 | import queryString from 'query-string'; 5 | import {SERVER_URL} from '../constants'; 6 | import InputWithLabel from './InputWithLabel'; 7 | import Table from './Table'; 8 | import styled, {createGlobalStyle} from 'styled-components'; 9 | import axios from 'axios'; 10 | import {ToastContainer, toast, Slide} from 'react-toastify'; 11 | import {DeleteIcon, EditIcon} from '../icons'; 12 | 13 | const GlobalStyle = createGlobalStyle` 14 | body { 15 | text-align: center; 16 | padding: 2em; 17 | background: #000; 18 | color: #fff; 19 | border: 1em solid; 20 | border-image: linear-gradient(90deg, rgb(64,224,208), rgb(255,140,0), rgb(255,0,128) ) 1; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | } 25 | button, 26 | input { 27 | appearance: none; 28 | border: 0; 29 | } 30 | .Toastify__toast--info { 31 | background: #2cce9f; 32 | border-radius: 8px; 33 | color: #000; 34 | margin: 2em; 35 | font: inherit; 36 | } 37 | .Toastify__close-button { 38 | color: #000; 39 | } 40 | `; 41 | 42 | const ErrorText = styled.p` 43 | color: red; 44 | font-size: 0.8rem; 45 | `; 46 | 47 | const handleKeyUp = ({ 48 | e, 49 | initialDecks, 50 | setFilteredDecks, 51 | setPublicDecksInputVal, 52 | }) => { 53 | if (initialDecks.length) { 54 | if (e.target.value.trim() === '') { 55 | setFilteredDecks([]); 56 | setPublicDecksInputVal(false); 57 | return; 58 | } 59 | if (e.target.value !== '') { 60 | setPublicDecksInputVal(true); 61 | } 62 | const result = initialDecks.filter(({name}) => 63 | name.startsWith(e.target.value.toLowerCase().trim()) 64 | ); 65 | 66 | setFilteredDecks(result); 67 | } 68 | }; 69 | 70 | const getCardsLength = ({type, deckTable}) => { 71 | if (!deckTable && !deckTable.length) { 72 | return 0; 73 | } 74 | 75 | const cardLength = deckTable.filter((card) => card.type === type).length; 76 | return ( 77 | <> 78 | {cardLength} 79 | {` ${type} card${cardLength !== 1 ? 's' : ''}`} 80 | 81 | ); 82 | }; 83 | 84 | const addCard = ({ 85 | e, 86 | setIsLoading, 87 | deckTable, 88 | type, 89 | text, 90 | setDeckTable, 91 | location, 92 | setError, 93 | setWhiteCard, 94 | setBlackCard, 95 | defaultLocation, 96 | reactGA, 97 | }) => { 98 | e.preventDefault(); 99 | 100 | if (!text.trim().length) { 101 | return setError( 102 | 'Please enter something, you know, more than 0 characters.' 103 | ); 104 | } 105 | 106 | // if text already exists in the deck, return error 107 | if ( 108 | deckTable.find( 109 | (card) => 110 | card.type === type && card.text.toLowerCase() === text.toLowerCase() 111 | ) 112 | ) { 113 | return setError( 114 | `This same ${type} card has already been submitted. Please try again.` 115 | ); 116 | } 117 | 118 | setIsLoading(true); 119 | const deckName = location.replace('/', ''); 120 | const secret = queryString.parse(defaultLocation.search).secret; 121 | axios 122 | .post(`${SERVER_URL}/api/addCard/`, {type, text, deckName, secret}) 123 | .then((res) => { 124 | // if successful, update state 125 | // const data = cleanUpData(res.data); 126 | // setDeckTable(data); 127 | if (res.data.includes('Error')) { 128 | return setError(res.data); 129 | } 130 | 131 | setDeckTable((deckTable) => { 132 | const newDeckTable = [...deckTable]; 133 | newDeckTable.unshift({type, text}); 134 | return newDeckTable; 135 | }); 136 | setError(''); 137 | 138 | reactGA.event({ 139 | category: 'Deck', 140 | action: `Added a ${type} card to the ${deckName} deck`, 141 | label: text, 142 | }); 143 | 144 | if (type === 'black') { 145 | setBlackCard(''); 146 | } else { 147 | setWhiteCard(''); 148 | } 149 | }) 150 | .catch((err) => setError(err)) 151 | .finally((info) => { 152 | setIsLoading(false); 153 | }); 154 | }; 155 | 156 | const deleteCard = ({ 157 | e, 158 | type, 159 | text, 160 | setDeckTable, 161 | location, 162 | setError, 163 | setIsDeleting, 164 | defaultLocation, 165 | reactGA, 166 | }) => { 167 | e.preventDefault(); 168 | 169 | setIsDeleting(true); 170 | 171 | const confirmed = 172 | window && 173 | window.confirm( 174 | `Are you sure you want to delete the ${type} card "${text}" from your deck?` 175 | ); 176 | 177 | if (!confirmed) { 178 | return setIsDeleting(false); 179 | } 180 | 181 | const deckName = location.replace('/', ''); 182 | const secret = queryString.parse(defaultLocation.search).secret; 183 | 184 | axios 185 | .post(`${SERVER_URL}/api/deleteCard/`, {type, text, deckName, secret}) 186 | .then((res) => { 187 | // if successful, update state 188 | if (res.data.includes('Error')) { 189 | return setError(res.data); 190 | } 191 | 192 | setDeckTable((deckTable) => { 193 | const newDeckTable = [...deckTable]; 194 | const cardToRemoveIndex = newDeckTable.findIndex( 195 | (card) => card.text === text 196 | ); 197 | newDeckTable.splice(cardToRemoveIndex, 1); 198 | 199 | return newDeckTable; 200 | }); 201 | setError(''); 202 | 203 | reactGA.event({ 204 | category: 'Deck', 205 | action: `Removed a ${type} card from the ${deckName} deck`, 206 | label: text, 207 | }); 208 | }) 209 | .catch((err) => setError(err)) 210 | .finally((info) => { 211 | setIsDeleting(false); 212 | }); 213 | }; 214 | 215 | const Title = ({location}) => { 216 | if (location && location !== '/') { 217 | return ( 218 | 219 | Add cards to the{' '} 220 | {location.replace(/\/|-/g, ' ')} deck 221 | 222 | ); 223 | } else { 224 | return Edit a deck; 225 | } 226 | }; 227 | 228 | const EditADeck = ({title, reactGA}) => { 229 | const [whiteCard, setWhiteCard] = useState(''); 230 | const [blackCard, setBlackCard] = useState(''); 231 | const [initialDecks, setInitialDecks] = useState([]); 232 | const [filteredDecks, setFilteredDecks] = useState([]); 233 | const [deckTable, setDeckTable] = useState([]); 234 | const [deckExists, setDeckExists] = useState(''); 235 | const [error, setError] = useState(''); 236 | const [isLoading, setIsLoading] = useState(false); 237 | const [isDeleting, setIsDeleting] = useState(false); 238 | const [publicDecksInputVal, setPublicDecksInputVal] = useState(false); 239 | const defaultLocation = useLocation(); 240 | const location = defaultLocation.pathname.replace('/edit-deck', ''); 241 | 242 | useEffect(() => { 243 | // If we haven't chosen a deck and are just hitting the "/edit-deck" page 244 | if (!location || location === '/') { 245 | axios.get(`${SERVER_URL}/api/getPublicDecks`).then((res) => { 246 | console.log(res.data); 247 | setInitialDecks(res.data); 248 | }); 249 | } else { 250 | const secret = queryString.parse(defaultLocation.search).secret; 251 | async function checkSecret() { 252 | try { 253 | // check for the deck secret first 254 | await axios.post(`${SERVER_URL}/api/getDeckSecret`, { 255 | secret, 256 | deckName: location.replace('/', ''), 257 | }); 258 | // if secret is legit, keep going and get cards from the deck 259 | axios 260 | .get(`${SERVER_URL}/api/getCardsFromDeck${location}`) 261 | .then((res) => { 262 | if (res.data === 'no result') { 263 | return setDeckExists('no result'); 264 | } 265 | if (!secret) { 266 | return setError( 267 | "You don't have permissions to edit this deck." 268 | ); 269 | } 270 | if (res.data.includes('Error')) { 271 | return setError(res.data); 272 | } 273 | setDeckTable(res.data); 274 | setDeckExists('result found'); 275 | // Pop a success toast 276 | toast.info( 277 | 'Note: Bookmark or save this page. You can only update this deck with this exact link. Only send to people you trust.', 278 | { 279 | toastId: 'copy-link-info', 280 | position: toast.POSITION.TOP_CENTER, 281 | autoClose: 5000, 282 | } 283 | ); 284 | }); 285 | } catch (err) { 286 | console.log('Woahbhhhhh', err); 287 | return setError(err?.response?.data); 288 | } 289 | } 290 | checkSecret(); 291 | } 292 | }, [location, defaultLocation]); 293 | return ( 294 | 295 | 296 | 297 | {title} 298 | 299 | 300 | 301 | {location && location !== '/' ? ( 302 | <> 303 | {deckExists === 'result found' ? ( 304 | <> 305 | <p> 306 | <em> 307 | This deck has {getCardsLength({type: 'white', deckTable})} and{' '} 308 | {getCardsLength({type: 'black', deckTable})} 309 | </em> 310 | </p> 311 | 312 | <Form 313 | onSubmit={(e) => 314 | addCard({ 315 | e, 316 | setIsLoading, 317 | deckTable, 318 | type: 'white', 319 | text: whiteCard, 320 | initialDecks, 321 | setFilteredDecks, 322 | setDeckTable, 323 | location, 324 | setError, 325 | setWhiteCard, 326 | defaultLocation, 327 | reactGA, 328 | }) 329 | } 330 | > 331 | <InputWithLabel 332 | type="white" 333 | whiteCard={whiteCard} 334 | buttonText="ADD WHITE CARD" 335 | labelText="Add a White Card" 336 | onChange={setWhiteCard} 337 | placeholderText="e.g. Spontaneous combustion" 338 | isLoading={isLoading} 339 | /> 340 | </Form> 341 | {error && error.includes('white') && ( 342 | <ErrorText>{error}</ErrorText> 343 | )} 344 | 345 | <Form 346 | onSubmit={(e) => 347 | addCard({ 348 | e, 349 | setIsLoading, 350 | deckTable, 351 | type: 'black', 352 | text: blackCard, 353 | initialDecks, 354 | setFilteredDecks, 355 | setDeckTable, 356 | location, 357 | setError, 358 | setBlackCard, 359 | defaultLocation, 360 | reactGA, 361 | }) 362 | } 363 | > 364 | <InputWithLabel 365 | type="black" 366 | blackCard={blackCard} 367 | buttonText="ADD BLACK CARD" 368 | labelText="Add a Black Card" 369 | onChange={setBlackCard} 370 | placeholderText="e.g. Abraham Lincoln once said _______." 371 | isLoading={isLoading} 372 | /> 373 | </Form> 374 | {error && error.includes('black') && ( 375 | <ErrorText>{error}</ErrorText> 376 | )} 377 | <Subtitle>The cards in this deck so far</Subtitle> 378 | <Block> 379 | <Table headers={['White Cards']} color="white" isCollapsible> 380 | {deckTable && 381 | [...deckTable] 382 | .filter((card) => card.type === 'white') 383 | .map(({text, type, _id}, index) => ( 384 | <tr key={text}> 385 | <td style={{padding: '.5em'}}> 386 | <CardButton 387 | initialText={text} 388 | type={type} 389 | id={_id} 390 | deckTable={deckTable} 391 | setDeckTable={setDeckTable} 392 | location={location} 393 | setError={setError} 394 | defaultLocation={defaultLocation} 395 | reactGA={reactGA} 396 | /> 397 | </td> 398 | 399 | <td 400 | style={{textAlign: 'right', paddingRight: '.5em'}} 401 | > 402 | <RegularButton 403 | onClick={(e) => 404 | deleteCard({ 405 | e, 406 | setIsDeleting, 407 | type, 408 | text, 409 | setDeckTable, 410 | location, 411 | setError, 412 | defaultLocation, 413 | reactGA, 414 | }) 415 | } 416 | disabled={isDeleting} 417 | > 418 | <DeleteIcon /> 419 | </RegularButton> 420 | </td> 421 | </tr> 422 | ))} 423 | </Table> 424 | </Block> 425 | <Block> 426 | <Table headers={['Black Cards']} color="green" isCollapsible> 427 | {deckTable && 428 | [...deckTable] 429 | .filter((card) => card.type === 'black') 430 | .map(({text, type, _id}, index) => ( 431 | <tr key={text}> 432 | <td style={{padding: '.5em'}}> 433 | <CardButton 434 | initialText={text} 435 | type={type} 436 | id={_id} 437 | deckTable={deckTable} 438 | setDeckTable={setDeckTable} 439 | location={location} 440 | setError={setError} 441 | defaultLocation={defaultLocation} 442 | reactGA={reactGA} 443 | /> 444 | </td> 445 | <td 446 | style={{textAlign: 'right', paddingRight: '.5em'}} 447 | > 448 | <RegularButton 449 | onClick={(e) => 450 | deleteCard({ 451 | e, 452 | setIsDeleting, 453 | type, 454 | text, 455 | setDeckTable, 456 | location, 457 | setError, 458 | defaultLocation, 459 | reactGA, 460 | }) 461 | } 462 | disabled={isDeleting} 463 | > 464 | <DeleteIcon /> 465 | </RegularButton> 466 | </td> 467 | </tr> 468 | ))} 469 | </Table> 470 | </Block> 471 | </> 472 | ) : deckExists === 'no result' ? ( 473 | <> 474 | <p>Deck not found. Would you like to create one?</p> 475 | <Link to="/create-deck">Create Deck</Link> 476 | </> 477 | ) : error && 478 | (error.includes('permissions') || error.includes('exist')) ? ( 479 | <ErrorText>{error}</ErrorText> 480 | ) : ( 481 | <p>Loading...</p> 482 | )} 483 | </> 484 | ) : ( 485 | <Wrapper> 486 | <Label htmlFor="searchPublicDecks">Search Public Decks</Label> 487 | <Input 488 | type="text" 489 | onKeyUp={(e) => 490 | handleKeyUp({ 491 | e, 492 | initialDecks, 493 | setFilteredDecks, 494 | setPublicDecksInputVal, 495 | }) 496 | } 497 | /> 498 | {filteredDecks && filteredDecks.length > 0 && ( 499 | <ResultsList> 500 | {filteredDecks.map(({name}) => ( 501 | <li> 502 | <StyledLink to={`/edit-deck/${name}`}> 503 | {name.replace(/-/g, ' ')} 504 | </StyledLink> 505 | </li> 506 | ))} 507 | {publicDecksInputVal && 508 | filteredDecks && 509 | !filteredDecks.length && <li>No results found.</li>} 510 | </ResultsList> 511 | )} 512 | </Wrapper> 513 | )} 514 | <ToastContainer 515 | limit={1} 516 | autoClose={false} 517 | hideProgressBar 518 | closeOnClick 519 | transition={Slide} 520 | pauseOnFocusLoss={false} 521 | /> 522 | </Page> 523 | ); 524 | }; 525 | 526 | const CardButton = ({ 527 | initialText, 528 | type, 529 | id, 530 | deckTable, 531 | setDeckTable, 532 | location, 533 | setError, 534 | defaultLocation, 535 | reactGA, 536 | }) => { 537 | const [isActive, setIsActive] = useState(false); 538 | const [newText, setNewText] = useState(initialText); 539 | 540 | const editCard = () => { 541 | if (!newText.trim().length) { 542 | return setError( 543 | 'Please enter something, you know, more than 0 characters.' 544 | ); 545 | } 546 | 547 | // if text already exists in the deck, return error 548 | if ( 549 | deckTable.find( 550 | (card) => 551 | card.type === type && 552 | card.text.toLowerCase() === newText.toLowerCase() 553 | ) 554 | ) { 555 | return setError( 556 | `This same ${type} card has already been submitted. Please try again.` 557 | ); 558 | } 559 | 560 | const deckName = location.replace('/', ''); 561 | const secret = queryString.parse(defaultLocation.search).secret; 562 | 563 | axios 564 | .post(`${SERVER_URL}/api/editCard/`, { 565 | type, 566 | oldText: initialText, 567 | text: newText, 568 | deckName, 569 | secret, 570 | }) 571 | .then((res) => { 572 | // if successful, update state 573 | if (res.data.includes('Error')) { 574 | return setError(res.data); 575 | } 576 | 577 | setDeckTable((deckTable) => { 578 | const newDeckTable = [...deckTable]; 579 | const editCardIndex = newDeckTable.findIndex(({_id}) => _id === id); 580 | const theId = newDeckTable[editCardIndex]._id; 581 | newDeckTable.splice(editCardIndex, 1); 582 | newDeckTable.unshift({type, text: newText, _id: theId}); 583 | 584 | return newDeckTable; 585 | }); 586 | setError(''); 587 | 588 | reactGA.event({ 589 | category: 'Deck', 590 | action: `Edited a ${type} card from the ${deckName} deck`, 591 | label: newText, 592 | }); 593 | 594 | toast.info( 595 | `${ 596 | type.charAt(0).toUpperCase() + type.slice(1) 597 | } card succesfully saved as "${newText}"`, 598 | { 599 | toastId: `edit-card-info${newText}`, 600 | position: toast.POSITION.BOTTOM_CENTER, 601 | autoClose: 5000, 602 | } 603 | ); 604 | }) 605 | .catch((err) => setError(err)) 606 | .finally((info) => {}); 607 | }; 608 | 609 | const handleBlur = () => { 610 | if (newText.trim() !== initialText) { 611 | editCard(); 612 | setIsActive(false); 613 | } 614 | }; 615 | 616 | const handleChange = (e) => { 617 | setNewText(e.target.value); 618 | }; 619 | 620 | const handleSubmit = (e) => { 621 | e.preventDefault(); 622 | handleBlur(); 623 | }; 624 | 625 | if (isActive) { 626 | return ( 627 | <form onSubmit={handleSubmit}> 628 | <CardTextInput 629 | type="text" 630 | value={newText} 631 | onBlur={handleBlur} 632 | onChange={handleChange} 633 | autoFocus 634 | /> 635 | </form> 636 | ); 637 | } 638 | 639 | return ( 640 | <CardTextButton 641 | onClick={() => { 642 | setIsActive((prevIsActive) => !prevIsActive); 643 | }} 644 | > 645 | {initialText} 646 | </CardTextButton> 647 | ); 648 | }; 649 | 650 | const CardTextInput = styled.input` 651 | background: transparent; 652 | font: inherit; 653 | color: inherit; 654 | padding: 1px 6px; 655 | width: 100%; 656 | text-align: left; 657 | `; 658 | const CardTextButton = styled.button` 659 | appearance: none; 660 | background: transparent; 661 | font: inherit; 662 | color: inherit; 663 | padding: 1px 6px; 664 | cursor: text; 665 | width: 100%; 666 | text-align: left; 667 | `; 668 | const Block = styled.div` 669 | display: flex; 670 | justify-content: center; 671 | width: 100%; 672 | margin-top: 2em; 673 | `; 674 | const Page = styled.div` 675 | min-height: 100%; 676 | display: flex; 677 | flex-direction: column; 678 | justify-content: center; 679 | align-items: center; 680 | `; 681 | const Form = styled.form` 682 | width: 100%; 683 | max-width: 270px; 684 | `; 685 | const GreenText = styled.span` 686 | color: #2cce9f; 687 | `; 688 | const MainHeading = styled.h1` 689 | color: #fff; 690 | margin: 0; 691 | font-weight: normal; 692 | font-size: 2em; 693 | `; 694 | const NameOfDeck = styled.em` 695 | background: linear-gradient( 696 | 90deg, 697 | rgb(64, 224, 208), 698 | rgb(255, 140, 0), 699 | rgb(255, 0, 128) 700 | ); 701 | border-radius: 8px; 702 | padding: 0 0.25em 0; 703 | color: #000; 704 | white-space: nowrap; 705 | overflow: hidden; 706 | text-overflow: ellipsis; 707 | max-width: 100%; 708 | display: inline-block; 709 | vertical-align: bottom; 710 | text-transform: capitalize; 711 | `; 712 | const Input = styled.input` 713 | appearance: none; 714 | font-size: 1em; 715 | border: 0; 716 | margin: 0; 717 | padding: 0.5em 0 0.3em; 718 | background: transparent; 719 | border-bottom: 1px solid #fff; 720 | transition: border-color 0.25s; 721 | border-radius: 0; 722 | color: #fff; 723 | 724 | &:hover, 725 | &:focus { 726 | outline: 0; 727 | border-color: #2cce9f; 728 | } 729 | `; 730 | const Label = styled.label` 731 | text-align: left; 732 | text-transform: uppercase; 733 | font-size: 0.813em; 734 | display: block; 735 | font-weight: bold; 736 | `; 737 | const Wrapper = styled.div` 738 | position: relative; 739 | display: flex; 740 | flex-direction: column; 741 | width: 100%; 742 | max-width: 270px; 743 | justify-content: center; 744 | margin: 2em auto; 745 | `; 746 | const ResultsList = styled.ul` 747 | list-style: none; 748 | padding: 1em 0; 749 | position: absolute; 750 | top: calc(100% - 1px); 751 | width: 100%; 752 | border: 1px solid #2cce9f; 753 | margin: 0; 754 | border-radius: 0 0 8px 8px; 755 | max-height: 139px; 756 | overflow: auto; 757 | `; 758 | const StyledLink = styled(Link)` 759 | display: block; 760 | color: #2cce9f; 761 | padding: 0.5em; 762 | transition: color 0.25s; 763 | text-transform: capitalize; 764 | 765 | &:hover, 766 | &:focus { 767 | color: #fff; 768 | text-decoration: none; 769 | } 770 | `; 771 | const Subtitle = styled.h2` 772 | font-weight: normal; 773 | margin: 1em 0 0; 774 | `; 775 | const RegularButton = styled.button` 776 | appearance: none; 777 | transition: color 0.25s; 778 | background: transparent; 779 | color: #fff; 780 | padding: 0.5em; 781 | 782 | @media (hover) { 783 | &:hover { 784 | color: red; 785 | } 786 | } 787 | &:focus { 788 | color: red; 789 | outline: 0; 790 | } 791 | `; 792 | 793 | export default EditADeck; 794 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | <one line to give the program's name and a brief idea of what it does.> 633 | Copyright (C) <year> <name of author> 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see <https://www.gnu.org/licenses/>. 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | <https://www.gnu.org/licenses/>. --------------------------------------------------------------------------------