├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc ├── README.md ├── package.json ├── public ├── favicon.ico ├── favicon.png ├── index.html ├── manifest.json └── robots.txt ├── scripts └── createitems.mjs ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Reddit.md ├── State │ ├── CommonState.ts │ ├── GameTypeState.ts │ ├── PopupStatusState.ts │ ├── SearchState.ts │ ├── SpoilerState.ts │ ├── Types.tsx │ └── index.ts ├── components │ ├── Firebase │ │ ├── FirebaseProvider.tsx │ │ ├── config.ts │ │ └── index.tsx │ ├── GameSelector.tsx │ ├── Tabs │ │ ├── About.tsx │ │ ├── Account │ │ │ ├── Account.tsx │ │ │ ├── PasswordChangeDialog.tsx │ │ │ ├── SignedInAccount.tsx │ │ │ └── SignedOutAccount.tsx │ │ ├── MainView │ │ │ ├── ImportData.tsx │ │ │ ├── Items │ │ │ │ ├── Grid │ │ │ │ │ ├── ItemCard.tsx │ │ │ │ │ ├── ItemGrid.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── itemCard.scss │ │ │ │ │ └── itemGrid.scss │ │ │ │ ├── ItemList.tsx │ │ │ │ ├── ItemManagement │ │ │ │ │ ├── ItemManagementContainer.tsx │ │ │ │ │ ├── NoItemManagement.tsx │ │ │ │ │ ├── PartyItemManagement.tsx │ │ │ │ │ ├── SimpleItemManagement.tsx │ │ │ │ │ ├── items.ts │ │ │ │ │ ├── partyItemManagement.scss │ │ │ │ │ └── simpleItemManagement.scss │ │ │ │ ├── ItemsView.tsx │ │ │ │ ├── PurchaseItem.tsx │ │ │ │ ├── Search │ │ │ │ │ ├── Discount.tsx │ │ │ │ │ ├── FilterAvailability.tsx │ │ │ │ │ ├── FilterClass.tsx │ │ │ │ │ ├── FilterResources.tsx │ │ │ │ │ ├── FilterSlots.tsx │ │ │ │ │ ├── FindItemSearchBar.tsx │ │ │ │ │ ├── RenderAs.tsx │ │ │ │ │ ├── SearchOptions.tsx │ │ │ │ │ ├── SortItems.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── Table │ │ │ │ │ ├── ConsumptionPanel.tsx │ │ │ │ │ ├── ItemCost.tsx │ │ │ │ │ ├── ItemSummon.tsx │ │ │ │ │ ├── ItemTable.tsx │ │ │ │ │ ├── ItemTableRow.tsx │ │ │ │ │ ├── ItemText.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── itemTable.scss │ │ │ │ └── index.ts │ │ │ └── MainView.tsx │ │ ├── Share │ │ │ ├── Share.tsx │ │ │ └── UploadForm.tsx │ │ └── SpoilerFilters │ │ │ ├── Common │ │ │ ├── ConfirmSpecialUnlockPanel.tsx │ │ │ └── SpecialUnlockButton.tsx │ │ │ ├── Games │ │ │ ├── ConfirmGameRemoval.tsx │ │ │ ├── GameFilterCheckbox.tsx │ │ │ ├── GameFilters.tsx │ │ │ ├── GameHelp.tsx │ │ │ └── index.ts │ │ │ ├── Items │ │ │ ├── BuildingLevelFilter.tsx │ │ │ ├── FHSpoilerFilter.tsx │ │ │ ├── FilterCheckbox.tsx │ │ │ ├── GHSpoilerFilter.tsx │ │ │ ├── GameFilter.tsx │ │ │ ├── JOTLSpoilerFilter.tsx │ │ │ ├── ProsperityFilter.tsx │ │ │ ├── ReputationPulldown.tsx │ │ │ ├── ScenarioCompletedFilter.tsx │ │ │ ├── SoloClassFilter.tsx │ │ │ ├── SoloClassFilterBlock.tsx │ │ │ ├── SpoilerFilterItemList.tsx │ │ │ ├── ToggleAllButton.tsx │ │ │ └── index.ts │ │ │ ├── Party │ │ │ ├── ClassList.tsx │ │ │ ├── ConfirmClassDelete.tsx │ │ │ ├── OwnedItemsList.tsx │ │ │ ├── PartyManagementFilter.tsx │ │ │ ├── PartySpoiler.tsx │ │ │ ├── PartySpoilerList.tsx │ │ │ └── index.ts │ │ │ ├── Secrets │ │ │ └── Secrets.tsx │ │ │ └── SpoilerFilters.tsx │ └── Utils │ │ ├── ClassDropdown.tsx │ │ ├── ClassIcon.tsx │ │ ├── GHIcon.tsx │ │ └── index.ts ├── constants │ └── routes.ts ├── games │ ├── GameData.ts │ ├── GameInfo.ts │ ├── GameType.ts │ ├── fh │ │ ├── FHGameData.ts │ │ └── items.json │ ├── gh │ │ ├── GHGameData.ts │ │ └── items.json │ ├── index.ts │ ├── jotl │ │ ├── JOTlGameData.ts │ │ └── items.json │ └── useGameSort.ts ├── helpers.ts ├── hooks │ ├── useIsItemShown.tsx │ ├── useItems.tsx │ ├── useRemovePlayer.tsx │ └── useSetSorting.tsx ├── img │ ├── class-tokens │ │ ├── be.png │ │ ├── br.png │ │ ├── bt.png │ │ ├── ch.png │ │ ├── dm.png │ │ ├── dr.png │ │ ├── ds.png │ │ ├── el.png │ │ ├── ht.png │ │ ├── mt.png │ │ ├── ns.png │ │ ├── ph.png │ │ ├── qm.png │ │ ├── rg.png │ │ ├── sb.png │ │ ├── sc.png │ │ ├── sk.png │ │ ├── ss.png │ │ ├── su.png │ │ ├── sw.png │ │ ├── ti.png │ │ ├── vw.png │ │ └── xx.png │ ├── classes │ │ ├── BE.png │ │ ├── BR.png │ │ ├── BT.png │ │ ├── CH.png │ │ ├── CS1.png │ │ ├── CS10.png │ │ ├── CS11.png │ │ ├── CS2.png │ │ ├── CS3.png │ │ ├── CS4.png │ │ ├── CS5.png │ │ ├── CS6.png │ │ ├── CS7.png │ │ ├── CS8.png │ │ ├── CS9.png │ │ ├── CSA1.png │ │ ├── CSA2.png │ │ ├── CSA3.png │ │ ├── DM.png │ │ ├── DR.png │ │ ├── DS.png │ │ ├── EL.png │ │ ├── FH1.png │ │ ├── FH10.png │ │ ├── FH11.png │ │ ├── FH12.png │ │ ├── FH13.png │ │ ├── FH14.png │ │ ├── FH15.png │ │ ├── FH16.png │ │ ├── FH17.png │ │ ├── FH2.png │ │ ├── FH3.png │ │ ├── FH4.png │ │ ├── FH5.png │ │ ├── FH6.png │ │ ├── FH7.png │ │ ├── FH8.png │ │ ├── FH9.png │ │ ├── HT.png │ │ ├── MT.png │ │ ├── NS.png │ │ ├── PH.png │ │ ├── QM.png │ │ ├── RG.png │ │ ├── SB.png │ │ ├── SC.png │ │ ├── SK.png │ │ ├── SS.png │ │ ├── SU.png │ │ ├── SW.png │ │ ├── TI.png │ │ ├── VW.png │ │ └── XX.png │ ├── icons │ │ ├── conditions │ │ │ ├── bane.png │ │ │ ├── bless.png │ │ │ ├── brittle.png │ │ │ ├── chill.png │ │ │ ├── curse.png │ │ │ ├── disarm.png │ │ │ ├── dodge.png │ │ │ ├── empower.png │ │ │ ├── enfeeble.png │ │ │ ├── fh-wound.png │ │ │ ├── immobilize.png │ │ │ ├── impair.png │ │ │ ├── infect.png │ │ │ ├── invisible.png │ │ │ ├── muddle.png │ │ │ ├── pierce.png │ │ │ ├── poison.png │ │ │ ├── pull.png │ │ │ ├── push.png │ │ │ ├── regenerate.png │ │ │ ├── rolling.png │ │ │ ├── rupture.png │ │ │ ├── strengthen.png │ │ │ ├── stun.png │ │ │ ├── target.png │ │ │ ├── ward.png │ │ │ └── wound.png │ │ ├── elements │ │ │ ├── any.png │ │ │ ├── dark.png │ │ │ ├── earth.png │ │ │ ├── fh-air-icon.png │ │ │ ├── fh-air.png │ │ │ ├── fh-consume.png │ │ │ ├── fh-dark-icon.png │ │ │ ├── fh-dark.png │ │ │ ├── fh-earth-icon.png │ │ │ ├── fh-earth.png │ │ │ ├── fh-fire-icon.png │ │ │ ├── fh-fire.png │ │ │ ├── fh-ice-icon.png │ │ │ ├── fh-ice.png │ │ │ ├── fh-light-icon.png │ │ │ ├── fh-light.png │ │ │ ├── fh-wild-icon.png │ │ │ ├── fh-wild.png │ │ │ ├── fire.png │ │ │ ├── ice.png │ │ │ ├── light.png │ │ │ ├── use.png │ │ │ ├── wind.png │ │ │ ├── x-any.png │ │ │ ├── x-dark.png │ │ │ ├── x-earth.png │ │ │ ├── x-fire.png │ │ │ ├── x-ice.png │ │ │ ├── x-light.png │ │ │ └── x-wind.png │ │ ├── equipment_slot │ │ │ ├── 1h.png │ │ │ ├── 2h.png │ │ │ ├── body.png │ │ │ ├── head.png │ │ │ ├── legs.png │ │ │ └── small.png │ │ ├── general │ │ │ ├── Crystalize.png │ │ │ ├── TOA6.png │ │ │ ├── attack.png │ │ │ ├── attack_tile.png │ │ │ ├── check.png │ │ │ ├── checkmark.png │ │ │ ├── circle_x.png │ │ │ ├── consumed.png │ │ │ ├── consumed_white.png │ │ │ ├── damage.png │ │ │ ├── eot.png │ │ │ ├── event_card_rip.png │ │ │ ├── event_card_rip_white.png │ │ │ ├── event_card_shuffle.png │ │ │ ├── event_card_shuffle_white.png │ │ │ ├── experience.png │ │ │ ├── experience_1.png │ │ │ ├── experience_2.png │ │ │ ├── experience_white_1.png │ │ │ ├── experience_white_2.png │ │ │ ├── fh-anemone.png │ │ │ ├── fh-astral.png │ │ │ ├── fh-flying.png │ │ │ ├── fh-geminate-left.png │ │ │ ├── fh-geminate-right.png │ │ │ ├── fh-heal.png │ │ │ ├── fh-hourglass.png │ │ │ ├── fh-jump.png │ │ │ ├── fh-meter-blue.png │ │ │ ├── fh-meter-red.png │ │ │ ├── fh-meter-yellow.png │ │ │ ├── fh-move.png │ │ │ ├── fh-prism.png │ │ │ ├── fh-range.png │ │ │ ├── fh-shadow.png │ │ │ ├── fh-shards.png │ │ │ ├── fh-shield.png │ │ │ ├── flip_back_white.png │ │ │ ├── flip_white.png │ │ │ ├── flying.png │ │ │ ├── heal.png │ │ │ ├── jump.png │ │ │ ├── loot.png │ │ │ ├── lost.png │ │ │ ├── lost_white.png │ │ │ ├── modifier_2x_circle.png │ │ │ ├── modifier_minus_one.png │ │ │ ├── modifier_minus_one_1.png │ │ │ ├── modifier_minus_one_circle.png │ │ │ ├── modifier_minus_one_cropped.png │ │ │ ├── modifier_minus_one_white.png │ │ │ ├── modifier_minus_two_circle.png │ │ │ ├── modifier_no_damage.png │ │ │ ├── modifier_plus_1.png │ │ │ ├── modifier_plus_2.png │ │ │ ├── modifier_zero_circle.png │ │ │ ├── move.png │ │ │ ├── ongoing.png │ │ │ ├── player_tile.png │ │ │ ├── range.png │ │ │ ├── recover.png │ │ │ ├── recover_white.png │ │ │ ├── refresh.png │ │ │ ├── refresh_white.png │ │ │ ├── retaliate.png │ │ │ ├── scrapx.png │ │ │ ├── shield.png │ │ │ ├── shuffle.png │ │ │ ├── shuffle_white.png │ │ │ ├── slot_empty.png │ │ │ ├── slot_experience.png │ │ │ ├── spent.png │ │ │ ├── spent_white.png │ │ │ ├── target.png │ │ │ └── teleport.png │ │ ├── multi_attack │ │ │ ├── cleave_0_1.png │ │ │ ├── cone_0_1.png │ │ │ ├── cone_1_1.png │ │ │ ├── cube_2_2.png │ │ │ ├── line_0_1_1.png │ │ │ └── line_1_1.png │ │ └── resources │ │ │ ├── arrowvine.png │ │ │ ├── axenut.png │ │ │ ├── corpsecap.png │ │ │ ├── flamefruit.png │ │ │ ├── hide.png │ │ │ ├── item.png │ │ │ ├── lumber.png │ │ │ ├── metal.png │ │ │ ├── rockroot.png │ │ │ └── snowthistle.png │ └── lost.png ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | worldhaven -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module", // Allows for the use of imports 6 | ecmaFeatures: { 7 | jsx: true, // Allows for the parsing of JSX 8 | }, 9 | }, 10 | settings: { 11 | react: { 12 | version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use 13 | }, 14 | }, 15 | plugins: [ 16 | // ... 17 | "react-hooks", 18 | ], 19 | extends: [ 20 | // "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 21 | // "plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin 22 | "plugin:react-hooks/recommended", 23 | ], 24 | rules: { 25 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 26 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 27 | "react-hooks/rules-of-hooks": "error", 28 | "react-hooks/exhaustive-deps": "warn", 29 | "@typescript-eslint/no-var-requires": "off", 30 | "@typescript-eslint/no-explicit-any": "off", 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | # Triggers the workflow on push or pull request events but only for the master branch 4 | push: 5 | branches: [master] 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 16 | run: | 17 | npm i 18 | npm run build 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 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | /.vscode/settings.json 25 | /.eslintcache 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "worldhaven"] 2 | path = worldhaven 3 | url = https://github.com/any2cards/worldhaven.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | worldhaven -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "arrowParens": "always", 4 | "useTabs": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gloomhaven Item DB 2 | 3 | https://heisch.github.io/gloomhaven-item-db/ 4 | 5 | ## Description 6 | 7 | a small item database for the game [Gloomhaven](http://www.cephalofair.com/gloomhaven) by [Cephalofair Games](http://www.cephalofair.com/) [Developer: Isaac Childres]. 8 | 9 | ## Third Party 10 | 11 | this project is using the item-image scans from https://github.com/any2cards/gloomhaven 12 | 13 | --- 14 | _Gloomhaven: Gloomhaven and all related properties, images and text are owned by Cephalofair Games._ 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gloomhaven-item-db", 3 | "homepage": "https://heisch.github.io/gloomhaven-item-db", 4 | "version": "1.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.21", 12 | "@types/react": "^18.0.30", 13 | "@types/react-dom": "^18.0.11", 14 | "firebase": "^9.18.0", 15 | "gh-pages": "^5.0.0", 16 | "lodash": "^4.17.21", 17 | "react": "^17.0.0", 18 | "react-dom": "^17.0.0", 19 | "react-firebaseui": "^6.0.0", 20 | "react-router-dom": "^6.9.0", 21 | "react-scripts": "5.0.1", 22 | "recoil": "^0.7.7", 23 | "sass": "^1.60.0", 24 | "semantic-ui-css": "^2.5.0", 25 | "semantic-ui-react": "^2.1.4", 26 | "typescript": "^4.9.5", 27 | "web-vitals": "^2.1.4" 28 | }, 29 | "scripts": { 30 | "deploy": "gh-pages -d build", 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject", 35 | "lint": "eslint '*/**/*.{js,ts,tsx}' --fix --cache" 36 | }, 37 | "eslintConfig": { 38 | "extends": [ 39 | "react-app", 40 | "react-app/jest" 41 | ] 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@types/lodash": "^4.14.192" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heisch/gloomhaven-item-db/05900d3c31968b2de6ba6f4be5f6acae30698298/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heisch/gloomhaven-item-db/05900d3c31968b2de6ba6f4be5f6acae30698298/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Gloomhaven Item DB 24 | 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scripts/createitems.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const folderName = "../worldhaven/images/items/frosthaven"; 4 | 5 | const dirs = fs.readdirSync(folderName); 6 | 7 | let fileList = []; 8 | dirs.forEach((dir) => { 9 | const files = fs.readdirSync(`${folderName}/${dir}`); 10 | const filteredFiles = files.filter((name) => !name.includes("-back.png")); 11 | fileList = [ 12 | ...fileList, 13 | ...filteredFiles 14 | .map((file) => { 15 | const splitName = file.split("-"); 16 | const gameType = splitName[0]; 17 | const idStr = splitName[1]; 18 | if ( 19 | idStr.endsWith("b") || 20 | idStr.endsWith("c") || 21 | idStr.endsWith("d") 22 | ) { 23 | return null; 24 | } 25 | const count = idStr.endsWith("a") ? 2 : 1; 26 | const imageSuffix = idStr.endsWith("a") ? "a" : undefined; 27 | const id = parseInt(splitName[1], 10); 28 | const name = splitName.slice(2).join(" ").replace(".png", ""); 29 | const item = { 30 | id, 31 | gameType, 32 | name, 33 | count, 34 | cost: 50, 35 | slot: "head", 36 | source: "unknown", 37 | desc: "", 38 | folder: dir, 39 | imageSuffix, 40 | }; 41 | return item; 42 | }) 43 | .filter((item) => item), 44 | ]; 45 | }); 46 | 47 | fs.writeFileSync("./items.json", JSON.stringify(fileList, null, 2)); 48 | console.log(fileList.length); 49 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "semantic-ui-react"; 3 | import "semantic-ui-css/semantic.min.css"; 4 | import "./App.css"; 5 | import MainView from "./components/Tabs/MainView/MainView"; 6 | import { GameSelector } from "./components/GameSelector"; 7 | 8 | const App = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/Reddit.md: -------------------------------------------------------------------------------- 1 | ###**[Gloomhaven Item Database](https://heisch.github.io/gloomhaven-item-db/)** 2 | 3 | **Functionality:** Searchable/Filterable Database of all item cards. 4 | 5 | 6 | **Summary:** (12/11/2022) This is a simple web based app that can be used as a quick reference for viewing, searching and filtering items for 7 | - Gloomhaven 8 | - Forgotten Circles 9 | - Jaws of the Lion 10 | - Crimson Scales. 11 | - Frosthaven (Coming SOON!) 12 | 13 | **Pros:** 14 | 15 | - Simple UI, easy to interact with. (Searchable, Filterable) 16 | - Uses spoiler-friendly filtering to only show what items the party has unlocked to prevent spoilers. 17 | - Keeps track of what items are owned by what character 18 | - Share the database bewtween players so other team members can easily see what's available in the campaign 19 | 20 | **Cons:** 21 | 22 | - Only a reference tool -------------------------------------------------------------------------------- /src/State/CommonState.ts: -------------------------------------------------------------------------------- 1 | import { atom, RecoilState, selector } from "recoil"; 2 | import { gameTypeState } from "."; 3 | import { gameDataTypes, GameType } from "../games"; 4 | import { gameInfo } from "../games/GameInfo"; 5 | 6 | export const LOCAL_STORAGE_PREFIX = "ItemView:spoilerFilter_"; 7 | 8 | export type AtomObject = { 9 | [key: string]: RecoilState; 10 | }; 11 | 12 | export const dataDirtyState = atom({ 13 | key: "data-dirty-state", 14 | default: false, 15 | }); 16 | 17 | type FixUpFunction = (old: any, gameType: GameType, spoilerObj?: any) => T; 18 | 19 | function getDefaultValue( 20 | gameType: GameType, 21 | name: string, 22 | defaultValue: T, 23 | fixUp?: FixUpFunction 24 | ) { 25 | const key = LOCAL_STORAGE_PREFIX + gameType; 26 | const spoilerStorage = localStorage.getItem(key); 27 | const spoilerObj = spoilerStorage ? JSON.parse(spoilerStorage) : {}; 28 | const value = spoilerObj[name] || defaultValue; 29 | if (fixUp) { 30 | const newValue = fixUp(value, gameType, spoilerObj); 31 | spoilerObj[name] = newValue; 32 | localStorage.setItem(key, JSON.stringify(spoilerObj)); 33 | return newValue; 34 | } 35 | return value; 36 | } 37 | 38 | function storeValue(gameType: GameType, name: string, value: T) { 39 | const key = LOCAL_STORAGE_PREFIX + gameType; 40 | const spoilerStorage = localStorage.getItem(key); 41 | const spoilerObj = spoilerStorage ? JSON.parse(spoilerStorage) : {}; 42 | spoilerObj[name] = value; 43 | localStorage.setItem(key, JSON.stringify(spoilerObj)); 44 | } 45 | 46 | export function createState(name: string, defaultValue: T) { 47 | const atoms: AtomObject = {}; 48 | Object.values(gameDataTypes).forEach(({ gameType }) => { 49 | const gameName = gameInfo[gameType].title; 50 | atoms[gameType] = atom({ 51 | key: `${gameName}-${name}-state`, 52 | default: defaultValue, 53 | }); 54 | }); 55 | 56 | const stateSelector = selector({ 57 | key: `${name}-state`, 58 | get: ({ get }) => { 59 | const gameType: GameType = get(gameTypeState); 60 | return atoms[gameType]; 61 | }, 62 | set: ({ set, get }, newValue) => { 63 | const gameType: GameType = get(gameTypeState); 64 | return set(atoms[gameType], newValue); 65 | }, 66 | }); 67 | return stateSelector; 68 | } 69 | 70 | export function createSpoilerState( 71 | name: string, 72 | defaultValue: T, 73 | fixUp?: FixUpFunction 74 | ) { 75 | const atoms: AtomObject = {}; 76 | Object.values(gameDataTypes).forEach(({ gameType }) => { 77 | const gameName = gameInfo[gameType].title; 78 | atoms[gameType] = atom({ 79 | key: `${gameName}-${name}-state`, 80 | default: getDefaultValue(gameType, name, defaultValue, fixUp), 81 | effects: [ 82 | ({ onSet }) => { 83 | onSet((value) => { 84 | storeValue(gameType, name, value); 85 | }); 86 | }, 87 | ], 88 | }); 89 | }); 90 | 91 | const stateSelector = selector({ 92 | key: `${name}-state`, 93 | get: ({ get }) => { 94 | const gameType: GameType = get(gameTypeState); 95 | return atoms[gameType]; 96 | }, 97 | set: ({ set, get }, newValue) => { 98 | set(dataDirtyState, true); 99 | const gameType: GameType = get(gameTypeState); 100 | return set(atoms[gameType], newValue); 101 | }, 102 | }); 103 | 104 | return stateSelector; 105 | } 106 | -------------------------------------------------------------------------------- /src/State/GameTypeState.ts: -------------------------------------------------------------------------------- 1 | import { atom, selector } from "recoil"; 2 | import { gameDataTypes, GameType } from "../games"; 3 | import { GameData } from "../games/GameData"; 4 | import QueryString from "qs"; 5 | 6 | const getStartingGameType = () => { 7 | const urlParams = QueryString.parse(window.location.search.substr(1)); 8 | const lastGameQSP = urlParams["lastGame"] as GameType; 9 | if (lastGameQSP) { 10 | if (Object.values(GameType).includes(lastGameQSP)) { 11 | localStorage.setItem("lastGame", lastGameQSP); 12 | return lastGameQSP; 13 | } 14 | } 15 | 16 | const lastGame = localStorage.getItem("lastGame") as GameType; 17 | if (!lastGame) { 18 | return GameType.Gloomhaven; 19 | } 20 | return lastGame; 21 | }; 22 | 23 | export const gameTypeState = atom({ 24 | key: "gameTypeState", 25 | default: getStartingGameType(), 26 | effects: [ 27 | ({ onSet }) => { 28 | onSet((gameType) => { 29 | localStorage.setItem("lastGame", gameType); 30 | }); 31 | }, 32 | ], 33 | }); 34 | 35 | export const gameDataState = selector({ 36 | key: "gameDataState", 37 | get: ({ get }) => { 38 | const gameType: GameType = get(gameTypeState); 39 | return gameDataTypes[gameType]; 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/State/PopupStatusState.ts: -------------------------------------------------------------------------------- 1 | import { createState } from "./CommonState"; 2 | import { ClassesInUse, GloomhavenItem } from "./Types"; 3 | 4 | export const classToDeleteState = createState( 5 | "classToDelete", 6 | undefined 7 | ); 8 | export const confirmSpecialUnlockOpenState = createState( 9 | "ConfirmSpecialUnlockOpen", 10 | undefined 11 | ); 12 | export const selectedItemState = createState( 13 | "SelectedItem", 14 | undefined 15 | ); 16 | -------------------------------------------------------------------------------- /src/State/SearchState.ts: -------------------------------------------------------------------------------- 1 | import { AllGames } from "../games/GameType"; 2 | import { createState } from "./CommonState"; 3 | import { 4 | ClassesInUse, 5 | GloomhavenItemSlot, 6 | ResourceTypes, 7 | SortDirection, 8 | SortProperty, 9 | } from "./Types"; 10 | 11 | export const slotsState = createState("slotState", []); 12 | export const resourcesState = createState( 13 | "resourcesState", 14 | [] 15 | ); 16 | export const availableOnlyState = createState("availableOnly", false); 17 | export const searchState = createState("search", ""); 18 | export const selectedClassState = createState( 19 | "selectedClass", 20 | undefined 21 | ); 22 | export const sortDirectionState = createState( 23 | "sortDirection", 24 | SortDirection.ascending 25 | ); 26 | export const sortPropertyState = createState( 27 | "sortProperty", 28 | SortProperty.Id 29 | ); 30 | export const removingGameState = createState( 31 | "removingGame", 32 | undefined 33 | ); 34 | -------------------------------------------------------------------------------- /src/State/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GameTypeState"; 2 | export * from "./SearchState"; 3 | export * from "./PopupStatusState"; 4 | export * from "./SpoilerState"; 5 | export * from "./Types"; 6 | -------------------------------------------------------------------------------- /src/components/Firebase/config.ts: -------------------------------------------------------------------------------- 1 | export const firebaseConfig = { 2 | apiKey: "AIzaSyDZjS32Jz6XHm56VxxvUyJHmdgEPFBvyU4", 3 | authDomain: "gloomhaven-item-db-db.firebaseapp.com", 4 | databaseURL: "https://gloomhaven-item-db-db.firebaseio.com", 5 | projectId: "gloomhaven-item-db-db", 6 | storageBucket: "gloomhaven-item-db-db.appspot.com", 7 | messagingSenderId: "1055959523566", 8 | appId: "1:1055959523566:web:c9e9dd38591be00ea493ff", 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Firebase/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./FirebaseProvider"; 2 | -------------------------------------------------------------------------------- /src/components/GameSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState } from "recoil"; 3 | import { DropdownProps, Form } from "semantic-ui-react"; 4 | import { gameDataTypes, GameType } from "../games"; 5 | import { gameInfo } from "../games/GameInfo"; 6 | import { gameTypeState } from "../State"; 7 | 8 | export const GameSelector = () => { 9 | const [gameType, setGameType] = useRecoilState(gameTypeState); 10 | 11 | const options: any[] = []; 12 | Object.values(gameDataTypes).forEach((gameData) => { 13 | const { gameType: value } = gameData; 14 | const text = gameInfo[value].title; 15 | options.push({ text, value }); 16 | }); 17 | 18 | return ( 19 | { 23 | setGameType(e.value as GameType); 24 | }} 25 | /> 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Tabs/About.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const About = () => { 4 | return ( 5 | <> 6 |

7 | Thanks for using the Gloomhaven Item Database. If you find any 8 | issues with the content or bugs with the db you can{" "} 9 | 14 | report issues here 15 | 16 | . 17 |

18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/Tabs/Account/Account.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Form } from "semantic-ui-react"; 3 | import { SignedInAccount } from "./SignedInAccount"; 4 | import { SignedOutAccount } from "./SignedOutAccount"; 5 | 6 | export const Account = () => { 7 | // Create a provider for all this and lock spoiler 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/components/Tabs/Account/PasswordChangeDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, useState } from "react"; 2 | import { Button, Form, List, Modal } from "semantic-ui-react"; 3 | import { useFirebase } from "../../Firebase"; 4 | 5 | type Props = { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | }; 9 | 10 | export const PasswordChangeDialog = (props: Props) => { 11 | const { isOpen, onClose } = props; 12 | const { passwordUpdate, error } = useFirebase(); 13 | const [passwordOne, setPasswordOne] = useState(""); 14 | const [passwordTwo, setPasswordTwo] = useState(""); 15 | 16 | const onSubmit = (event: any) => { 17 | passwordUpdate(passwordOne); 18 | event.preventDefault(); 19 | }; 20 | 21 | const isInvalid = passwordOne !== passwordTwo || passwordOne === ""; 22 | 23 | return ( 24 | 25 | Change Password 26 | 27 |
28 | 29 | 30 | ) => 34 | setPasswordOne(e.target.value) 35 | } 36 | type="password" 37 | placeholder="New Password" 38 | /> 39 | 40 | 41 | ) => 45 | setPasswordTwo(e.target.value) 46 | } 47 | type="password" 48 | placeholder="Confirm New Password" 49 | /> 50 | 51 | {error &&

{error.message}

}
52 | 53 |
54 |
55 |
56 | 57 | 60 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Tabs/Account/SignedOutAccount.tsx: -------------------------------------------------------------------------------- 1 | import { EmailAuthProvider, GoogleAuthProvider } from "@firebase/auth"; 2 | import React from "react"; 3 | import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth"; 4 | import { Form } from "semantic-ui-react"; 5 | import { auth, useFirebase } from "../../Firebase"; 6 | 7 | const uiConfig = { 8 | signInFlow: "popup", 9 | signInOptions: [ 10 | { 11 | provider: GoogleAuthProvider.PROVIDER_ID, 12 | customParameters: { 13 | prompt: "select_account", 14 | auth_type: "reauthenticate", 15 | }, 16 | }, 17 | EmailAuthProvider.PROVIDER_ID, 18 | ], 19 | }; 20 | 21 | export const SignedOutAccount = (): JSX.Element | null => { 22 | const { user } = useFirebase(); 23 | 24 | if (user) { 25 | return null; 26 | } 27 | return ( 28 |
29 |

Sign In

30 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Grid/ItemCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { GloomhavenItem } from "../../../../../State/Types"; 3 | import { Label } from "semantic-ui-react"; 4 | import { getItemPath } from "../../../../../games/GameData"; 5 | import { GHIcon } from "../../../../Utils"; 6 | import { getItemIdString } from "../../../../../helpers"; 7 | import { ItemManagementContainer } from "../ItemManagement/ItemManagementContainer"; 8 | import { NoItemManagement } from "../ItemManagement/NoItemManagement"; 9 | 10 | import "./itemCard.scss"; 11 | 12 | type Props = { 13 | item: GloomhavenItem; 14 | }; 15 | 16 | const ItemId = (props: Props) => { 17 | const { item } = props; 18 | const id = getItemIdString(item); 19 | return
{id}
; 20 | }; 21 | 22 | const ItemCard = (props: Props) => { 23 | const { item } = props; 24 | 25 | const [draw, setDraw] = useState(false); 26 | const [showBackside, setShowBackside] = useState(false); 27 | 28 | return ( 29 |
30 | {draw && ( 31 |
32 | 33 | {item.backDesc && ( 34 | 42 | setShowBackside((current) => !current) 43 | } 44 | /> 45 | )} 46 |
47 | )} 48 | {item.name} setDraw(true)} 52 | className={"item-card"} 53 | /> 54 | {draw && ( 55 |
56 | 57 |
58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default ItemCard; 64 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Grid/ItemGrid.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GloomhavenItem } from "../../../../../State/Types"; 3 | import ItemCard from "./ItemCard"; 4 | 5 | import "./itemGrid.scss"; 6 | 7 | type Props = { 8 | items: GloomhavenItem[]; 9 | }; 10 | 11 | export const ItemGrid = (props: Props) => { 12 | const { items } = props; 13 | return ( 14 |
15 | {items.map((item) => { 16 | let key = `${item.id}`; 17 | if (item.imageSuffix) { 18 | key += `-${item.imageSuffix}`; 19 | } 20 | return ; 21 | })} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Grid/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ItemGrid"; 2 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Grid/itemCard.scss: -------------------------------------------------------------------------------- 1 | .item-card-container { 2 | max-width: 20%; 3 | aspect-ratio: 0.63; 4 | position: relative; 5 | padding: 2px; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | @media screen and (min-width: 768px) and (max-width: 990px) { 10 | max-width: 25%; 11 | } 12 | 13 | 14 | @media screen and (min-width: 560px) and (max-width: 768px) { 15 | max-width: 33.3333333333333%; 16 | } 17 | 18 | @media screen and (min-width: 390px) and (max-width: 560px) { 19 | max-width: 50%; 20 | } 21 | 22 | @media screen and (max-width: 390px) { 23 | max-width: 100%; 24 | } 25 | 26 | 27 | &-header { 28 | border-radius: 8px 8px 0px 0px; 29 | background: black; 30 | display: flex; 31 | justify-content: space-between; 32 | height: 7%; 33 | z-index: 1; // required because of the negative margin-top on .item-card 34 | 35 | .item-card-id { 36 | font-size: 1.1vh; 37 | font-weight: 700; 38 | width: 50%; 39 | padding-top: 0px; 40 | padding-left: 10px; 41 | color: white; 42 | border-radius: 8px 8px 0px 0px; 43 | } 44 | } 45 | 46 | .item-card { 47 | width: 100%; 48 | // height: auto; 49 | filter: brightness(105%) contrast(90%); 50 | height: 77%; 51 | margin-top: -4%; // you can get away with a bit of negative margins since there's no info you lose on the cards 52 | margin-bottom: -4%; 53 | } 54 | 55 | &-footer { 56 | display: flex; 57 | border-radius: 0px 0px 8px 8px; 58 | background: black; 59 | justify-content: space-between; 60 | margin: 0px; 61 | height: 16%; 62 | position: relative; 63 | } 64 | 65 | .ui.label.no-item-management-count { 66 | font-size: 1.1vh; 67 | width: 100%; 68 | color: white; 69 | display: flex; 70 | justify-content: center; 71 | align-items: center; 72 | border-radius: 0px 0px 8px 8px; 73 | background: transparent; 74 | } 75 | } 76 | 77 | .item-card-wrapper .ui.red.button { 78 | padding: 0; 79 | z-index: 2; 80 | position: absolute; 81 | } -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Grid/itemGrid.scss: -------------------------------------------------------------------------------- 1 | .item-grid { 2 | display: flex; 3 | flex-direction: row; 4 | flex-flow: wrap; 5 | } -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ItemManagementType } from "../../../../State/Types"; 3 | import SearchOptions from "./Search/SearchOptions"; 4 | import { Message, Icon } from "semantic-ui-react"; 5 | import PurchaseItem from "./PurchaseItem"; 6 | import { useRecoilValue } from "recoil"; 7 | import { 8 | allState, 9 | itemManagementTypeState, 10 | dataMismatchState, 11 | } from "../../../../State"; 12 | import { ItemsView } from "./ItemsView"; 13 | 14 | export const ItemList = () => { 15 | const all = useRecoilValue(allState); 16 | const itemManagementType = useRecoilValue(itemManagementTypeState); 17 | const dataMismatch = useRecoilValue(dataMismatchState); 18 | 19 | return ( 20 | <> 21 | {dataMismatch && ( 22 | 23 | 24 | 25 | Data out of sync 26 | 27 | Spoiler configuration differs from cloud storage. Remember 28 | to export your data. 29 | 30 | )} 31 | 32 | {all && ( 33 | 34 | 35 | 36 | Spoiler alert 37 | 38 | You are currently viewing all possible items. 39 | 40 | )} 41 | 42 | 43 | {itemManagementType === ItemManagementType.Party && ( 44 | 45 | )} 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/ItemManagementContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GloomhavenItem } from "../../../../../State/Types"; 3 | import { NoItemManagement } from "./NoItemManagement"; 4 | import { PartyItemManagement } from "./PartyItemManagement"; 5 | import { SimpleItemManagement } from "./SimpleItemManagement"; 6 | 7 | type Props = { 8 | item: GloomhavenItem; 9 | }; 10 | 11 | export const ItemManagementContainer = (props: Props) => { 12 | const { item } = props; 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/NoItemManagement.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilValue } from "recoil"; 3 | import { Label } from "semantic-ui-react"; 4 | import { itemManagementTypeState } from "../../../../../State"; 5 | import { GloomhavenItem, ItemManagementType } from "../../../../../State/Types"; 6 | 7 | type Props = { 8 | item: GloomhavenItem; 9 | }; 10 | 11 | export const NoItemManagement = (props: Props) => { 12 | const { item } = props; 13 | const itemManagementType = useRecoilValue(itemManagementTypeState); 14 | if (itemManagementType !== ItemManagementType.None) { 15 | return null; 16 | } 17 | 18 | return ( 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/PartyItemManagement.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilValue, useSetRecoilState } from "recoil"; 3 | import { useRemovePlayerUtils } from "../../../../../hooks/useRemovePlayer"; 4 | import { 5 | classesInUseState, 6 | itemManagementTypeState, 7 | itemsOwnedByState, 8 | lockSpoilerPanelState, 9 | selectedItemState, 10 | } from "../../../../../State"; 11 | import { 12 | ClassesInUse, 13 | GloomhavenItem, 14 | ItemManagementType, 15 | } from "../../../../../State/Types"; 16 | import { ClassIcon, GHIcon } from "../../../../Utils"; 17 | 18 | import "./partyItemManagement.scss"; 19 | 20 | type Props = { 21 | item: GloomhavenItem; 22 | }; 23 | 24 | type OwnerProps = { 25 | item: GloomhavenItem; 26 | owner?: ClassesInUse; 27 | }; 28 | 29 | const OwnerButton = (props: OwnerProps) => { 30 | const { removeItemsFromOwner } = useRemovePlayerUtils(); 31 | const setSelectedItem = useSetRecoilState(selectedItemState); 32 | const lockSpoilerPanel = useRecoilValue(lockSpoilerPanelState); 33 | 34 | const { item, owner } = props; 35 | let classNames = `ownerButton`; 36 | const onClick = () => { 37 | if (owner) { 38 | removeItemsFromOwner(item.id, owner); 39 | } else { 40 | setSelectedItem(item); 41 | } 42 | }; 43 | 44 | return ( 45 | 59 | ); 60 | }; 61 | 62 | export const PartyItemManagement = (props: Props) => { 63 | const classesInUse = useRecoilValue(classesInUseState); 64 | const itemsOwnedBy = useRecoilValue(itemsOwnedByState); 65 | const itemManagementType = useRecoilValue(itemManagementTypeState); 66 | const { item } = props; 67 | 68 | if (itemManagementType !== ItemManagementType.Party) { 69 | return null; 70 | } 71 | 72 | if (item.lockToClasses) { 73 | const classesCount = item.lockToClasses.filter((c) => 74 | classesInUse.includes(c) 75 | ).length; 76 | if (classesCount === 0) { 77 | return null; 78 | } 79 | } 80 | const owners = (itemsOwnedBy && itemsOwnedBy[item.id]) || []; 81 | const ownersLength = owners ? owners.length : 0; 82 | const classesAvailable = ownersLength 83 | ? classesInUse.filter((c) => !owners.includes(c)) 84 | : classesInUse; 85 | 86 | const addButtonsToShow = 87 | classesAvailable.length > 0 88 | ? Math.min(item.count - ownersLength, 4) 89 | : 0; 90 | let buttonData: OwnerProps[] = owners.map((owner, index) => ({ 91 | owner, 92 | item, 93 | })); 94 | buttonData = buttonData.concat( 95 | [...Array(addButtonsToShow).keys()].map((index) => ({ 96 | item, 97 | owner: undefined, 98 | })) 99 | ); 100 | return ( 101 |
102 | {buttonData.map((data, i) => ( 103 | 104 | ))} 105 |
106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/SimpleItemManagement.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Checkbox } from "semantic-ui-react"; 4 | import { 5 | itemManagementTypeState, 6 | itemsInUseCountState, 7 | lockSpoilerPanelState, 8 | } from "../../../../../State"; 9 | import { GloomhavenItem, ItemManagementType } from "../../../../../State/Types"; 10 | 11 | import "./simpleItemManagement.scss"; 12 | 13 | type Props = { 14 | item: GloomhavenItem; 15 | }; 16 | 17 | export const SimpleItemManagement = (props: Props) => { 18 | const [itemsInUseCount, setItemsInUseCount] = 19 | useRecoilState(itemsInUseCountState); 20 | const lockSpoilerPanel = useRecoilValue(lockSpoilerPanelState); 21 | const itemManagementType = useRecoilValue(itemManagementTypeState); 22 | const { item } = props; 23 | 24 | if (itemManagementType !== ItemManagementType.Simple) { 25 | return null; 26 | } 27 | 28 | const count = itemsInUseCount[item.id] || 0; 29 | 30 | const onClick = (checked: boolean) => { 31 | setItemsInUseCount((current) => { 32 | const value = Object.assign({}, current); 33 | value[item.id] = (value[item.id] || 0) + (checked ? -1 : 1); 34 | if (value[item.id] === 0) { 35 | delete value[item.id]; 36 | } 37 | return value; 38 | }); 39 | }; 40 | 41 | return ( 42 |
43 | {[...Array(item.count).keys()].map((index) => ( 44 | onClick(index < count)} 49 | disabled={lockSpoilerPanel} 50 | /> 51 | ))} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/items.ts: -------------------------------------------------------------------------------- 1 | export * from "./NoItemManagement"; 2 | export * from "./ItemManagementContainer"; 3 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/partyItemManagement.scss: -------------------------------------------------------------------------------- 1 | .item-card-container { 2 | .party-management-container { 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: row; 7 | 8 | .ownerButton { 9 | padding: 0; 10 | z-index: 2; 11 | width: 19%; 12 | aspect-ratio: 1; 13 | border-radius: 50%; 14 | border: unset; 15 | margin: 5px; 16 | position: relative; 17 | } 18 | } 19 | 20 | .addIcon { 21 | width: 100%; 22 | height: 100%; 23 | color: black; 24 | padding: 25%; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | 30 | .deleteIcon { 31 | height: 40%; 32 | width: 40%; 33 | position: absolute; 34 | z-index: 3; 35 | border-radius: 50%; 36 | background-color: white; 37 | right: 0; 38 | bottom: 0; 39 | } 40 | 41 | img.ui.image.ownerIcon { 42 | width: 100%; 43 | height: 100%; 44 | opacity: 1; 45 | } 46 | 47 | .flip { 48 | width: auto; 49 | height: 100%; 50 | vertical-align: baseline; 51 | margin-right: 10px; 52 | } 53 | } 54 | 55 | .store-inventory-col { 56 | .party-management-container { 57 | height: 75px; 58 | width: 75px; 59 | display: flex; 60 | flex-direction: column; 61 | flex-wrap: wrap; 62 | justify-content: space-between; 63 | position: relative; 64 | } 65 | 66 | .ownerButton { 67 | padding: 0; 68 | z-index: 2; 69 | width: 35px; 70 | height: 35px; 71 | border-radius: 50%; 72 | border: unset; 73 | position: relative; 74 | } 75 | 76 | img.ui.image.ownerIcon { 77 | width: 100%; 78 | height: 100%; 79 | opacity: 1; 80 | } 81 | 82 | .deleteIcon { 83 | height: 15px; 84 | width: 15px; 85 | position: absolute; 86 | z-index: 3; 87 | border-radius: 50%; 88 | background-color: white; 89 | right: 0; 90 | bottom: 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemManagement/simpleItemManagement.scss: -------------------------------------------------------------------------------- 1 | .item-card-container { 2 | .simple-management-container { 3 | width: 100%; 4 | position: absolute; 5 | display: flex; 6 | flex-flow: wrap; 7 | align-content: center; 8 | height: 100%; 9 | 10 | .ui.fitted.checkbox{ 11 | width: 25%; 12 | justify-content: center; 13 | display: flex; 14 | left: -7px; 15 | } 16 | } 17 | } 18 | 19 | .store-inventory-col { 20 | .simple-management-container { 21 | .ui.fitted.checkbox{ 22 | width: 50%; 23 | padding: 0 5px; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/ItemsView.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilValue } from "recoil"; 3 | import { Message } from "semantic-ui-react"; 4 | import useItems from "../../../../hooks/useItems"; 5 | import { displayItemAsState } from "../../../../State"; 6 | import { ItemViewDisplayType } from "../../../../State/Types"; 7 | import { ItemGrid } from "./Grid"; 8 | import { ItemTable } from "./Table"; 9 | 10 | export const ItemsView = () => { 11 | const items = useItems(); 12 | const displayAs = useRecoilValue(displayItemAsState); 13 | return ( 14 | <> 15 | {items.length === 0 && ( 16 | 17 | No items found matching your filters and/or search criteria 18 | 19 | )} 20 | 21 | {displayAs === ItemViewDisplayType.List ? ( 22 | 23 | ) : ( 24 | 25 | )} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/Discount.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilValue } from "recoil"; 3 | import { Form } from "semantic-ui-react"; 4 | import { 5 | discountState, 6 | displayItemAsState, 7 | ItemViewDisplayType, 8 | } from "../../../../../State"; 9 | 10 | export const Discount = () => { 11 | const discount = useRecoilValue(discountState); 12 | const displayAs = useRecoilValue(displayItemAsState); 13 | 14 | if (displayAs !== ItemViewDisplayType.Images) { 15 | return null; 16 | } 17 | return ( 18 | 19 | 20 | {`${discount}g`} 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/FilterAvailability.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Button, Form } from "semantic-ui-react"; 4 | import { 5 | availableOnlyState, 6 | ItemManagementType, 7 | itemManagementTypeState, 8 | } from "../../../../../State"; 9 | 10 | type Props = { 11 | available: boolean; 12 | text: string; 13 | }; 14 | 15 | const FilterAvailabilityButton = (props: Props) => { 16 | const [availableOnly, setAvailableOnly] = 17 | useRecoilState(availableOnlyState); 18 | const { available, text } = props; 19 | return ( 20 | 28 | ); 29 | }; 30 | 31 | export const FilterAvailability = () => { 32 | const itemManagementType = useRecoilValue(itemManagementTypeState); 33 | if (itemManagementType === ItemManagementType.None) { 34 | return null; 35 | } 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/FilterClass.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Form } from "semantic-ui-react"; 4 | import { 5 | classesInUseState, 6 | itemManagementTypeState, 7 | selectedClassState, 8 | } from "../../../../../State"; 9 | import { ClassesInUse, ItemManagementType } from "../../../../../State/Types"; 10 | import { ClassList } from "../../../SpoilerFilters/Party/ClassList"; 11 | 12 | export const FilterClass = () => { 13 | const itemManagementType = useRecoilValue(itemManagementTypeState); 14 | const classesInUse = useRecoilValue(classesInUseState); 15 | const [selectedClass, setSelectedClass] = 16 | useRecoilState(selectedClassState); 17 | 18 | if (itemManagementType !== ItemManagementType.Party) { 19 | return null; 20 | } 21 | return ( 22 | 23 | { 27 | if (selectedClass === option) { 28 | setSelectedClass(undefined); 29 | } else { 30 | setSelectedClass(option); 31 | } 32 | }} 33 | isUsed={(options: ClassesInUse) => selectedClass === options} 34 | /> 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/FilterResources.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Form } from "semantic-ui-react"; 4 | import { gameDataState, resourcesState } from "../../../../../State"; 5 | import { ResourceTypes } from "../../../../../State/Types"; 6 | import { GHIcon } from "../../../../Utils"; 7 | 8 | export const FilterResorces = () => { 9 | const [resources, setResourcesState] = useRecoilState(resourcesState); 10 | const { resources: gameResource } = useRecoilValue(gameDataState); 11 | 12 | const setFilterResource = useCallback( 13 | (resource?: ResourceTypes) => { 14 | if (!resource) { 15 | setResourcesState([]); 16 | return; 17 | } 18 | const value = Object.assign([], resources); 19 | const index = value.indexOf(resource); 20 | if (index !== -1) { 21 | value.splice(index, 1); 22 | } else { 23 | value.push(resource); 24 | } 25 | setResourcesState(value); 26 | }, 27 | [resources, setResourcesState] 28 | ); 29 | 30 | if (!gameResource || gameResource.length === 0) { 31 | return null; 32 | } 33 | return ( 34 | 35 | 36 | setFilterResource(undefined)} 40 | /> 41 | {gameResource && 42 | gameResource.map((resource) => ( 43 | 50 | } 51 | checked={resources.includes(resource as ResourceTypes)} 52 | onChange={() => 53 | setFilterResource(resource as ResourceTypes) 54 | } 55 | alt={resource} 56 | /> 57 | ))} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/FilterSlots.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Form } from "semantic-ui-react"; 4 | import { gameDataState, slotsState } from "../../../../../State"; 5 | import { GloomhavenItemSlot } from "../../../../../State/Types"; 6 | import { GHIcon } from "../../../../Utils"; 7 | 8 | export const FilterSlots = () => { 9 | const [slots, setSlotsState] = useRecoilState(slotsState); 10 | const { filterSlots } = useRecoilValue(gameDataState); 11 | const setFilterSlot = useCallback( 12 | (slot?: GloomhavenItemSlot) => { 13 | if (!slot) { 14 | setSlotsState([]); 15 | return; 16 | } 17 | const value = Object.assign([], slots); 18 | const index = value.indexOf(slot); 19 | if (index !== -1) { 20 | value.splice(index, 1); 21 | } else { 22 | value.push(slot); 23 | } 24 | setSlotsState(value); 25 | }, 26 | [slots, setSlotsState] 27 | ); 28 | return ( 29 | 30 | 31 | setFilterSlot(undefined)} 35 | /> 36 | {filterSlots.map((itemSlot) => ( 37 | 44 | } 45 | checked={slots.includes(itemSlot)} 46 | onChange={() => setFilterSlot(itemSlot)} 47 | alt={itemSlot} 48 | /> 49 | ))} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/FindItemSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState } from "recoil"; 3 | import { Form, Input } from "semantic-ui-react"; 4 | import { searchState } from "../../../../../State"; 5 | 6 | export const FindItemSearchBar = () => { 7 | const [searchString, setSearchString] = useRecoilState(searchState); 8 | return ( 9 | 10 | 11 | { 14 | setSearchString(e.target.value); 15 | }} 16 | icon={{ 17 | name: "close", 18 | link: true, 19 | onClick: () => setSearchString(""), 20 | }} 21 | placeholder={"Search..."} 22 | /> 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/RenderAs.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState } from "recoil"; 3 | import { Button, Form } from "semantic-ui-react"; 4 | import { displayItemAsState } from "../../../../../State"; 5 | import { ItemViewDisplayType } from "../../../../../State/Types"; 6 | 7 | type Props = { 8 | type: ItemViewDisplayType; 9 | text: string; 10 | }; 11 | const RenderAsButton = (props: Props) => { 12 | const { type, text } = props; 13 | const [displayAs, setDisplayAs] = useRecoilState(displayItemAsState); 14 | return ( 15 | 23 | ); 24 | }; 25 | 26 | export const RenderAs = () => { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/SearchOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Segment } from "semantic-ui-react"; 3 | import { 4 | RenderAs, 5 | FilterSlots, 6 | FilterResorces, 7 | FindItemSearchBar, 8 | FilterClass, 9 | FilterAvailability, 10 | SortItems, 11 | Discount, 12 | } from "."; 13 | 14 | const SearchOptions = () => { 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default SearchOptions; 32 | -------------------------------------------------------------------------------- /src/components/Tabs/MainView/Items/Search/SortItems.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import { Button, Form, Icon } from "semantic-ui-react"; 4 | import { useSetSorting } from "../../../../../hooks/useSetSorting"; 5 | import { 6 | displayItemAsState, 7 | ItemViewDisplayType, 8 | SortDirection, 9 | sortDirectionState, 10 | SortProperty, 11 | sortPropertyState, 12 | } from "../../../../../State"; 13 | 14 | export const SortItems = () => { 15 | const setSorting = useSetSorting(); 16 | 17 | const displayAs = useRecoilValue(displayItemAsState); 18 | const sortProperty = useRecoilValue(sortPropertyState); 19 | const [sortDirection, setSortDirection] = 20 | useRecoilState(sortDirectionState); 21 | 22 | const toggleSortDirection = useCallback(() => { 23 | setSortDirection( 24 | sortDirection === SortDirection.ascending 25 | ? SortDirection.descending 26 | : SortDirection.ascending 27 | ); 28 | }, [setSortDirection, sortDirection]); 29 | 30 | if (displayAs !== ItemViewDisplayType.Images) { 31 | return null; 32 | } 33 | 34 | return ( 35 | <> 36 | 37 | 38 | setSorting(e.value as SortProperty)} 49 | /> 50 | 72 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/Tabs/SpoilerFilters/Games/ConfirmGameRemoval.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Modal, Form, List } from "semantic-ui-react"; 3 | import { useRecoilState } from "recoil"; 4 | import { removingGameState, includeGameState } from "../../../../State"; 5 | import { useRemovePlayerUtils } from "../../../../hooks/useRemovePlayer"; 6 | import { gameInfo } from "../../../../games/GameInfo"; 7 | 8 | export const ConfirmGameRemoval = () => { 9 | const { removeClasses, getClassesToRemove, getRemovingItemCount } = 10 | useRemovePlayerUtils(); 11 | const [removingGame, setRemovingGame] = useRecoilState(removingGameState); 12 | const [includeGames, setIncludeGames] = useRecoilState(includeGameState); 13 | 14 | const onClose = () => { 15 | setRemovingGame(undefined); 16 | }; 17 | 18 | const onApply = () => { 19 | if (!removingGame) { 20 | return; 21 | } 22 | // Go through all classes and see if any of them are used.. 23 | // if so then remove their ownership 24 | const classesToRemove = getClassesToRemove(removingGame); 25 | if (classesToRemove.length > 0) { 26 | removeClasses(classesToRemove, removingGame); 27 | } 28 | 29 | // Remove the game 30 | const value = Object.assign([], includeGames); 31 | value.splice(value.indexOf(removingGame), 1); 32 | setIncludeGames(value); 33 | onClose(); 34 | }; 35 | 36 | if (!removingGame) { 37 | return null; 38 | } 39 | 40 | const { title } = gameInfo[removingGame]; 41 | 42 | return ( 43 | 44 | {`Remove ${title}`} 45 | 46 |
47 | {`Remove ${title} from the db?`} 48 | {removingGame && ( 49 | 50 | 51 | Confirming this will: 52 | {getClassesToRemove(removingGame).length > 53 | 0 && ( 54 | 55 | {`Remove this game's classes from the 56 | party`} 57 | 58 | )} 59 | {getRemovingItemCount(removingGame) > 0 && ( 60 | 61 | {`Put any items owned by this game's 62 | classes back into available inventory`} 63 | 64 | )} 65 | 66 | 67 | )} 68 |
69 |
70 | 71 | 74 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/Tabs/SpoilerFilters/Items/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ToggleAllButton"; 2 | export * from "./GameFilter"; 3 | -------------------------------------------------------------------------------- /src/components/Tabs/SpoilerFilters/Party/ClassList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form } from "semantic-ui-react"; 3 | import { ClassesInUse } from "../../../../State/Types"; 4 | import { ClassIcon } from "../../../Utils"; 5 | 6 | type Props = { 7 | label: string; 8 | classes: ClassesInUse[]; 9 | onClick: (className: ClassesInUse) => void; 10 | isEnabled?: (className: ClassesInUse) => boolean; 11 | isUsed: (className: ClassesInUse) => boolean; 12 | }; 13 | 14 | export const ClassList = (props: Props) => { 15 | const { label, classes, onClick, isEnabled, isUsed } = props; 16 | 17 | return ( 18 | 19 | 20 | {classes.map((name) => { 21 | return ( 22 | 30 | ); 31 | })} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Tabs/SpoilerFilters/Party/ConfirmClassDelete.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Button, 4 | Modal, 5 | Form, 6 | Accordion, 7 | AccordionTitle, 8 | AccordionContent, 9 | Icon, 10 | } from "semantic-ui-react"; 11 | import { ClassIcon } from "../../../Utils"; 12 | import { useRecoilState, useRecoilValue } from "recoil"; 13 | import { classToDeleteState, gameDataState } from "../../../../State"; 14 | import { useRemovePlayerUtils } from "../../../../hooks/useRemovePlayer"; 15 | import { itemGoldValue, OwnedItemList } from "./OwnedItemsList"; 16 | import { useIsItemShown } from "../../../../hooks/useIsItemShown"; 17 | 18 | export const ConfirmClassDelete = () => { 19 | const { removeClasses, itemsOwnedByClass } = useRemovePlayerUtils(); 20 | const isItemShown = useIsItemShown(); 21 | const [classToDelete, setClassToDelete] = 22 | useRecoilState(classToDeleteState); 23 | const [itemsOpen, setItemsOpen] = useState(false); 24 | 25 | const { items } = useRecoilValue(gameDataState); 26 | 27 | const onClose = () => { 28 | setClassToDelete(undefined); 29 | }; 30 | 31 | const itemsOwned = itemsOwnedByClass(classToDelete); 32 | const itemsToList = itemsOwned 33 | .map((id) => items[id - 1]) 34 | .filter(isItemShown); 35 | 36 | const goldAmount = () => { 37 | let totalGold = 0; 38 | itemsToList.forEach((item) => { 39 | totalGold += itemGoldValue(item); 40 | }); 41 | return totalGold; 42 | }; 43 | 44 | const onApply = () => { 45 | if (classToDelete) { 46 | removeClasses(classToDelete); 47 | setClassToDelete(undefined); 48 | } 49 | onClose(); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | Remove Class 56 | {classToDelete && ( 57 | 61 | )} 62 | 63 | 64 |
65 | Remove this class from the party? 66 | {itemsOwned.length > 0 && ( 67 | <> 68 | 69 |

70 | Warning: This will remove all the items 71 | assigned to this character. 72 |

73 |
74 | 75 |

76 | {`If you are retiring this character, they would get `} 77 | {`${goldAmount()}g`} 83 | {` for their items`} 84 |

85 |
86 | 87 | 90 | setItemsOpen((current) => !current) 91 | } 92 | > 93 | 94 | {`Items Owned - ${ 95 | itemsOpen 96 | ? "Click to hide" 97 | : "Click to show" 98 | } items`} 99 | 100 | 101 | 105 | 106 | 107 | 108 | )} 109 |
110 |
111 | 112 | 115 |