├── Procfile ├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── pictures │ │ ├── menu.png │ │ ├── userPage.png │ │ ├── gameDetails.png │ │ ├── gameOverview.png │ │ └── loginScreen.png │ ├── api │ │ ├── fetchMechanics.js │ │ ├── GetDataFromApi.js │ │ ├── fetchGames.js │ │ ├── fetchWishlist.js │ │ └── fetchUser.js │ ├── animations │ │ ├── slideIn.js │ │ ├── morph.js │ │ ├── textFadeIn.js │ │ └── alert.js │ ├── components │ │ ├── User-page.js │ │ │ ├── UserName.js │ │ │ ├── CompareFunction.js │ │ │ ├── UserNameArea.js │ │ │ ├── MainContent.js │ │ │ ├── Title.js │ │ │ ├── RoundPicture.js │ │ │ ├── UserNavigation.js │ │ │ ├── NameText.js │ │ │ ├── AmountGames.js │ │ │ ├── RecentlyAdded.js │ │ │ ├── NavBar.js │ │ │ └── Placeholder.js │ │ ├── popup-card │ │ │ ├── Grid.js │ │ │ ├── GameName.js │ │ │ ├── Players.js │ │ │ ├── TrimText.js │ │ │ ├── RatingContainer.js │ │ │ ├── DescriptionContainer.js │ │ │ ├── DetailButton.js │ │ │ ├── ModalContainer.js │ │ │ ├── RemoveButton.js │ │ │ ├── GamePositioned.js │ │ │ ├── AddButtonWishlist.js │ │ │ ├── ConfirmationMessage.js │ │ │ ├── AddButtonCollection.js │ │ │ ├── CardBack.js │ │ │ └── CardModal.js │ │ ├── IconButton.js │ │ ├── user-management │ │ │ ├── Form.js │ │ │ ├── InputField.js │ │ │ ├── LoginForm.js │ │ │ └── RegisterForm.js │ │ ├── library │ │ │ ├── BrowseEmpty.js │ │ │ ├── NavigationMenuButton.js │ │ │ ├── CollectionItem.js │ │ │ ├── OptionBox.js │ │ │ ├── SortModal.js │ │ │ ├── SearchBar.js │ │ │ ├── CollectionGrid.js │ │ │ └── LibraryNav.js │ │ ├── Header.js │ │ └── BurgerMenuList.js │ ├── stories │ │ ├── Collection.stories.js │ │ ├── SortModal.stories.js │ │ ├── Searchbar.stories.js │ │ ├── LibraryNav.stories.js │ │ ├── AddButton.stories.js │ │ ├── Header.stories.js │ │ ├── OptionBox.stories.js │ │ ├── IconButtons.stories.js │ │ ├── CollectionItem.stories.js │ │ ├── Icons.stories.js │ │ ├── NavigationMenu.stories.js │ │ └── DetailCard.stories.js │ ├── App.test.js │ ├── themes │ │ └── dark.js │ ├── pages │ │ ├── UserFriends.js │ │ ├── NotFound.js │ │ ├── UserStatistics.js │ │ ├── Impressum.js │ │ ├── login.js │ │ ├── register.js │ │ ├── LibraryBrowse.js │ │ ├── UserOverview.js │ │ ├── LibraryGames.js │ │ ├── LibraryWishlist.js │ │ ├── Library.js │ │ └── User.js │ ├── icons │ │ ├── BurgerMenuIcon.js │ │ ├── DoubleArrow.js │ │ ├── StarRatingEmpty.js │ │ ├── Magnifier.js │ │ ├── StarRatingFull.js │ │ ├── Clear.js │ │ ├── VerticalPoints.js │ │ ├── Logout.js │ │ ├── Star.js │ │ ├── Dice.js │ │ ├── Upload.js │ │ ├── Person.js │ │ ├── Wishlist.js │ │ ├── WoodSign.js │ │ └── Hero.js │ ├── index.js │ ├── GlobalStyles.js │ ├── hooks.js │ ├── App.js │ └── serviceWorker.js ├── .storybook │ ├── addons.js │ ├── preview-head.html │ └── config.js └── package.json ├── app.json ├── .gitignore ├── lib ├── sessions.js ├── database.js ├── users.js ├── games.js └── wishlist.js ├── README.md ├── now.json ├── LICENSE ├── package.json └── index.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/src/pictures/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/src/pictures/menu.png -------------------------------------------------------------------------------- /client/src/pictures/userPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/src/pictures/userPage.png -------------------------------------------------------------------------------- /client/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | -------------------------------------------------------------------------------- /client/src/pictures/gameDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/src/pictures/gameDetails.png -------------------------------------------------------------------------------- /client/src/pictures/gameOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/src/pictures/gameOverview.png -------------------------------------------------------------------------------- /client/src/pictures/loginScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsmunsch/boardhero/HEAD/client/src/pictures/loginScreen.png -------------------------------------------------------------------------------- /client/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /client/src/api/fetchMechanics.js: -------------------------------------------------------------------------------- 1 | export function getMechanics() { 2 | return fetch(`/api/mechanics`).then(response => response.json()); 3 | } 4 | -------------------------------------------------------------------------------- /client/src/api/GetDataFromApi.js: -------------------------------------------------------------------------------- 1 | export function getGamesFromApi(name) { 2 | return fetch(`/api/search?name=${name}`).then(response => response.json()); 3 | } 4 | -------------------------------------------------------------------------------- /client/src/animations/slideIn.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components"; 2 | 3 | export const slideIn = keyframes` 4 | 0% { width: 0px;} 5 | 100% { width: 100%} 6 | `; 7 | -------------------------------------------------------------------------------- /client/src/animations/morph.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components"; 2 | 3 | export const morph = keyframes` 4 | 0% { transform: translateX(-100%)} 5 | 100% { transform: translateX(0)} 6 | `; 7 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/UserName.js: -------------------------------------------------------------------------------- 1 | import react from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.section` 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | -------------------------------------------------------------------------------- /client/src/stories/Collection.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Library from "../pages/Library"; 3 | 4 | export default { 5 | title: "CollectionPage" 6 | }; 7 | 8 | export const CollectionPage = () => ; 9 | -------------------------------------------------------------------------------- /client/src/stories/SortModal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SortModal from "../components/SortModal"; 3 | 4 | export default { 5 | title: "SortModal" 6 | }; 7 | 8 | export const twoSortModal = () => ; 9 | -------------------------------------------------------------------------------- /client/src/stories/Searchbar.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SearchBar from "../components/SearchBar"; 3 | 4 | export default { 5 | title: "SearchBarwf" 6 | }; 7 | 8 | export const SearchBarPreview = () => ; 9 | -------------------------------------------------------------------------------- /client/src/components/popup-card/Grid.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Grid = styled.section` 4 | display: grid; 5 | grid-template-rows: 60px 1fr; 6 | margin-left: 15px; 7 | `; 8 | 9 | export default Grid; 10 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/CompareFunction.js: -------------------------------------------------------------------------------- 1 | export default function compare(a, b) { 2 | if (a.date_added > b.date_added) { 3 | return -1; 4 | } 5 | if (a.date_added < b.date_added) { 6 | return 1; 7 | } 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/stories/LibraryNav.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LibraryNav from "../components/LibraryNav"; 3 | 4 | export default { 5 | title: "LibraryNav" 6 | }; 7 | 8 | export const LibraryNavbar = () => { 9 | return ; 10 | }; 11 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/UserNameArea.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const UserNameArea = styled.div` 4 | height: 13%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-around; 8 | `; 9 | 10 | export default UserNameArea; 11 | -------------------------------------------------------------------------------- /client/src/components/IconButton.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const IconButton = styled.button` 4 | border: none; 5 | outline: none; 6 | fill: white; 7 | width: 40px; 8 | height: 40px; 9 | background: none; 10 | `; 11 | 12 | export default IconButton; 13 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/MainContent.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const MainContent = styled.section` 4 | display: flex; 5 | flex-direction: column; 6 | overflow: auto; 7 | flex-grow: 1; 8 | max-height: 50%; 9 | `; 10 | 11 | export default MainContent; 12 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/Title.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Title = styled.div` 4 | display: flex; 5 | font-size: 1.3em; 6 | margin-top: 20px; 7 | justify-content: flex-start; 8 | margin-left: 8%; 9 | min-height: 15px; 10 | `; 11 | 12 | export default Title; 13 | -------------------------------------------------------------------------------- /client/src/components/popup-card/GameName.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const GameName = styled.h1` 4 | color: ${props => props.theme.brightEffect}; 5 | text-align: center; 6 | z-index: 20; 7 | font-size: 1.8em; 8 | margin: 30px 0 15px 0; 9 | `; 10 | 11 | export default GameName; 12 | -------------------------------------------------------------------------------- /client/src/themes/dark.js: -------------------------------------------------------------------------------- 1 | const darkTheme = { 2 | main: "#22262F", 3 | lightFont: "#fff", 4 | darkFont: "#9F9898", 5 | highlight: "#6F7477", 6 | lightBackground: "#707070", 7 | accent: "#404345", 8 | brightEffect: "#F0A42A", 9 | mainLight: "#2b303b" 10 | }; 11 | 12 | export default darkTheme; 13 | -------------------------------------------------------------------------------- /client/src/components/user-management/Form.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Form = styled.form` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | margin: auto; 9 | width: 80%; 10 | max-width: 400px; 11 | `; 12 | 13 | export default Form; 14 | -------------------------------------------------------------------------------- /client/src/components/popup-card/Players.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Players = styled.div` 4 | display: flex; 5 | justify-content: flex-start; 6 | flex-direction: column; 7 | margin: 0px 4px; 8 | fill: white; 9 | color: white; 10 | font-size: 1.4em; 11 | `; 12 | 13 | export default Players; 14 | -------------------------------------------------------------------------------- /client/src/stories/AddButton.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AddButton from "../components/AddButtonCollection"; 3 | import Star from "../icons/Star"; 4 | 5 | export default { 6 | title: "AddToLibrary" 7 | }; 8 | 9 | export const AddCollection = () => ( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /client/src/components/library/BrowseEmpty.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const BrowseEmpty = styled.div` 4 | display: flex; 5 | font-size: 26px; 6 | color: white; 7 | align-items: center; 8 | text-align: center; 9 | height: 60vh; 10 | justify-content: center; 11 | padding: 25px; 12 | `; 13 | 14 | export default BrowseEmpty; 15 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/RoundPicture.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const PictureContainer = styled.img` 4 | width: 90px; 5 | height: 90px; 6 | border-radius: 50%; 7 | border: 2px solid ${props => props.theme.brightEffect}; 8 | box-shadow: 2px 2px 2px 2px rgba(0, 0, 0, 0.15); 9 | `; 10 | 11 | export default PictureContainer; 12 | -------------------------------------------------------------------------------- /client/src/components/popup-card/TrimText.js: -------------------------------------------------------------------------------- 1 | export default function TrimText(str, length, ending) { 2 | if (length == null) { 3 | length = 100; 4 | } 5 | if (ending == null) { 6 | ending = "..."; 7 | } 8 | if (str.length > length) { 9 | return str.substring(0, length - ending.length) + ending; 10 | } else { 11 | return str; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/popup-card/RatingContainer.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const RatingContainer = styled.div` 4 | position: absolute; 5 | top: 65px; 6 | width: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | color: white; 11 | margin-left: 10px; 12 | `; 13 | 14 | export default RatingContainer; 15 | -------------------------------------------------------------------------------- /client/src/animations/textFadeIn.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components"; 2 | 3 | export const textFadeIn = keyframes` 4 | 0% { 5 | opacity: 0; 6 | } 7 | 30% { 8 | opacity: 0; 9 | } 10 | 50% { 11 | opacity: 1; 12 | } 13 | 75% { 14 | opacity: 0; 15 | } 16 | 100% { 17 | opacity: 0; 18 | } 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /client/src/pages/UserFriends.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Placeholder from "../components/User-page.js/Placeholder"; 3 | 4 | export default function UserFriends() { 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/stories/Header.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "../components/Header"; 3 | import SearchBar from "../components/SearchBar"; 4 | 5 | export default { 6 | title: "Header" 7 | }; 8 | 9 | export const HeaderBar = () =>
; 10 | 11 | export const HeaderBarWithSearch = () => ( 12 |
13 | 14 |
15 | ); 16 | -------------------------------------------------------------------------------- /client/src/stories/OptionBox.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import OptionBox from "../components/OptionBox"; 3 | import Header from "../components/Header"; 4 | 5 | export default { 6 | title: "OptionBox" 7 | }; 8 | 9 | export const SortFilterBox = () => ; 10 | 11 | export const HeaderWithOptionBox = () => ( 12 | <> 13 |
14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boardhero", 3 | "scripts": {}, 4 | "env": { 5 | "DB_NAME": { 6 | "required": true 7 | }, 8 | "DB_URL": { 9 | "required": true 10 | } 11 | }, 12 | "formation": { 13 | "web": { 14 | "quantity": 1 15 | } 16 | }, 17 | "addons": [], 18 | "buildpacks": [ 19 | { 20 | "url": "heroku/nodejs" 21 | } 22 | ], 23 | "stack": "heroku-18" 24 | } 25 | -------------------------------------------------------------------------------- /client/src/icons/BurgerMenuIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function BurgerMenuIcon() { 4 | return ( 5 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/icons/DoubleArrow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function DoubleArrow() { 4 | return ( 5 | 11 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.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 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .FakeData.js 26 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/UserNavigation.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { BrowserRouter as Router, Link } from "react-router-dom"; 3 | 4 | const StyledLink = styled(Link)` 5 | text-decoration: none; 6 | color: inherit; 7 | padding: 10px; 8 | font-size: 1.1em; 9 | border-bottom: ${props => 10 | props.selected ? `3px solid ${props.theme.brightEffect}` : "none"}; 11 | `; 12 | 13 | export default StyledLink; 14 | -------------------------------------------------------------------------------- /client/src/components/popup-card/DescriptionContainer.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const DescriptionContainer = styled.div` 4 | display: flex; 5 | fill: white; 6 | color: white; 7 | justify-content: center; 8 | margin: 0px 20px 0 10px; 9 | max-height: 40vh; 10 | @media (min-height: 750px) { 11 | max-height: 35vh; 12 | } 13 | overflow-x: scroll; 14 | font-size: 1.2em; 15 | `; 16 | 17 | export default DescriptionContainer; 18 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /lib/sessions.js: -------------------------------------------------------------------------------- 1 | const uuidv1 = require("uuid/v1"); 2 | 3 | const sessions = {}; 4 | 5 | function createSession(user) { 6 | const id = uuidv1(); 7 | sessions[id] = user; 8 | return id; 9 | } 10 | 11 | function getUserBySession(id) { 12 | return sessions[id]; 13 | } 14 | 15 | function deleteSession(id) { 16 | delete sessions[id]; 17 | } 18 | 19 | exports.createSession = createSession; 20 | exports.getUserBySession = getUserBySession; 21 | exports.deleteSession = deleteSession; 22 | -------------------------------------------------------------------------------- /client/src/components/popup-card/DetailButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import DoubleArrow from "../../icons/DoubleArrow"; 4 | 5 | const ButtonContainer = styled.button` 6 | background: transparent; 7 | fill: white; 8 | border: none; 9 | `; 10 | 11 | export default function DetailButton({ onClick }) { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/user-management/InputField.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const InputField = styled.input` 4 | padding: 10px; 5 | margin-top: 10px; 6 | font-size: 1.2em; 7 | width: inherit; 8 | border-radius: 2px; 9 | outline: none; 10 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 11 | border: none; 12 | background-color: ${props => 13 | props.highlight ? props.theme.brightEffect : "white"}; 14 | -webkit-appearance: none; 15 | `; 16 | 17 | export default InputField; 18 | -------------------------------------------------------------------------------- /client/src/GlobalStyles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | html, body { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | *, *::after, *::before { 9 | box-sizing: border-box; 10 | } 11 | body { 12 | font-family: Roboto, sans-serif; 13 | height: 100vh; 14 | width: 100vw; 15 | background: ${props => props.theme.main}; 16 | } 17 | 18 | button { 19 | cursor: pointer; 20 | } 21 | `; 22 | 23 | export default GlobalStyles; 24 | -------------------------------------------------------------------------------- /client/src/components/popup-card/ModalContainer.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ModalContainer = styled.div` 4 | width: 320px; 5 | height: 480px; 6 | right: 0; 7 | left: 0; 8 | margin-left: auto; 9 | margin-right: auto; 10 | top: 50%; 11 | margin-top: -240px; 12 | background: ${props => props.theme.main}; 13 | position: fixed; 14 | z-index: 2; 15 | border: 4px solid white; 16 | border-radius: 15px; 17 | backdrop-filter: blur(2px); 18 | `; 19 | 20 | export default ModalContainer; 21 | -------------------------------------------------------------------------------- /client/src/components/popup-card/RemoveButton.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const RemoveButton = styled.button` 4 | border-radius: 15px; 5 | background-color: ${props => props.theme.brightEffect}; 6 | position: absolute; 7 | bottom: 10px; 8 | right: 50%; 9 | margin-right: -65px; 10 | height: 40px; 11 | width: 130px; 12 | border: none; 13 | outline: none; 14 | box-shadow: 4px 4px 6px 4px rgba(0, 0, 0, 0.15); 15 | font-size: 1.1em; 16 | color: white; 17 | `; 18 | 19 | export default RemoveButton; 20 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const FlexContainer = styled.section` 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100vw; 9 | height: 100vh; 10 | `; 11 | 12 | const Styledh1 = styled.h1` 13 | color: ${props => props.theme.brightEffect}; 14 | font-size: 2em; 15 | `; 16 | 17 | export default function NotFound() { 18 | return ( 19 | 20 | Error: 404 Not found 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/src/pages/UserStatistics.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { getMechanics } from "../api/fetchMechanics"; 3 | import Placeholder from "../components/User-page.js/Placeholder"; 4 | 5 | export default function UserStatistics() { 6 | const [mechanics, setMechanics] = useState(null); 7 | useEffect(() => { 8 | getMechanics().then(mechanics => setMechanics(mechanics)); 9 | }, []); 10 | 11 | return ( 12 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/popup-card/GamePositioned.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { ImageContainer } from "../library/CollectionItem"; 3 | 4 | export const GamePositioned = styled(ImageContainer)` 5 | position: absolute; 6 | top: -60px; 7 | margin-top: 0; 8 | left: 50%; 9 | margin-left: -50px; 10 | height: 120px; 11 | width: 120px; 12 | @media (min-width: 600px) { 13 | width: 120px; 14 | height: 120px; 15 | margin-left: -60px; 16 | } 17 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 18 | `; 19 | 20 | export default GamePositioned; 21 | -------------------------------------------------------------------------------- /client/src/icons/StarRatingEmpty.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function StarRatingEmpty() { 4 | return ( 5 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | const { MongoClient } = require("mongodb"); 2 | let db = null; 3 | async function initDatabase(url, dbName) { 4 | const client = new MongoClient(url, { 5 | useNewUrlParser: true, 6 | useUnifiedTopology: true 7 | }); 8 | await client.connect(); 9 | db = client.db(process.env.DB_NAME); 10 | } 11 | 12 | async function getCollections(collectionName) { 13 | if (!db) { 14 | throw new Error("You have to initialize the database first"); 15 | } 16 | return db.collection(collectionName); 17 | } 18 | 19 | exports.initDatabase = initDatabase; 20 | exports.getCollection = getCollections; 21 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 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 | -------------------------------------------------------------------------------- /client/src/api/fetchGames.js: -------------------------------------------------------------------------------- 1 | export async function getGamesCollection() { 2 | return fetch("/api/games").then(response => response.json()); 3 | } 4 | 5 | export function newGame(game) { 6 | return fetch("/api/games", { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "application/json" 10 | }, 11 | 12 | body: JSON.stringify(game) 13 | }); 14 | } 15 | 16 | export function removeGameEntry(game) { 17 | return fetch("/api/gamesremove", { 18 | method: "POST", 19 | headers: { 20 | "Content-Type": "application/json" 21 | }, 22 | body: JSON.stringify(game) 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/api/fetchWishlist.js: -------------------------------------------------------------------------------- 1 | export function getWishlistCollection() { 2 | return fetch("/api/wishlist").then(response => response.json()); 3 | } 4 | 5 | export function newWishlistEntry(game) { 6 | return fetch("/api/wishlist", { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "application/json" 10 | }, 11 | body: JSON.stringify(game) 12 | }); 13 | } 14 | 15 | export function removeGameWishlist(game) { 16 | return fetch("/api/wishlistremove", { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json" 20 | }, 21 | body: JSON.stringify(game) 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/NameText.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | max-width: 70%; 8 | `; 9 | 10 | const Name = styled.span` 11 | font-style: bold; 12 | margin: 0; 13 | font-size: 1.4em; 14 | `; 15 | 16 | const Description = styled.div` 17 | font-weight: 100; 18 | `; 19 | 20 | export default function NameText({ name, description }) { 21 | return ( 22 | 23 | {name} 24 | {description} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client/src/components/library/NavigationMenuButton.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const NavigationMenuButton = styled.button` 4 | display: flex; 5 | width: inherit; 6 | height: 8vh; 7 | align-items: center; 8 | justify-content: flex-start; 9 | border: none; 10 | border-left: ${props => (props.selected ? "3px solid #F0A42A" : "none")}; 11 | background-color: ${props => 12 | props.selected ? props.theme.highlight : props.theme.accent}; 13 | color: ${props => 14 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 15 | font-size: 20px; 16 | `; 17 | 18 | export default NavigationMenuButton; 19 | -------------------------------------------------------------------------------- /client/src/icons/Magnifier.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Magnifier() { 4 | return ( 5 | 13 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/AmountGames.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | max-width: 70%; 8 | margin-left: 30px; 9 | `; 10 | 11 | const Amount = styled.span` 12 | font-style: bold; 13 | margin: 0; 14 | font-size: 1.2em; 15 | `; 16 | 17 | const Description = styled.div` 18 | font-weight: 100; 19 | `; 20 | 21 | export default function AmountGames({ name, description }) { 22 | return ( 23 | 24 | {name} 25 | {description} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/src/stories/IconButtons.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconButton from "../components/IconButton"; 3 | import BurgerMenu from "../icons/BurgerMenuIcon"; 4 | import Magnifier from "../icons/Magnifier"; 5 | import VerticalPoints from "../icons/VerticalPoints"; 6 | 7 | export default { 8 | title: "IconButtons" 9 | }; 10 | 11 | export const BurgerMenuButton = () => ( 12 | 13 | 14 | 15 | ); 16 | 17 | export const SearchButton = () => ( 18 | 19 | 20 | 21 | ); 22 | 23 | export const OptionButton = () => ( 24 | 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /client/src/icons/StarRatingFull.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function StarRatingFull() { 4 | return ( 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/RecentlyAdded.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import CollectionItem from "../library/CollectionItem"; 4 | import AmountGames from "./AmountGames"; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | align-items: center; 9 | margin-left: 7%; 10 | `; 11 | 12 | const Span = styled.span` 13 | margin-top: 20px; 14 | `; 15 | 16 | export default function RecentlyAdded({ src, name, description }) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/icons/Clear.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function ClearIcon({ className }) { 5 | return ( 6 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const StyledClearIcon = styled(ClearIcon)``; 21 | export default function Clear({ onClick }) { 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/icons/VerticalPoints.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function VerticalPoints() { 4 | return ( 5 | 12 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { configure, addDecorator } from "@storybook/react"; 3 | import GlobalStyles from "../src/GlobalStyles"; 4 | import styled, { ThemeProvider } from "styled-components"; 5 | import darkTheme from "../src/themes/dark"; 6 | 7 | // automatically import all files ending in *.stories.js 8 | configure(require.context("../src/stories", true, /\.stories\.js$/), module); 9 | 10 | const Main = styled.div` 11 | margin: 10px; 12 | `; 13 | 14 | // add GlobalStyle for every story 15 | const GlobalStyleDecorator = storyFn => ( 16 | 17 |
18 | 19 | {storyFn()} 20 |
21 |
22 | ); 23 | addDecorator(GlobalStyleDecorator); 24 | -------------------------------------------------------------------------------- /client/src/pages/Impressum.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "../components/Header"; 3 | import styled from "styled-components"; 4 | 5 | const Section = styled.section` 6 | display: flex; 7 | margin: 20px; 8 | color: white; 9 | flex-direction: column; 10 | `; 11 | 12 | export default function Impressum() { 13 | return ( 14 | <> 15 |
16 |
17 |

Impressum

18 | 19 | Videogame Vectors by Vecteezy 20 | 21 | 22 | Batman Icon by Laura Reen 23 | 24 |
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client/src/api/fetchUser.js: -------------------------------------------------------------------------------- 1 | export function fetchUser() { 2 | return fetch("/api/users").then(response => response.json()); 3 | } 4 | 5 | export function createUser(user) { 6 | return fetch("/api/users", { 7 | method: "POST", 8 | headers: { 9 | "Content-Type": "application/json" 10 | }, 11 | 12 | body: JSON.stringify(user) 13 | }); 14 | } 15 | 16 | export function validateCredentials(user) { 17 | return fetch("/api/login", { 18 | method: "POST", 19 | headers: { 20 | "Content-Type": "application/json" 21 | }, 22 | 23 | body: JSON.stringify(user) 24 | }); 25 | } 26 | 27 | export function unsetUser() { 28 | return fetch("/api/logout", { 29 | method: "POST", 30 | headers: { 31 | "Content-Type": "application/json" 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/icons/Logout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function LogoutIcon({ className }) { 5 | return ( 6 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | const LogoutIconStyled = styled(LogoutIcon)` 20 | fill: ${props => 21 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 22 | height: 32px; 23 | width: 32px; 24 | margin-right: 5px; 25 | `; 26 | 27 | export default function Logout({ selected }) { 28 | return ; 29 | } 30 | -------------------------------------------------------------------------------- /lib/users.js: -------------------------------------------------------------------------------- 1 | const { getCollection } = require("./database"); 2 | 3 | const collectionName = "users"; 4 | 5 | async function setUser(user) { 6 | const Users = await getCollection(collectionName); 7 | await Users.insertOne(user); 8 | } 9 | 10 | async function getUsers(email) { 11 | const UsersCollection = await getCollection(collectionName); 12 | const User = await UsersCollection.findOne({ email: email }); 13 | return User; 14 | } 15 | 16 | async function validateUser(credentials) { 17 | const UsersCollection = await getCollection(collectionName); 18 | const Password = await UsersCollection.findOne({ 19 | email: credentials.email, 20 | password: credentials.password 21 | }); 22 | return Password; 23 | } 24 | 25 | exports.setUser = setUser; 26 | exports.getUsers = getUsers; 27 | exports.validateUser = validateUser; 28 | -------------------------------------------------------------------------------- /client/src/components/library/CollectionItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | export const ImageContainer = styled.img` 5 | width: 88px; 6 | height: 88px; 7 | margin-top: 20px; 8 | @media (min-width: 600px) { 9 | width: 176px; 10 | height: 176px; 11 | margin-left: -35px; 12 | :hover { 13 | transform: scale(1.3); 14 | } 15 | } 16 | border: 4px solid white; 17 | object-fit: cover; 18 | object-position: center center; 19 | border-radius: 15px; 20 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 21 | `; 22 | 23 | export default function CollectionItem({ game, src, onClick, className }) { 24 | return ( 25 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /client/src/stories/CollectionItem.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CollectionItem from "../components/CollectionItem"; 3 | import CollectionGrid from "../components/CollectionGrid"; 4 | 5 | export default { 6 | title: "GameCollection" 7 | }; 8 | 9 | export const singleGame = () => ; 10 | 11 | export const multipleGames = () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /lib/games.js: -------------------------------------------------------------------------------- 1 | const { getCollection } = require("./database"); 2 | 3 | const collectionName = "games"; 4 | 5 | async function setGames(game) { 6 | const gamesCollection = await getCollection(collectionName); 7 | const gameExists = await gamesCollection.findOne(game); 8 | if (gameExists) return; 9 | await gamesCollection.insertOne(game); 10 | } 11 | 12 | async function getGames(owner) { 13 | const gamesCollection = await getCollection(collectionName); 14 | const games = await gamesCollection.find({ owner: owner }).toArray(); 15 | return games; 16 | } 17 | 18 | async function removeGame(name) { 19 | const gamesCollection = await getCollection(collectionName); 20 | const removedGame = await gamesCollection.deleteOne({ name: name }); 21 | return removedGame; 22 | } 23 | 24 | exports.setGames = setGames; 25 | exports.getGames = getGames; 26 | exports.removeGame = removeGame; 27 | -------------------------------------------------------------------------------- /client/src/stories/Icons.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BurgerMenu from "../icons/BurgerMenuIcon"; 3 | import Dice from "../icons/Dice"; 4 | import Logout from "../icons/Logout"; 5 | import Star from "../icons/Star"; 6 | import Upload from "../icons/Upload"; 7 | import Magnifier from "../icons/Magnifier"; 8 | import VerticalPoints from "../icons/VerticalPoints"; 9 | import DoubleArrow from "../icons/DoubleArrow"; 10 | 11 | export default { 12 | title: "Icons" 13 | }; 14 | 15 | export const MenuIcon = () => ; 16 | export const CollectionIcon = () => ; 17 | export const LogoutIcon = () => ; 18 | export const WishlistIcon = () => ; 19 | export const UploadIcon = () => ; 20 | export const SearchIcon = () => ; 21 | export const FilterIcon = () => ; 22 | export const DetailIcon = () => ; 23 | -------------------------------------------------------------------------------- /client/src/pages/login.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Hero from "../icons/Hero"; 4 | import LoginForm from "../components/user-management/LoginForm"; 5 | 6 | const FlexContainer = styled.section` 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | const VectorContainer = styled.div` 13 | width: 100%; 14 | height: 100%; 15 | margin: 5px; 16 | display: flex; 17 | justify-content: center; 18 | `; 19 | 20 | const Headline = styled.h1` 21 | text-align: center; 22 | color: white; 23 | font-size: 2.5em; 24 | `; 25 | 26 | export default function Login() { 27 | return ( 28 | 29 | Boardhero 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/src/pages/register.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Hero from "../icons/Hero"; 4 | import RegisterForm from "../components/user-management/RegisterForm"; 5 | 6 | const FlexContainer = styled.section` 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | const VectorContainer = styled.div` 13 | width: 100%; 14 | height: 100%; 15 | margin: 5px; 16 | display: flex; 17 | justify-content: center; 18 | `; 19 | 20 | const Headline = styled.h1` 21 | text-align: center; 22 | color: white; 23 | font-size: 2.5em; 24 | `; 25 | 26 | export default function Register() { 27 | return ( 28 | 29 | Boardhero 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/wishlist.js: -------------------------------------------------------------------------------- 1 | const { getCollection } = require("./database"); 2 | 3 | const collectionName = "wishlist"; 4 | 5 | async function setWishlist(game) { 6 | const gamesCollection = await getCollection(collectionName); 7 | const gameExists = await gamesCollection.findOne(game); 8 | if (gameExists) return; 9 | await gamesCollection.insertOne(game); 10 | } 11 | 12 | async function getWishlist(owner) { 13 | const gameCollection = await getCollection(collectionName); 14 | const Collection = await gameCollection.find({ owner: owner }).toArray(); 15 | return Collection; 16 | } 17 | 18 | async function removeWishlistEntry(name) { 19 | const gameCollection = await getCollection(collectionName); 20 | const game = await gameCollection.deleteOne({ name: name }); 21 | return game; 22 | } 23 | 24 | exports.setWishlist = setWishlist; 25 | exports.getWishlist = getWishlist; 26 | exports.removeWishlistEntry = removeWishlistEntry; 27 | -------------------------------------------------------------------------------- /client/src/pages/LibraryBrowse.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import BrowseEmpty from "../components/library/BrowseEmpty"; 3 | import CollectionGrid from "../components/library/CollectionGrid"; 4 | import { getGamesFromApi } from "../api/GetDataFromApi"; 5 | 6 | export default function LibraryBrowse({ searchbarInput }) { 7 | const [apiGame, setApiGame] = useState(null); 8 | useEffect(() => { 9 | getGamesFromApi(searchbarInput).then(games => setApiGame(games)); 10 | }, [searchbarInput]); 11 | return ( 12 | <> 13 | {!searchbarInput && ( 14 | 15 | Please use the searchbar to browse through our available games. 16 | 17 | )} 18 | {searchbarInput && apiGame && ( 19 | 24 | )} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | # boardhero 4 | 5 | A mobile first application to keep track of your boardgames. Create your own account with a digital portfolio of all the games you own and add the ones you want to your Wishlist. 6 | 7 | # Showcase 8 | 9 | ![Login](client/src/pictures/loginScreen.png) 10 | ![GameOverview](client/src/pictures/gameOverview.png) 11 | ![Details](client/src/pictures/gameDetails.png) 12 | ![userPage](client/src/pictures/userPage.png) 13 | ![Menu](client/src/pictures/menu.png) 14 | 15 | # Hosting 16 | 17 | The Website is currently hosted via heroku. 18 | 19 | [Boardhero](https://board-hero.herokuapp.com/) 20 | 21 | ## Usage 22 | 23 | Install client and server dependencies: 24 | 25 | ``` 26 | npm install 27 | cd client 28 | npm install 29 | ``` 30 | 31 | To start the server and client at the same time: 32 | 33 | ``` 34 | npm run dev 35 | ``` 36 | -------------------------------------------------------------------------------- /client/src/components/popup-card/AddButtonWishlist.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const StyledButton = styled.button` 5 | border-radius: 15px; 6 | background-color: ${props => props.theme.brightEffect}; 7 | position: absolute; 8 | bottom: 10px; 9 | right: 15px; 10 | height: 40px; 11 | width: 135px; 12 | border: none; 13 | outline: none; 14 | box-shadow: 4px 4px 6px 4px rgba(0, 0, 0, 0.15); 15 | font-size: 1.1em; 16 | color: white; 17 | `; 18 | 19 | export default function AddButtonWishlist({ 20 | children, 21 | setStartAnimation, 22 | singleGame, 23 | newWishlistEntry 24 | }) { 25 | return ( 26 | { 28 | newWishlistEntry(singleGame); 29 | setStartAnimation(true); 30 | setTimeout(() => { 31 | setStartAnimation(false); 32 | }, 5000); 33 | }} 34 | > 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/popup-card/ConfirmationMessage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { alert } from "../../animations/alert"; 4 | import { textFadeIn } from "../../animations/textFadeIn"; 5 | 6 | const MessageBox = styled.div` 7 | background: ${props => props.theme.darkFont}; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | position: fixed; 12 | bottom: -5em; 13 | left: 50%; 14 | margin-left: -175px; 15 | width: 80%px; 16 | height: 65px; 17 | color: #fff; 18 | font-size: 22px; 19 | padding: 0 1.2em 0 0; 20 | z-index: 10; 21 | animation: ${alert} 2.5s ease-in-out; 22 | display: "block"; 23 | `; 24 | 25 | const MessageText = styled.span` 26 | animation: ${textFadeIn} 2.5s ease-in-out; 27 | display: "block"; 28 | `; 29 | 30 | export default function ConfirmationMessage() { 31 | return ( 32 | 33 | Game Succesfully added 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/src/icons/Star.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function StarIcon({ className }) { 5 | return ( 6 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | const StarIconStyled = styled(StarIcon)` 20 | fill: ${props => 21 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 22 | height: 32px; 23 | width: 32px; 24 | margin-right: 5px; 25 | `; 26 | 27 | export default function Star({ selected }) { 28 | return ; 29 | } 30 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boardhero", 3 | "version": 2, 4 | "builds": [ 5 | { 6 | "src": "client/package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "build" } 9 | }, 10 | { "src": "index.js", "use": "@now/node-server" } 11 | ], 12 | "routes": [ 13 | { 14 | "src": "/static/(.*)", 15 | "dest": "/client/static/$1" 16 | }, 17 | { 18 | "src": "/api/(.*)", 19 | "dest": "/index.js" 20 | }, 21 | { 22 | "src": "/logo192.png", 23 | "dest": "/client/logo192.png" 24 | }, 25 | { 26 | "src": "/logo512.png", 27 | "dest": "/client/logo512.png" 28 | }, 29 | { 30 | "src": "/favicon.ico", 31 | "dest": "/client/favicon.ico" 32 | }, 33 | { 34 | "src": "/manifest.json", 35 | "dest": "/client/manifest.json" 36 | }, 37 | { 38 | "src": "/(.*)", 39 | "dest": "/client/index.html" 40 | } 41 | ], 42 | "env": { 43 | "DB_URL": "@db_url", 44 | "DB_NAME": "@db_name" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/library/OptionBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const OptionBoxDiv = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | height: 120px; 8 | width: 170px; 9 | align-items: center; 10 | justify-content: center; 11 | background-color: ${props => props.theme.lightBackground}; 12 | border: none; 13 | position: absolute; 14 | top: 35px; 15 | right: 25px; 16 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 17 | `; 18 | 19 | const OptionButton = styled.button` 20 | height: 50%; 21 | width: 100%; 22 | background-color: inherit; 23 | border: inherit; 24 | color: white; 25 | font-size: 1.4em; 26 | outline: none; 27 | `; 28 | 29 | export default function OptionBox({ show, onClick }) { 30 | return ( 31 | <> 32 | {show && ( 33 | 34 | onClick("Sort")}>Sort 35 | Filter 36 | 37 | )} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/icons/Dice.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Cubes({ className }) { 5 | return ( 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | const DiceStyled = styled(Cubes)` 25 | fill: ${props => 26 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 27 | height: 32px; 28 | width: 32px; 29 | margin-right: 5px; 30 | `; 31 | 32 | export default function Dice({ selected }) { 33 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /client/src/components/popup-card/AddButtonCollection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | 5 | const StyledButton = styled.button` 6 | border-radius: 15px; 7 | background-color: ${props => props.theme.brightEffect}; 8 | position: absolute; 9 | bottom: 10px; 10 | left: 15px; 11 | height: 40px; 12 | width: 135px; 13 | border: none; 14 | color: white; 15 | outline: none; 16 | box-shadow: 4px 4px 6px 4px rgba(0, 0, 0, 0.15); 17 | font-size: 1.1em; 18 | `; 19 | 20 | export default function AddButtonCollection({ 21 | children, 22 | singleGame, 23 | setStartAnimation, 24 | newGame 25 | }) { 26 | return ( 27 | { 29 | newGame(singleGame); 30 | setStartAnimation(true); 31 | setTimeout(() => { 32 | setStartAnimation(false); 33 | }, 5000); 34 | }} 35 | > 36 | {children} 37 | 38 | ); 39 | } 40 | 41 | AddButtonCollection.propTypes = { 42 | children: PropTypes.string 43 | }; 44 | -------------------------------------------------------------------------------- /client/src/components/library/SortModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const PopupContainer = styled.div` 5 | background-color: ${props => props.theme.lightBackground}; 6 | color: white; 7 | padding: 30px; 8 | position: fixed; 9 | top: 100px; 10 | left: 50%; 11 | top: 50%; 12 | margin-left: -110px; 13 | margin-top: -80px; 14 | font-size: 1.4em; 15 | z-index: 3; 16 | display: ${props => (props.show ? "block" : "none")}; 17 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 18 | `; 19 | 20 | const RadioButtonContainer = styled.div` 21 | display: flex; 22 | margin-top: 10px; 23 | `; 24 | 25 | export default function SortModal({ show }) { 26 | return ( 27 | 28 | Sort 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/src/icons/Upload.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function UploadIcon({ className }) { 5 | return ( 6 | 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | const UploadIconStyled = styled(UploadIcon)` 20 | fill: ${props => 21 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 22 | height: 32px; 23 | width: 32px; 24 | margin-right: 5px; 25 | `; 26 | 27 | export default function Upload({ selected }) { 28 | return ; 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jsmunsch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/hooks.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | export const useOnClickOutside = (ref, handler) => { 4 | useEffect(() => { 5 | const listener = event => { 6 | if (!ref.current || ref.current.contains(event.target)) { 7 | return; 8 | } 9 | handler(event); 10 | }; 11 | document.addEventListener("touchstart", listener); 12 | document.addEventListener("mousedown", listener); 13 | return () => { 14 | document.removeEventListener("touchstart", listener); 15 | document.removeEventListener("mousedown", listener); 16 | }; 17 | }, [ref, handler]); 18 | }; 19 | 20 | export const useUser = () => { 21 | const [user, setUser] = React.useState(localStorage.getItem("user")); 22 | 23 | useEffect(() => { 24 | const handleStorageChange = () => { 25 | console.log("storage changed"); 26 | setUser(localStorage.getItem("user")); 27 | }; 28 | 29 | window.addEventListener("storage", handleStorageChange); 30 | 31 | return () => { 32 | window.removeEventListener("storage", handleStorageChange); 33 | }; 34 | }, []); 35 | 36 | return [user]; 37 | }; 38 | -------------------------------------------------------------------------------- /client/src/icons/Person.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function PersonIcon({ className }) { 5 | return ( 6 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const PersonStyled = styled(PersonIcon)` 18 | fill: ${props => 19 | props.selected ? props.theme.lightFont : props.theme.darkFont}; 20 | height: 32px; 21 | width: 32px; 22 | margin-right: 5px; 23 | `; 24 | 25 | export default function Person({ selected }) { 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /client/src/pages/UserOverview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MainContent from "../components/User-page.js/MainContent"; 3 | import Title from "../components/User-page.js/Title"; 4 | import RecentlyAdded from "../components/User-page.js/RecentlyAdded"; 5 | import Placeholder from "../components/User-page.js/Placeholder"; 6 | import { useHistory } from "react-router-dom"; 7 | 8 | export default function UserOverview({ filteredGames }) { 9 | let history = useHistory(); 10 | return ( 11 | 12 | {filteredGames.length === 0 && ( 13 | history.push("/library/browse")} 17 | /> 18 | )} 19 | {filteredGames.length > 0 && ( 20 | 21 | <span>Recently Added</span> 22 | 23 | )} 24 | {filteredGames.slice(0, 5).map(game => ( 25 | 31 | ))} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import StyledLink from "./UserNavigation"; 4 | import { 5 | BrowserRouter as Router, 6 | useRouteMatch, 7 | useLocation 8 | } from "react-router-dom"; 9 | 10 | const ScrollBar = styled.div` 11 | display: flex; 12 | height: 8%; 13 | overflow-x: auto; 14 | align-items: center; 15 | justify-content: space-around; 16 | border-top: 1px solid ${props => props.theme.brightEffect}; 17 | border-bottom: 1px solid ${props => props.theme.brightEffect}; 18 | `; 19 | 20 | export default function NavBar() { 21 | let { url } = useRouteMatch(); 22 | let location = useLocation(); 23 | return ( 24 | 25 | 29 | Overview 30 | 31 | 35 | Statistics 36 | 37 | 41 | Friends 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boardhero", 3 | "version": "1.0.0", 4 | "engines": { 5 | "node": "12.x" 6 | }, 7 | "description": "", 8 | "main": "server.js", 9 | "scripts": { 10 | "client": "cd client && npm start", 11 | "build": "cd client && npm run build", 12 | "dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"", 13 | "dev:server": "npm run build && cd .. && npm start", 14 | "server": "nodemon index.js", 15 | "start": "node index.js", 16 | "heroku-prebuild": "cd client && npm ci --only=prod", 17 | "reset": "killall -9 node" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/jsmunsch/boardhero" 22 | }, 23 | "author": "Jonas Munsch", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/jsmunsch/boardhero" 27 | }, 28 | "homepage": "https://github.com/jsmunsch/boardhero#readme", 29 | "dependencies": { 30 | "axios": "^0.19.0", 31 | "cookie-parser": "^1.4.4", 32 | "dotenv": "^8.2.0", 33 | "express": "^4.17.1", 34 | "mongodb": "^3.3.2", 35 | "node": "^12.13.0", 36 | "now": "^16.4.4", 37 | "uuid": "^3.3.3" 38 | }, 39 | "devDependencies": { 40 | "concurrently": "^5.0.0", 41 | "nodemon": "^1.19.3", 42 | "prettier": "^1.18.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/src/stories/NavigationMenu.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StyledMenuButton from "../components/NavigationMenuButton"; 3 | import Dice from "../icons/Dice"; 4 | import Person from "../icons/Person"; 5 | import Upload from "../icons/Upload"; 6 | import Star from "../icons/Star"; 7 | 8 | export default { 9 | title: "NavigationMenu" 10 | }; 11 | 12 | export const NavButtonInactive = () => ( 13 | 14 | 15 | Collection 16 | 17 | ); 18 | 19 | export const NavButtonActive = () => ( 20 | 21 | 22 | Peter 23 | 24 | ); 25 | 26 | export const NavigationMenu = () => ( 27 | <> 28 | onClick("Collection")} selected> 29 | 30 | Collection 31 | 32 | onClick("User")}> 33 | 34 | User 35 | 36 | onClick("Share")}> 37 | 38 | Share 39 | 40 | onClick("Wishlist")}> 41 | 42 | Wishlist 43 | 44 | 45 | ); 46 | 47 | function onClick(input) { 48 | console.log(input); 49 | } 50 | -------------------------------------------------------------------------------- /client/src/components/User-page.js/Placeholder.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import PropTypes from "prop-types"; 4 | 5 | const Container = styled.section` 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | margin: 15px; 10 | background: ${props => props.theme.mainLight}; 11 | flex-grow: 1; 12 | flex-direction: column; 13 | `; 14 | 15 | const TextContainer = styled.div` 16 | width: 80%; 17 | max-width: 420px; 18 | text-align: center; 19 | `; 20 | 21 | const FriendButton = styled.button` 22 | border-radius: 5px; 23 | width: 60%; 24 | max-width: 320px; 25 | height: 15%; 26 | margin: 20px; 27 | background: ${props => props.theme.brightEffect}; 28 | color: white; 29 | border: none; 30 | font-size: 1.1em; 31 | box-shadow: 4px 4px 6px 4px rgba(0, 0, 0, 0.15); 32 | padding: 20px; 33 | line-height: 1px; 34 | `; 35 | 36 | export default function Placeholder({ text, buttonText, onClick }) { 37 | return ( 38 | 39 | {text} 40 | 41 | {buttonText} 42 | 43 | 44 | ); 45 | } 46 | 47 | Placeholder.propTypes = { 48 | text: PropTypes.string, 49 | buttonText: PropTypes.string 50 | }; 51 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boardhero", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "prop-types": "^15.7.2", 8 | "react": "^16.10.2", 9 | "react-dom": "^16.10.2", 10 | "react-rating": "^2.0.0", 11 | "react-router-dom": "^5.1.2", 12 | "react-scripts": "3.2.0", 13 | "styled-components": "^4.4.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "storybook": "start-storybook -p 9009 -s public", 21 | "build-storybook": "build-storybook -s public" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@storybook/addon-actions": "^5.2.3", 40 | "@storybook/addon-links": "^5.2.3", 41 | "@storybook/addons": "^5.2.3", 42 | "@storybook/react": "^5.2.3", 43 | "eslint": "^6.5.1", 44 | "prettier": "^1.18.2", 45 | "dotenv": "^8.2.0" 46 | }, 47 | "proxy": "http://localhost:8080" 48 | } 49 | -------------------------------------------------------------------------------- /client/src/stories/DetailCard.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CollectionItemPositioned, { 3 | GamePositioned 4 | } from "../components/GamePositioned"; 5 | import CardModal from "../components/CardModal"; 6 | import CardBadge from "../components/CardBadge"; 7 | import CardDetails from "../components/CardDetails"; 8 | import CardCategories from "../components/CardCategories"; 9 | import CardTitle from "../components/CardTitle"; 10 | import CardFlip from "../components/CardFlip"; 11 | 12 | export default { 13 | title: "DetailCard" 14 | }; 15 | 16 | export const CardWithImage = () => ( 17 | 18 | 19 | 20 | ); 21 | 22 | export const test = () => ( 23 | 24 | 25 | Fest für Odin 26 | 27 | Players: 2-4 28 | Time: test 29 | Categories 30 | 31 | Economics 32 | Sci-Fi 33 | Territory-Building 34 | 35 | Mechanics 36 | 37 | Area Control 38 | Route/Network Building 39 | 40 | 41 | 42 | ); 43 | -------------------------------------------------------------------------------- /client/src/pages/LibraryGames.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import CollectionGrid from "../components/library/CollectionGrid"; 4 | import { getGamesCollection } from "../api/fetchGames"; 5 | import Placeholder from "../components/User-page.js/Placeholder"; 6 | import { useHistory } from "react-router-dom"; 7 | 8 | const FlexContainer = styled.div` 9 | display: flex; 10 | height: 300px; 11 | color: white; 12 | `; 13 | 14 | export default function LibraryCollection({ searchbarInput }) { 15 | const [games, setGames] = useState(null); 16 | let history = useHistory(); 17 | 18 | React.useEffect(() => { 19 | getGamesCollection().then(gameArray => { 20 | setGames(gameArray); 21 | }); 22 | }, []); 23 | 24 | const searchGames = (games || []).filter(info => 25 | info.name.toLowerCase().includes(searchbarInput.toLowerCase()) 26 | ); 27 | 28 | return ( 29 | <> 30 | {games && games.length === 0 && ( 31 | 32 | history.push("/library/browse")} 36 | /> 37 | 38 | )} 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/pages/LibraryWishlist.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import CollectionGrid from "../components/library/CollectionGrid"; 4 | import { getWishlistCollection } from "../api/fetchWishlist"; 5 | import { useHistory } from "react-router-dom"; 6 | import Placeholder from "../components/User-page.js/Placeholder"; 7 | 8 | const FlexContainer = styled.div` 9 | display: flex; 10 | height: 300px; 11 | color: white; 12 | `; 13 | 14 | export default function LibraryWishlist({ searchbarInput }) { 15 | let history = useHistory(); 16 | React.useEffect(() => { 17 | getWishlistCollection().then(gameArray => { 18 | setWishlistGame(gameArray); 19 | }); 20 | }, []); 21 | 22 | const [wishlistGame, setWishlistGame] = useState(null); 23 | const searchWishlistGames = (wishlistGame || []).filter(info => 24 | info.name.toLowerCase().includes(searchbarInput.toLowerCase()) 25 | ); 26 | return ( 27 | <> 28 | {wishlistGame && wishlistGame.length === 0 && ( 29 | 30 | history.push("/library/browse")} 34 | /> 35 | 36 | )} 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/popup-card/CardBack.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CollectionItemPositioned from "./GamePositioned"; 3 | import CardGameName from "./GameName"; 4 | import DetailButton from "./DetailButton"; 5 | import Grid from "./Grid"; 6 | import Rating from "react-rating"; 7 | import StarRatingFull from "../../icons/StarRatingFull"; 8 | import StarRatingEmpty from "../../icons/StarRatingEmpty"; 9 | import RatingContainer from "./RatingContainer"; 10 | import ModalContainer from "./ModalContainer"; 11 | import DescriptionContainer from "./DescriptionContainer"; 12 | 13 | export default function CardBack({ singleGame, setShowBack, showBack }) { 14 | return ( 15 | 16 | 17 | 18 | } 22 | emptySymbol={} 23 | /> 24 | ({singleGame.num_user_ratings}) 25 | 26 | 27 | 28 | Description 29 | 30 | 33 | 34 | setShowBack(!showBack)} /> 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider } from "styled-components"; 3 | import GlobalStyles from "./GlobalStyles"; 4 | import darkTheme from "./themes/dark"; 5 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 6 | import Login from "./pages/login"; 7 | import Library from "./pages/Library"; 8 | import Register from "./pages/register"; 9 | import { useUser } from "./hooks"; 10 | import NotFound from "./pages/NotFound"; 11 | import User from "./pages/User"; 12 | import Impressum from "./pages/Impressum"; 13 | 14 | function App() { 15 | const [user] = useUser(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {user && ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | )} 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /client/src/components/library/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { morph } from "../../animations/morph"; 4 | import { slideIn } from "../../animations/slideIn"; 5 | 6 | const StyledSearchBar = styled.div` 7 | display: flex; 8 | flex-direction: row; 9 | flex-grow: 1; 10 | justify-content: space-between; 11 | align-items: center; 12 | background-color: inherit; 13 | color: white; 14 | margin-left: 10px; 15 | `; 16 | 17 | const StyledInput = styled.input` 18 | background-color: inherit; 19 | border: 1px solid ${props => props.theme.brightEffect}; 20 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 21 | ::placeholder { 22 | color: white; 23 | } 24 | :focus { 25 | outline: none; 26 | } 27 | font-size: 18px; 28 | padding: 10px; 29 | width: 100%; 30 | fill: white; 31 | color: white; 32 | animation: ${slideIn} 0.25s ease-in; 33 | -webkit-appearance: none; 34 | `; 35 | 36 | export default function SearchBar({ active, onSearch }) { 37 | let throttleTimeout; 38 | 39 | function handleInputChange(event) { 40 | clearTimeout(throttleTimeout); 41 | const inputValue = event.target.value; 42 | throttleTimeout = setTimeout(function() { 43 | onSearch(inputValue); 44 | }, 225); 45 | } 46 | return ( 47 | 48 | {active && ( 49 | 55 | )} 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | 28 | Boardhero 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/src/components/library/CollectionGrid.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import CollectionItem from "./CollectionItem"; 4 | import CardModal from "../popup-card/CardModal"; 5 | 6 | const BodyGrid = styled.div` 7 | width: 100%; 8 | display: grid; 9 | grid-template-columns: repeat(3, 90px); 10 | grid-column-gap: 8%; 11 | grid-row-gap: 30px; 12 | background-color: ${props => props.theme.main}; 13 | justify-content: center; 14 | @media (min-width: 600px) { 15 | grid-column-gap: 130px; 16 | } 17 | @media (min-width: 800px) { 18 | grid-template-columns: repeat(4, 90px); 19 | } 20 | @media (min-width: 1400px) { 21 | grid-template-columns: repeat(5, 90px); 22 | } 23 | `; 24 | 25 | const WrapperDiv = styled.div` 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | width: 100%; 30 | height: 100%; 31 | background-color: ${props => props.theme.main}; 32 | padding: 10px; 33 | `; 34 | export default function CollectionGrid({ 35 | collection, 36 | enabled, 37 | removeGame, 38 | removeWishlist 39 | }) { 40 | const [showModal, setShowModal] = useState(false); 41 | const [selectedGame, setSelectedGame] = useState(); 42 | 43 | return ( 44 | <> 45 | {showModal && ( 46 | setShowModal(false)} 49 | enabled={enabled} 50 | removeGame={removeGame} 51 | removeWishlist={removeWishlist} 52 | /> 53 | )} 54 | 55 | 56 | {collection.map(game => ( 57 | { 60 | setSelectedGame(game); 61 | setShowModal(true); 62 | }} 63 | src={game.image_url} 64 | /> 65 | ))} 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /client/src/animations/alert.js: -------------------------------------------------------------------------------- 1 | import { keyframes } from "styled-components"; 2 | 3 | export const alert = keyframes` 4 | 0% { 5 | bottom: -5em; 6 | width: 65px; 7 | margin-left: -32.5px; 8 | border-radius: 50%; 9 | } 10 | 7% { 11 | bottom: 3em; 12 | width: 65px; 13 | margin-left: -32.5px; 14 | border-radius: 50%; 15 | } 16 | 12% { 17 | bottom: 3em; 18 | width: 65px; 19 | margin-left: -32.5px; 20 | border-radius: 50%; 21 | } 22 | 25% { 23 | bottom: 3em; 24 | width: 300px; 25 | margin-left: -150px; 26 | border-radius: 15px; 27 | } 28 | 37% { 29 | bottom: 3em; 30 | width: 300px; 31 | margin-left: -150px; 32 | } 33 | 42% { 34 | bottom: 3em; 35 | width: 300px; 36 | margin-left: -150px; 37 | } 38 | 47% { 39 | bottom: 3.5em; 40 | width: 300px; 41 | margin-left: -150px; 42 | border-radius: 15px; 43 | } 44 | 50% { 45 | bottom: 3.5em; 46 | width: 300px; 47 | margin-left: -150px; 48 | border-radius: 15px; 49 | } 50 | 53% { 51 | bottom: 3.5em; 52 | width: 300px; 53 | margin-left: -150px; 54 | border-radius: 15px; 55 | } 56 | 58% { 57 | bottom: 3em; 58 | width: 300px; 59 | margin-left: -150px; 60 | } 61 | 63% { 62 | bottom: 3em; 63 | width: 300px; 64 | margin-left: -150px; 65 | } 66 | 75% { 67 | bottom: 3em; 68 | width: 300px; 69 | margin-left: -150px; 70 | border-radius: 15px; 71 | } 72 | 87% { 73 | bottom: 3em; 74 | width: 65px; 75 | margin-left: -32.5px; 76 | border-radius: 50%; 77 | } 78 | 92% { 79 | bottom: 3em; 80 | width: 65px; 81 | margin-left: -32.5px; 82 | border-radius: 50%; 83 | } 84 | 100% { 85 | bottom: -5em; 86 | width: 65px; 87 | margin-left: -32.5px; 88 | border-radius: 50%; 89 | } 90 | } 91 | `; 92 | -------------------------------------------------------------------------------- /client/src/pages/Library.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Header from "../components/Header"; 3 | import LibraryNav from "../components/library/LibraryNav"; 4 | import OptionBox from "../components/library/OptionBox"; 5 | import SortModal from "../components/library/SortModal"; 6 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 7 | import LibraryCollection from "./LibraryGames"; 8 | import LibraryBrowse from "./LibraryBrowse"; 9 | import LibraryWishlist from "./LibraryWishlist"; 10 | import { useLocation } from "react-router-dom"; 11 | 12 | export default function Library() { 13 | let location = useLocation(); 14 | 15 | const [currentNavigation, setCurrentNavigation] = useState(location.pathname); 16 | const [toggleOptions, setToggleOptions] = useState(false); 17 | const [displaySort, setDisplaySort] = useState(false); 18 | const [showSearchbar, setShowSearchbar] = useState(false); 19 | const [searchbarInput, setSearchbarInput] = React.useState(""); 20 | 21 | function handleSearch(value) { 22 | setSearchbarInput(value); 23 | } 24 | 25 | return ( 26 | <> 27 |
setToggleOptions(false)} 29 | toggleSearchbar={() => setShowSearchbar(!showSearchbar)} 30 | active={showSearchbar} 31 | handleInputChange={setSearchbarInput} 32 | onSearch={handleSearch} 33 | page="Library" 34 | /> 35 | setDisplaySort(!displaySort)} 38 | /> 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /client/src/components/library/LibraryNav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | BrowserRouter as Router, 5 | Route, 6 | Link, 7 | useRouteMatch, 8 | useLocation 9 | } from "react-router-dom"; 10 | 11 | const NavGrid = styled.div` 12 | display: grid; 13 | height: 60px; 14 | grid-template-columns: 1fr 1fr 1fr; 15 | border: none; 16 | `; 17 | 18 | const NavGridButton = styled.button` 19 | color: ${props => (props ? "white" : props.theme.darkFont)}; 20 | background: ${props => props.theme.accent}; 21 | font-size: 20px; 22 | display: flex; 23 | width: 100%; 24 | height: inherit; 25 | align-items: center; 26 | justify-content: center; 27 | border: none; 28 | border-bottom: ${props => 29 | props.selected ? `6px solid ${props.theme.brightEffect}` : "none"}; 30 | color: ${props => (props.selected ? "white" : props.theme.darkFont)}; 31 | outline: none; 32 | `; 33 | 34 | const StyledLink = styled(Link)` 35 | text-decoration: none; 36 | color: inherit; 37 | height: inherit; 38 | `; 39 | 40 | export default function LibraryNav({ selected, onNavigationChange }) { 41 | let { url } = useRouteMatch(); 42 | let location = useLocation(); 43 | return ( 44 | 45 | 46 | onNavigationChange(`${url}/games`)} 50 | > 51 | Games 52 | 53 | 54 | 55 | onNavigationChange(`${url}/browse`)} 59 | > 60 | Browse 61 | 62 | 63 | 64 | onNavigationChange(`${url}/wishlist`)} 68 | > 69 | Wishlist 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /client/src/components/user-management/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import InputField from "./InputField"; 4 | import Form from "./Form"; 5 | import { Link } from "react-router-dom"; 6 | import { validateCredentials } from "../../api/fetchUser"; 7 | 8 | const FailMessage = styled.div` 9 | color: red; 10 | `; 11 | const LinkContainer = styled.div` 12 | fill: white; 13 | color: white; 14 | margin: 6px; 15 | `; 16 | 17 | const StyledLink = styled(Link)` 18 | text-decoration: none; 19 | color: ${props => props.theme.brightEffect}; 20 | `; 21 | 22 | export default function LoginForm() { 23 | const [failed, setFailed] = useState(null); 24 | const [user, setUser] = useState({ 25 | email: "", 26 | password: "" 27 | }); 28 | const { email, password } = user; 29 | 30 | function handleChange(event) { 31 | setUser({ ...user, [event.target.name]: event.target.value }); 32 | } 33 | 34 | function submit(event) { 35 | event.preventDefault(); 36 | 37 | validateCredentials(user) 38 | .then(response => response.json()) 39 | .then(function(data) { 40 | if (data === "access denied") { 41 | setFailed(true); 42 | window.navigator.vibrate(400); 43 | } else { 44 | localStorage.setItem("user", data.name); 45 | window.location.pathname = "/Library/browse"; 46 | } 47 | }); 48 | } 49 | 50 | return ( 51 | <> 52 | {failed && Username or password incorrect} 53 |
54 | 62 | 70 | 71 | 72 | 73 | Don't have an Account ? Sign Up 74 | 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /client/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import styled from "styled-components"; 3 | import IconButton from "./IconButton"; 4 | import BurgerMenu from "../icons/BurgerMenuIcon"; 5 | import Magnifier from "../icons/Magnifier"; 6 | import VerticalPoints from "../icons/VerticalPoints"; 7 | import SearchBar from "./library/SearchBar"; 8 | import BurgerMenuList from "./BurgerMenuList"; 9 | import { useOnClickOutside } from "../hooks"; 10 | import OptionBox from "./library/OptionBox"; 11 | import SortModal from "./library/SortModal"; 12 | import { Background } from "./popup-card/CardModal"; 13 | import AxeViking from "../icons/AxeViking"; 14 | 15 | const HeaderContainer = styled.header` 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | background-color: ${props => props.theme.lightBackground}; 20 | color: white; 21 | height: 60px; 22 | font-size: 20px; 23 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.15); 24 | position: sticky; 25 | top: 0; 26 | `; 27 | 28 | const PageSpan = styled.span``; 29 | 30 | const IconSpan = styled.span` 31 | position: absolute; 32 | `; 33 | export default function Header({ 34 | toggleOptions, 35 | toggleSearchbar, 36 | active, 37 | handleInputChange, 38 | onSearch, 39 | page 40 | }) { 41 | const [showMenu, setShowMenu] = React.useState(false); 42 | const [showOptions, setShowOptions] = useState(false); 43 | const [showSort, setShowSort] = useState(false); 44 | const node = useRef(); 45 | 46 | useOnClickOutside(node, () => { 47 | setShowMenu(false); 48 | setShowOptions(false); 49 | setShowSort(false); 50 | }); 51 | return ( 52 | <> 53 | {showMenu && ( 54 | <> 55 | 56 |
57 | setShowMenu(false)} /> 58 |
59 | 60 | )} 61 | 62 | 63 | setShowMenu(!showMenu)}> 64 | 65 | 66 | {page} 67 | {!active && ( 68 | 69 | 70 | 71 | )} 72 | 73 | handleInputChange(event)} 76 | onSearch={onSearch} 77 | /> 78 | 79 | 80 | 81 | 82 | 83 | 84 | {showOptions && ( 85 |
86 | 87 | {showSort && } 88 |
89 | )} 90 |
91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /client/src/pages/User.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | useHistory 7 | } from "react-router-dom"; 8 | import styled from "styled-components"; 9 | import Header from "../components/Header"; 10 | import PictureContainer from "../components/User-page.js/RoundPicture"; 11 | import AmountGames from "../components/User-page.js/AmountGames"; 12 | import { useUser } from "../hooks"; 13 | import { getGamesCollection } from "../api/fetchGames"; 14 | import compare from "../components/User-page.js/CompareFunction"; 15 | import UserNameArea from "../components/User-page.js/UserNameArea"; 16 | import { getWishlistCollection } from "../api/fetchWishlist"; 17 | import UserStatistics from "./UserStatistics"; 18 | import UserFriends from "./UserFriends"; 19 | import UserOverview from "./UserOverview"; 20 | import NameText from "../components/User-page.js/NameText"; 21 | import NavBar from "../components/User-page.js/NavBar"; 22 | import LibraryBrowse from "./LibraryBrowse"; 23 | 24 | const Container = styled.div` 25 | display: flex; 26 | height: 100vh; 27 | width: 100vw; 28 | flex-direction: column; 29 | color: white; 30 | `; 31 | 32 | const TopArea = styled.section` 33 | height: 20%; 34 | display: flex; 35 | justify-content: space-around; 36 | align-items: center; 37 | `; 38 | 39 | export default function User() { 40 | const [games, setGames] = useState([]); 41 | const [wishlistGames, setWishlistGames] = useState([]); 42 | 43 | React.useEffect(() => { 44 | getWishlistCollection().then(gameArray => { 45 | setWishlistGames(gameArray); 46 | }); 47 | }, []); 48 | 49 | React.useEffect(() => { 50 | getGamesCollection().then(gameArray => { 51 | setGames(gameArray); 52 | }); 53 | }, []); 54 | 55 | const filteredGames = games.sort(compare); 56 | const [user] = useUser(); 57 | let history = useHistory(); 58 | 59 | return ( 60 | 61 |
{ 64 | history.push("/library/browse"); 65 | }} 66 | /> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /client/src/components/user-management/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import InputField from "./InputField"; 4 | import Form from "./Form"; 5 | import { Link, useHistory } from "react-router-dom"; 6 | import { createUser } from "../../api/fetchUser"; 7 | 8 | const ErrorContainer = styled.div` 9 | color: red; 10 | `; 11 | 12 | const SuccessContainer = styled.div` 13 | color: green; 14 | `; 15 | const LinkContainer = styled.div` 16 | fill: white; 17 | color: white; 18 | margin: 6px; 19 | `; 20 | 21 | const StyledLink = styled(Link)` 22 | text-decoration: none; 23 | color: ${props => props.theme.brightEffect}; 24 | `; 25 | 26 | export default function RegisterForm() { 27 | const [feedback, setFeedback] = useState(null); 28 | const [success, setSuccess] = useState(null); 29 | const [failed, setFailed] = useState(null); 30 | const [user, setUser] = useState({ 31 | name: "", 32 | email: "", 33 | password: "", 34 | password2: "" 35 | }); 36 | 37 | let history = useHistory(); 38 | const { name, email, password, password2 } = user; 39 | 40 | function handleChange(event) { 41 | setUser({ ...user, [event.target.name]: event.target.value }); 42 | } 43 | 44 | function submit(event) { 45 | event.preventDefault(); 46 | if (password !== password2) { 47 | setFailed(true); 48 | setFeedback("passwords don't match"); 49 | return; 50 | } else { 51 | createUser(user) 52 | .then(response => response.json()) 53 | .then(function(data) { 54 | if (data === "email already exists") { 55 | setFeedback("Email adress is already being used"); 56 | setSuccess(false); 57 | setFailed(true); 58 | return; 59 | } else { 60 | setFeedback("Account succesfully created"); 61 | setFailed(false); 62 | setSuccess(true); 63 | setTimeout(() => { 64 | history.push("/"); 65 | }, 1300); 66 | return; 67 | } 68 | }) 69 | .catch(function() { 70 | console.log("error"); 71 | }); 72 | } 73 | } 74 | return ( 75 | <> 76 | {failed && {feedback}} 77 | {success && {feedback}} 78 |
79 | 87 | 95 | 103 | 111 | 112 | 113 | 114 | Already have an Account ? Login 115 | 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /client/src/components/BurgerMenuList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import NavigationMenuButton from "./library/NavigationMenuButton"; 4 | import Dice from "../icons/Dice"; 5 | import { useHistory, useLocation, useRouteMatch, Link } from "react-router-dom"; 6 | import Logout from "../icons/Logout"; 7 | 8 | import { useUser } from "../hooks"; 9 | import { unsetUser } from "../api/fetchUser"; 10 | import Person from "../icons/Person"; 11 | import Clear from "../icons/Clear"; 12 | import { morph } from "../animations/morph"; 13 | 14 | const PositionContainer = styled.nav` 15 | position: fixed; 16 | left: 0px; 17 | z-index: 1; 18 | display: flex; 19 | flex-grow: 1; 20 | flex-direction: column; 21 | justify-content: flex-start; 22 | width: 70vw; 23 | height: 100vh; 24 | background-color: ${props => props.theme.accent}; 25 | animation: ${morph} 0.4s ease-in-out; 26 | `; 27 | 28 | const NameContainer = styled.div` 29 | color: ${props => props.theme.brightEffect}; 30 | margin-left: 5px; 31 | font-size: 1.2em; 32 | height: 10vh; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | `; 37 | 38 | const FlexDiv = styled.div` 39 | display: flex; 40 | flex-grow: 1; 41 | justify-content: center; 42 | align-items: center; 43 | font-size: 1.1em; 44 | margin-left: -10px; 45 | `; 46 | 47 | const CloseButton = styled.button` 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | border: none; 52 | outline: none; 53 | color: inherit; 54 | width: 40px; 55 | height: 40px; 56 | background: inherit; 57 | `; 58 | 59 | const ImpressumLink = styled(Link)` 60 | position: absolute; 61 | bottom: 15px; 62 | right: 5%; 63 | color: white; 64 | `; 65 | export default function BurgerMenuList({ handleClose }) { 66 | let history = useHistory(); 67 | let location = useLocation(); 68 | let { url } = useRouteMatch(); 69 | const [user] = useUser(); 70 | return ( 71 | <> 72 | 73 | 74 | 75 | 76 | 77 | Hello, {user}! 78 | 79 | 80 | history.push("/library/browse")} 82 | selected={ 83 | location.pathname === `${url}/browse` || 84 | location.pathname === `${url}/games` || 85 | location.pathname === `${url}/wishlist` 86 | } 87 | > 88 | 95 | Library 96 | 97 | { 99 | history.push("/user/overview"); 100 | }} 101 | selected={ 102 | location.pathname === `${url}/overview` || 103 | location.pathname === `${url}/statistics` || 104 | location.pathname === `${url}/friends` 105 | } 106 | > 107 | 114 | User 115 | 116 | { 118 | unsetUser(); 119 | localStorage.clear(); 120 | history.push("/"); 121 | }} 122 | > 123 | 124 | Logout 125 | 126 | Impressum 127 | 128 | 129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /client/src/icons/Wishlist.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Wishlist({ className }) { 4 | return ( 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/popup-card/CardModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import styled from "styled-components"; 3 | import CollectionItemPositioned from "./GamePositioned"; 4 | import AddButtonCollection from "./AddButtonCollection"; 5 | import AddButtonWishlist from "./AddButtonWishlist"; 6 | import { removeGameEntry, newGame } from "../../api/fetchGames"; 7 | import { newWishlistEntry, removeGameWishlist } from "../../api/fetchWishlist"; 8 | import DetailButton from "./DetailButton"; 9 | import ConfirmationMessage from "./ConfirmationMessage"; 10 | import Grid from "./Grid"; 11 | import GameName from "./GameName"; 12 | import Players from "./Players"; 13 | import TrimText from "./TrimText"; 14 | import Rating from "react-rating"; 15 | import StarRatingFull from "../../icons/StarRatingFull"; 16 | import StarRatingEmpty from "../../icons/StarRatingEmpty"; 17 | import RatingContainer from "./RatingContainer"; 18 | import RemoveButton from "./RemoveButton"; 19 | import CardBack from "./CardBack"; 20 | import ModalContainer from "./ModalContainer"; 21 | 22 | export const Background = styled.div` 23 | height: 100%; 24 | width: 100%; 25 | background: transparent; 26 | position: fixed; 27 | outline: none; 28 | border: none; 29 | top: 0; 30 | z-index: 1; 31 | backdrop-filter: blur(2.8px); 32 | overflow-y: hidden; 33 | `; 34 | 35 | const FlexContainer = styled.div` 36 | display: flex; 37 | justify-content: center; 38 | align-items: flex-end; 39 | `; 40 | 41 | const Info = styled.span` 42 | margin: 6px 0px; 43 | `; 44 | 45 | const ColoredSpan = styled.span` 46 | color: ${props => props.theme.brightEffect}; 47 | margin-right: 10px; 48 | `; 49 | 50 | export default function CardModal({ 51 | handleOutsideClick, 52 | singleGame, 53 | enabled, 54 | removeGame, 55 | removeWishlist 56 | }) { 57 | const [showBack, setShowBack] = useState(false); 58 | const [startAnimation, setStartAnimation] = useState(false); 59 | 60 | const regex = /((<([^>&"]+)>)|(\"\;))/gi; 61 | const description = singleGame.description.replace(regex, ""); 62 | 63 | return ( 64 | <> 65 | 66 | 67 | {!showBack && ( 68 | 69 | 70 | 71 | } 75 | emptySymbol={} 76 | /> 77 | ({singleGame.num_user_ratings}) 78 | 79 | 80 | 81 | {TrimText(`${singleGame.name}`, 45)} 82 | 83 | Players: {singleGame.min_players}- 84 | {singleGame.max_players} 85 | 86 | Playtime: 87 | {singleGame.min_playtime}min - {singleGame.max_playtime}min 88 | 89 | Description: 90 | 91 | {TrimText(`${description}`, 43)} 92 | setShowBack(!showBack)} /> 93 | 94 | 95 | 96 | {enabled && ( 97 | 102 | To Collection 103 | 104 | )} 105 | {enabled && ( 106 | 111 | To Wishlist 112 | 113 | )} 114 | {removeWishlist && ( 115 | { 117 | removeGameWishlist(singleGame); 118 | window.location.reload(true); 119 | }} 120 | > 121 | Remove 122 | 123 | )} 124 | {removeGame && ( 125 | { 127 | removeGameEntry(singleGame); 128 | window.location.reload(true); 129 | }} 130 | > 131 | Remove 132 | 133 | )} 134 | 135 | )} 136 | {showBack && ( 137 | 142 | )} 143 | 144 | {startAnimation && } 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /client/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.1/8 is 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 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get("content-type"); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf("javascript") === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | "No internet connection found. App is running in offline mode." 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ("serviceWorker" in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const express = require("express"); 4 | const { getGames, setGames, removeGame } = require("./lib/games"); 5 | const { 6 | getWishlist, 7 | setWishlist, 8 | removeWishlistEntry 9 | } = require("./lib/wishlist"); 10 | const { getUsers, setUser, validateUser } = require("./lib/users"); 11 | const { initDatabase } = require("./lib/database"); 12 | const { 13 | createSession, 14 | getUserBySession, 15 | deleteSession 16 | } = require("./lib/sessions"); 17 | const axios = require("axios"); 18 | 19 | const app = express(); 20 | const path = require("path"); 21 | const cookieParser = require("cookie-parser"); 22 | 23 | app.use(express.json()); 24 | app.use(cookieParser()); 25 | 26 | app.get(`/api/wishlist`, async (request, response) => { 27 | try { 28 | const user = getUserBySession(request.cookies.session); 29 | if (!user) return response.status(403).end("unauthorized request"); 30 | if (user) { 31 | const wishlist = await getWishlist(user.name); 32 | return response.json(wishlist); 33 | } else { 34 | response.send("User not found"); 35 | } 36 | } catch (error) { 37 | return response.end("Error"); 38 | } 39 | response.end(); 40 | }); 41 | 42 | app.get(`/api/games`, async (request, response) => { 43 | try { 44 | const user = getUserBySession(request.cookies.session); 45 | if (!user) return response.status(403).end("unauthorized request"); 46 | if (user) { 47 | const games = await getGames(user.name); 48 | return response.json(games); 49 | } else { 50 | response.send("User not found"); 51 | } 52 | } catch (error) { 53 | console.log(error); 54 | return response.end("Error"); 55 | } 56 | }); 57 | 58 | app.post("/api/wishlist", async (request, response) => { 59 | try { 60 | const user = getUserBySession(request.cookies.session); 61 | if (!user) return response.status(403).end("unauthorized request"); 62 | request.body.owner = user.name; 63 | const WishlistEntry = await setWishlist(request.body); 64 | return response.json({ WishlistEntry }); 65 | } catch (error) { 66 | console.error(error); 67 | response.end(error); 68 | } 69 | }); 70 | 71 | app.post("/api/games", async (request, response) => { 72 | try { 73 | const user = getUserBySession(request.cookies.session); 74 | if (!user) return response.status(403).end("unauthorized request"); 75 | request.body.owner = user.name; 76 | request.body.date_added = Date.now(); 77 | const newGame = await setGames(request.body); 78 | return response.json({ newGame }); 79 | } catch (error) { 80 | console.error(error); 81 | response.end("Error"); 82 | } 83 | }); 84 | 85 | app.get("/api/users", async (request, response) => { 86 | try { 87 | const users = await getUsers(request.params); 88 | return response.json(users); 89 | } catch (error) { 90 | return response.end("Error"); 91 | } 92 | }); 93 | 94 | app.post("/api/users", async (request, response) => { 95 | try { 96 | const emailExist = await getUsers(request.body.email); 97 | if (emailExist) { 98 | return response.status(400).json("email already exists"); 99 | } else { 100 | await setUser(request.body); 101 | return response.status(200).json("Account succesfully created"); 102 | } 103 | } catch (error) { 104 | console.error(error); 105 | response.end("Error"); 106 | } 107 | }); 108 | 109 | app.post("/api/login", async (request, response) => { 110 | try { 111 | const userExist = await validateUser(request.body); 112 | if (userExist) { 113 | const sessionId = createSession(userExist); 114 | response.cookie("session", sessionId); 115 | return response.json({ name: userExist.name }); 116 | } else if (!userExist) { 117 | return response.status(401).json("access denied"); 118 | } 119 | } catch (error) { 120 | response.end("Error"); 121 | console.log(error); 122 | } 123 | response.end(); 124 | }); 125 | 126 | app.post("/api/logout", async (request, response) => { 127 | try { 128 | const session = request.cookies.session; 129 | deleteSession(session); 130 | response.clearCookie("session"); 131 | return response.end(); 132 | } catch (error) { 133 | response.end(); 134 | } 135 | }); 136 | 137 | app.get("/api/search", async (request, response) => { 138 | const name = request.query.name; 139 | const targetUrl = `https://www.boardgameatlas.com/api/search?name=${name}&limit=15&fields=id,name,description,image_url,publishers,average_user_rating,num_user_ratings,mechanics,categories,min_players,max_players,min_playtime,max_playtime&client_id=5cIY9zBPpt`; 140 | 141 | return axios({ 142 | url: targetUrl, 143 | method: "GET", 144 | headers: { 145 | Accept: "application/json", 146 | "client-id": "5cIY9zBPpt" 147 | } 148 | }) 149 | .then(response => response.data) 150 | .then(games => response.json(games)); 151 | }); 152 | 153 | app.get("/api/mechanics", async (request, response) => { 154 | const targetUrl = `https://www.boardgameatlas.com/api/game/mechanics?client_id=5cIY9zBPpt`; 155 | 156 | return axios({ 157 | url: targetUrl, 158 | method: "GET", 159 | headers: { 160 | Accept: "application/json", 161 | "client-id": "5cIY9zBPpt" 162 | } 163 | }) 164 | .then(response => response.data) 165 | .then(mechanics => response.json(mechanics)); 166 | }); 167 | 168 | app.post("/api/wishlistremove", async (request, response) => { 169 | try { 170 | const user = getUserBySession(request.cookies.session); 171 | if (!user) return response.status(403).end("unauthorized request"); 172 | removeWishlistEntry(request.body.name); 173 | response.send("Wishlist entry deleted"); 174 | } catch (error) { 175 | console.error; 176 | } 177 | }); 178 | 179 | app.post("/api/gamesremove", async (request, response) => { 180 | try { 181 | const user = getUserBySession(request.cookies.session); 182 | if (!user) return response.status(403).end("unauthorized request"); 183 | removeGame(request.body.name); 184 | response.send("Game deleted"); 185 | } catch (error) { 186 | console.error; 187 | } 188 | }); 189 | 190 | if (process.env.NODE_ENV === "production") { 191 | app.use(express.static(path.join(__dirname, "client/build"))); 192 | 193 | app.get("*", function(req, res) { 194 | res.sendFile(path.join(__dirname, "client/build", "index.html")); 195 | }); 196 | } 197 | initDatabase(process.env.DB_URL, process.env.DB_NAME).then(() => { 198 | console.log(`Database ${(process.env.DB_URL, process.env.DB_NAME)} is ready`); 199 | 200 | app.listen(process.env.PORT, () => { 201 | console.log(`Server listens on http://localhost:${process.env.PORT}`); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /client/src/icons/WoodSign.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Woodsign() { 4 | return ( 5 | 11 | 12 | 13 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 44 | 50 | 51 | 52 | 53 | 54 | 61 | 67 | 68 | 69 | 73 | 74 | 81 | 87 | 88 | 89 | 93 | 94 | 101 | 107 | 108 | 109 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 125 | 126 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 172 | 173 | 174 | 175 | 176 | 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /client/src/icons/Hero.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Hero() { 4 | return ( 5 | 11 | 16 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 64 | 70 | 76 | 82 | 88 | 94 | 95 | 100 | 101 | 102 | 107 | 108 | 114 | 115 | 120 | 121 | 122 | 127 | 128 | 129 | 134 | 135 | 136 | 141 | 142 | 143 | 148 | 149 | 150 | 155 | 156 | 157 | 162 | 163 | 164 | 169 | 170 | 171 | 176 | 177 | 178 | 183 | 184 | 185 | 190 | 191 | 192 | 197 | 198 | 199 | 204 | 205 | 206 | 211 | 212 | 213 | 218 | 219 | 220 | 225 | 226 | 227 | 232 | 233 | 234 | 239 | 240 | 241 | 246 | 247 | 248 | 253 | 254 | 255 | 260 | 261 | 262 | 267 | 268 | 269 | 275 | 281 | 282 | 283 | 289 | 295 | 296 | 297 | 303 | 309 | 310 | 316 | 322 | 323 | 329 | 330 | 331 | 337 | 338 | 339 | 345 | 346 | 347 | 353 | 354 | 355 | 361 | 362 | 363 | 369 | 370 | 371 | 377 | 378 | 379 | 385 | 386 | 387 | 393 | 394 | 400 | 401 | 407 | 408 | 409 | 415 | 416 | 417 | 423 | 424 | 425 | 431 | 432 | 433 | 439 | 440 | 441 | 447 | 448 | 449 | 455 | 456 | 457 | 463 | 464 | 465 | 471 | 472 | 473 | 479 | 480 | 481 | 487 | 488 | 489 | 495 | 496 | 497 | 503 | 504 | 505 | 511 | 512 | 518 | 519 | 520 | ); 521 | } 522 | --------------------------------------------------------------------------------