]> git.localhorst.tv Git - alttp.git/commitdiff
zootr mp tracker prototype
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 4 May 2025 20:21:04 +0000 (22:21 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 4 May 2025 20:21:04 +0000 (22:21 +0200)
package-lock.json
package.json
resources/js/app/Routes.js
resources/js/components/zootr/MixedPoolsTracker.js [new file with mode: 0644]
resources/js/pages/ZootrMixedPoolsTracker.js [new file with mode: 0644]
resources/sass/app.scss
resources/sass/zootr.scss [new file with mode: 0644]

index 0885cc4f240b8484c29c3438379fd57af2207997..736ff49bfdd56c5fa24fb2c0d1fe1ea7ec09110a 100644 (file)
@@ -18,6 +18,7 @@
                 "d3-drag": "^3.0.0",
                 "file-saver": "^2.0.5",
                 "formik": "^2.2.9",
+                "fuzzy-search": "^3.2.1",
                 "i18next": "^23.4.9",
                 "i18next-browser-languagedetector": "^8.0.0",
                 "laravel-echo": "^1.16.1",
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/fuzzy-search": {
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/fuzzy-search/-/fuzzy-search-3.2.1.tgz",
+            "integrity": "sha512-vAcPiyomt1ioKAsAL2uxSABHJ4Ju/e4UeDM+g1OlR0vV4YhLGMNsdLNvZTpEDY4JCSt0E4hASCNM5t2ETtsbyg=="
+        },
         "node_modules/gensync": {
             "version": "1.0.0-beta.2",
             "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
index 32f9e0da8949b0c21fbf680ed4a47054525d6a67..2fb3fae49fd6cfe418c681fa5fa90aeaecbb8518 100644 (file)
         "d3-drag": "^3.0.0",
         "file-saver": "^2.0.5",
         "formik": "^2.2.9",
+        "fuzzy-search": "^3.2.1",
         "i18next": "^23.4.9",
         "i18next-browser-languagedetector": "^8.0.0",
         "laravel-echo": "^1.16.1",
index 5e120040d41cb8410acd0eb5e904e29e3b1d0d4b..abfeee4491e5aeff296ba79b52e44bbd6b0b391b 100644 (file)
@@ -164,6 +164,13 @@ const router = createBrowserRouter(
                                        '../pages/Tracker'
                                )}
                        />
+                       <Route
+                               path="zootr-mixed-pools-tracker"
+                               lazy={() => import(
+                                       /* webpackChunkName: "zootr" */
+                                       '../pages/ZootrMixedPoolsTracker'
+                               )}
+                       />
                </Route>
        )
 );
diff --git a/resources/js/components/zootr/MixedPoolsTracker.js b/resources/js/components/zootr/MixedPoolsTracker.js
new file mode 100644 (file)
index 0000000..b0a2fa9
--- /dev/null
@@ -0,0 +1,2051 @@
+import FuzzySearch from 'fuzzy-search';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const AREAS = [
+       {
+               id: 'kf',
+               bgColor: '#6aa84f',
+               fgColor: '#000000',
+               name: 'Kokiri Forest',
+               short: 'KF',
+               entrances: [
+                       {
+                               id: 'bro',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Brothers\' House',
+                               short: 'Bros',
+                       },
+                       {
+                               id: 'link',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Link\'s House',
+                               short: 'Links',
+                       },
+                       {
+                               id: 'sariah',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Sariah\'s House',
+                               short: 'Sariahs',
+                       },
+                       {
+                               id: 'twin',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Twins\' House',
+                               short: 'Twins',
+                       },
+                       {
+                               id: 'shop',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Shop',
+                               short: 'Shop',
+                       },
+                       {
+                               id: 'mido',
+                               bgColor: '#b6d7a8',
+                               fgColor: '#000000',
+                               name: 'Mido\'s House',
+                               short: 'Mido',
+                       },
+                       {
+                               id: 'deku',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'Deku Tree',
+                               short: 'Deku',
+                               spacer: true,
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+                       {
+                               id: 'lw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lost Woods',
+                               short: 'LW',
+                       },
+               ],
+       },
+       {
+               id: 'lw',
+               bgColor: '#38761d',
+               fgColor: '#000000',
+               name: 'Lost Woods',
+               short: 'LW',
+               entrances: [
+                       {
+                               id: 'gcg',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'GC Grotto',
+                               short: 'GC Grotto',
+                       },
+                       {
+                               id: 'tg',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Theater Grotto',
+                               short: 'Theater Grotto',
+                       },
+                       {
+                               id: 'sfmg',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'SFM Grotto',
+                               short: 'SFM Grotto',
+                       },
+                       {
+                               id: 'kf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Kokiri Forest',
+                               short: 'KF',
+                               spacer: true,
+                       },
+                       {
+                               id: 'gc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Goron City',
+                               short: 'GC',
+                       },
+                       {
+                               id: 'zr',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s River',
+                               short: 'ZR',
+                       },
+                       {
+                               id: 'sfm',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Sacred Forest Maedows',
+                               short: 'SFM',
+                       },
+               ],
+       },
+       {
+               id: 'sfm',
+               bgColor: '#274e13',
+               fgColor: '#000000',
+               name: 'Sacred Forest Maedows',
+               short: 'SFM',
+               entrances: [
+                       {
+                               id: 'forest',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Forest Temple',
+                               short: 'Forest Temple',
+                       },
+                       {
+                               id: 'wolf',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Wolf Grotto',
+                               short: 'Wolf Grotto',
+                       },
+                       {
+                               id: 'fairy',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Fairy Grotto',
+                               short: 'Fairy Grotto',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'lw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lost Woods',
+                               short: 'LW',
+                       },
+               ],
+       },
+       {
+               id: 'gy',
+               bgColor: '#a64d79',
+               fgColor: '#000000',
+               name: 'Graveyard',
+               short: 'Grave',
+               entrances: [
+                       {
+                               id: 'dh',
+                               bgColor: '#c27ba0',
+                               fgColor: '#000000',
+                               name: 'Dampe Hut',
+                               short: 'Dampe Hut',
+                       },
+                       {
+                               id: 'shadow',
+                               bgColor: '#c27ba0',
+                               fgColor: '#000000',
+                               name: 'Shadow Temple',
+                               short: 'Shadow',
+                       },
+                       {
+                               id: 'shield',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Shield Grave',
+                               short: 'Shield Grave',
+                       },
+                       {
+                               id: 'race',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Dampe Race',
+                               short: 'Dampe Race',
+                       },
+                       {
+                               id: 'sun',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Sun Song Grave',
+                               short: 'Sun Song Gr',
+                       },
+                       {
+                               id: 'family',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Family Tomb',
+                               short: 'Family Tomb',
+                       },
+                       {
+                               id: 'kak',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Kakariko',
+                               short: 'Kak',
+                       },
+               ],
+       },
+       {
+               id: 'kak',
+               bgColor: '#ff9900',
+               fgColor: '#000000',
+               name: 'Kakariko Village',
+               short: 'Kak',
+               entrances: [
+                       {
+                               id: 'talon',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Talon\'s House',
+                               short: 'Talons',
+                       },
+                       {
+                               id: 'skull',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Skulltula House',
+                               short: 'Skulltula',
+                       },
+                       {
+                               id: 'impaf',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Impa Front',
+                               short: 'Impa Front',
+                       },
+                       {
+                               id: 'impab',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Impa Back',
+                               short: 'Impa Back',
+                       },
+                       {
+                               id: 'shield',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Shield Shop',
+                               short: 'Shield Shop',
+                       },
+                       {
+                               id: 'potion',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Potion Shop',
+                               short: 'Potion Shop',
+                       },
+                       {
+                               id: 'back',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Shop Back',
+                               short: 'Shop Back',
+                       },
+                       {
+                               id: 'witch',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Witch',
+                               short: 'Witch',
+                       },
+                       {
+                               id: 'arch',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Archery',
+                               short: 'Archery',
+                       },
+                       {
+                               id: 'mill',
+                               bgColor: '#f9cb9c',
+                               fgColor: '#000000',
+                               name: 'Windmill',
+                               short: 'Windmill',
+                       },
+                       {
+                               id: 'botw',
+                               bgColor: '#a64d79',
+                               fgColor: '#000000',
+                               name: 'Bottom of the Well',
+                               short: 'Bottom Well',
+                               spacer: true,
+                       },
+                       {
+                               id: 'open',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Open Grotto',
+                               short: 'Open Grotto',
+                       },
+                       {
+                               id: 'redead',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Redead Grotto',
+                               short: 'Redead Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+                       {
+                               id: 'dmt',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Death Mountain Trail',
+                               short: 'DMT',
+                       },
+                       {
+                               id: 'gy',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Graveyard',
+                               short: 'Grave',
+                       },
+               ],
+       },
+       {
+               id: 'm1',
+               bgColor: '#9900ff',
+               fgColor: '#000000',
+               name: 'Market 1',
+               short: 'M1',
+               entrances: [
+                       {
+                               id: 'shield',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Shield Shop',
+                               short: 'Shield Shop',
+                       },
+                       {
+                               id: 'potion',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Potion Shop',
+                               short: 'Potion Shop',
+                       },
+                       {
+                               id: 'mask',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Mask Shop',
+                               short: 'Mask Shop',
+                       },
+                       {
+                               id: 'sling',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Slingshot/Arrow Game',
+                               short: 'Sling Game',
+                       },
+                       {
+                               id: 'chuu',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Bombchu Bowling',
+                               short: 'Bombchu',
+                       },
+                       {
+                               id: 'tcg',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Treasure Chest Game',
+                               short: 'TCG',
+                       },
+                       {
+                               id: 'alleyl',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Backalley Left',
+                               short: 'Alley L',
+                       },
+                       {
+                               id: 'alleyr',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Backalley Right',
+                               short: 'Alley R',
+                       },
+                       {
+                               id: 'tot',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Temple of Time',
+                               short: 'ToT',
+                       },
+                       {
+                               id: 'hc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Castle',
+                               short: 'HC',
+                       },
+                       {
+                               id: 'm2',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Market 2',
+                               short: 'M2',
+                       },
+               ],
+       },
+       {
+               id: 'm2',
+               bgColor: '#9900ff',
+               fgColor: '#000000',
+               name: 'Market 2',
+               short: 'M2',
+               entrances: [
+                       {
+                               id: 'bp',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Big Poe',
+                               short: 'Big Poe',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+                       {
+                               id: 'm1',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Market 1',
+                               short: 'M1',
+                       },
+               ],
+       },
+       {
+               id: 'hc',
+               bgColor: '#bf9000',
+               fgColor: '#000000',
+               name: 'Hyrule Castle',
+               short: 'HC',
+               entrances: [
+                       {
+                               id: 'hcfairy',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Hyrule Castle Fairy',
+                               short: 'HC Fairy',
+                       },
+                       {
+                               id: 'gfairy',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Ganon\'s Castle Fairy',
+                               short: 'Ganon Fairy',
+                       },
+                       {
+                               id: 'igc',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Inside Ganon\'s Castle',
+                               short: 'IGC',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'm1',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Market 1',
+                               short: 'M1',
+                       },
+               ],
+       },
+       {
+               id: 'hf',
+               bgColor: '#674ea7',
+               fgColor: '#000000',
+               name: 'Hyrule Field',
+               short: 'HF',
+               entrances: [
+                       {
+                               id: 'destiny',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Destiny Grotto',
+                               short: 'Destiny Grotto',
+                       },
+                       {
+                               id: 'tektite',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Tektite Grotto',
+                               short: 'Tektite',
+                       },
+                       {
+                               id: 'nw',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Northwest Grotto',
+                               short: 'NW Grotto',
+                       },
+                       {
+                               id: 'nk',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Grotto near Kakariko',
+                               short: 'near Kak Gro',
+                       },
+                       {
+                               id: 'se',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Southeast Grotto',
+                               short: 'SE Grotto',
+                       },
+                       {
+                               id: 'open',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Open Grotto',
+                               short: 'Open Grotto',
+                       },
+                       {
+                               id: 'sg',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'South Grotto',
+                               short: 'South Grotto',
+                       },
+                       {
+                               id: 'cow',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Cow Grotto',
+                               short: 'Cow Grotto',
+                       },
+                       {
+                               id: 'town',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Town',
+                               short: 'Town',
+                               spacer: true,
+                       },
+                       {
+                               id: 'llr',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lon Lon Ranch',
+                               short: 'LLR',
+                       },
+                       {
+                               id: 'kak',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Kakariko',
+                               short: 'Kak',
+                       },
+                       {
+                               id: 'zr',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s River',
+                               short: 'ZR',
+                       },
+                       {
+                               id: 'kf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Kokiri Forest',
+                               short: 'KF',
+                       },
+                       {
+                               id: 'lh',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lake Hylia',
+                               short: 'LH',
+                       },
+                       {
+                               id: 'gv',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Gerudo Valley',
+                               short: 'GV',
+                       },
+               ],
+       },
+       {
+               id: 'zr',
+               bgColor: '#3c78d8',
+               fgColor: '#000000',
+               name: 'Zora\'s River',
+               short: 'ZR',
+               entrances: [
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'open',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Open Grotto',
+                               short: 'Open Grotto',
+                       },
+                       {
+                               id: 'boulder',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Boulder Grotto',
+                               short: 'Boulder Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                               spacer: true,
+                       },
+                       {
+                               id: 'lw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lost Woods',
+                               short: 'LW',
+                       },
+                       {
+                               id: 'zd',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s Domain',
+                               short: 'ZD',
+                       },
+               ],
+       },
+       {
+               id: 'zd',
+               bgColor: '#3c78d8',
+               fgColor: '#000000',
+               name: 'Zora\'s Domain',
+               short: 'ZD',
+               entrances: [
+                       {
+                               id: 'shop',
+                               bgColor: '#aac2f1',
+                               fgColor: '#000000',
+                               name: 'Shop',
+                               short: 'Shop',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'zr',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s River',
+                               short: 'ZR',
+                       },
+                       {
+                               id: 'lh',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lake Hylia',
+                               short: 'LH',
+                       },
+                       {
+                               id: 'zf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s Fountain',
+                               short: 'ZF',
+                       },
+               ],
+       },
+       {
+               id: 'zf',
+               bgColor: '#3c78d8',
+               fgColor: '#000000',
+               name: 'Zora\'s Fountain',
+               short: 'ZF',
+               entrances: [
+                       {
+                               id: 'wall',
+                               bgColor: '#aac2f1',
+                               fgColor: '#000000',
+                               name: 'Fairy Wall',
+                               short: 'Fairy Wall',
+                       },
+                       {
+                               id: 'jabu',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'Jabu Jabu\'s Belly',
+                               short: 'Jabu',
+                       },
+                       {
+                               id: 'ice',
+                               bgColor: '#a64d79',
+                               fgColor: '#000000',
+                               name: 'Ice Cavern',
+                               short: 'Ice Cavern',
+                       },
+                       {
+                               id: 'zd',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s Domain',
+                               short: 'ZD',
+                       },
+               ],
+       },
+       {
+               id: 'lh',
+               bgColor: '#4a86e8',
+               fgColor: '#000000',
+               name: 'Lake Hylia',
+               short: 'LH',
+               entrances: [
+                       {
+                               id: 'dive',
+                               bgColor: '#cfe2f3',
+                               fgColor: '#000000',
+                               name: 'Lab Diving',
+                               short: 'Lab Dive',
+                       },
+                       {
+                               id: 'fishing',
+                               bgColor: '#cfe2f3',
+                               fgColor: '#000000',
+                               name: 'Fishing Game',
+                               short: 'Fishing',
+                       },
+                       {
+                               id: 'water',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Water Temple',
+                               short: 'Water Temple',
+                       },
+                       {
+                               id: 'owl',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Owl Grotto',
+                               short: 'Owl Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+                       {
+                               id: 'zd',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Zora\'s Domain',
+                               short: 'ZD',
+                       },
+               ],
+       },
+       {
+               id: 'llr',
+               bgColor: '#f1c232',
+               fgColor: '#000000',
+               name: 'Lon Lon Ranch',
+               short: 'LLR',
+               entrances: [
+                       {
+                               id: 'chicken',
+                               bgColor: '#ffd966',
+                               fgColor: '#000000',
+                               name: 'Chicken Game',
+                               short: 'Chicken',
+                       },
+                       {
+                               id: 'stable',
+                               bgColor: '#ffd966',
+                               fgColor: '#000000',
+                               name: 'Stable',
+                               short: 'Stable',
+                       },
+                       {
+                               id: 'tower',
+                               bgColor: '#ffd966',
+                               fgColor: '#000000',
+                               name: 'Tower',
+                               short: 'Tower',
+                       },
+                       {
+                               id: 'grotto',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Grotto',
+                               short: 'Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+               ],
+       },
+       {
+               id: 'gv',
+               bgColor: '#b45f06',
+               fgColor: '#000000',
+               name: 'Gerudo Valley',
+               short: 'GV',
+               entrances: [
+                       {
+                               id: 'tent',
+                               bgColor: '#b45f06',
+                               fgColor: '#000000',
+                               name: 'Tent',
+                               short: 'Tent',
+                       },
+                       {
+                               id: 'str2',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Strength 2 Grotto',
+                               short: 'Str2 Grotto',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'hf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Hyrule Field',
+                               short: 'HF',
+                       },
+                       {
+                               id: 'gf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Gerudo Fortress',
+                               short: 'GF',
+                       },
+                       {
+                               id: 'wf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Waterfall',
+                               short: 'Waterfall',
+                               oneway: true,
+                       },
+               ],
+       },
+       {
+               id: 'gf',
+               bgColor: '#b45f06',
+               fgColor: '#000000',
+               name: 'Gerudo Fortress',
+               short: 'GF',
+               entrances: [
+                       {
+                               id: 'gtg',
+                               bgColor: '#a64d79',
+                               fgColor: '#000000',
+                               name: 'Gerudo Training Grounds',
+                               short: 'GTG',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'gv',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Gerudo Valley',
+                               short: 'GV',
+                       },
+                       {
+                               id: 'hw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Haunted Wasteland',
+                               short: 'Waste',
+                       },
+               ],
+       },
+       {
+               id: 'dcol',
+               bgColor: '#f1c232',
+               fgColor: '#000000',
+               name: 'Desert Colossus',
+               short: 'DCol',
+               entrances: [
+                       {
+                               id: 'spirit',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Spirit Temple',
+                               short: 'Spirit',
+                       },
+                       {
+                               id: 'fairy',
+                               bgColor: '#ffd966',
+                               fgColor: '#000000',
+                               name: 'Fairy',
+                               short: 'Fairy',
+                       },
+                       {
+                               id: 'str2',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Strength 2 Grotto',
+                               short: 'Str2 Grotto',
+                       },
+                       {
+                               id: 'hw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Haunted Wasteland',
+                               short: 'Waste',
+                       },
+               ],
+       },
+       {
+               id: 'hw',
+               bgColor: '#f1c232',
+               fgColor: '#000000',
+               name: 'Haunted Wasteland',
+               short: 'Waste',
+               entrances: [
+                       {
+                               id: 'gf',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Gerudo Fortress',
+                               short: 'GF',
+                       },
+                       {
+                               id: 'dcol',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Desert Colossus',
+                               short: 'DCol',
+                       },
+               ],
+       },
+       {
+               id: 'dmc',
+               bgColor: '#ff0000',
+               fgColor: '#000000',
+               name: 'Death Mountain Crater',
+               short: 'DMC',
+               entrances: [
+                       {
+                               id: 'fairy',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Fairy',
+                               short: 'Fairy',
+                       },
+                       {
+                               id: 'fire',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Fire Temple',
+                               short: 'Fire',
+                       },
+                       {
+                               id: 'boulder',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Boulder Grotto',
+                               short: 'Boulder Gro',
+                       },
+                       {
+                               id: 'hammer',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Hammer Grotto',
+                               short: 'Hammer Gro',
+                       },
+                       {
+                               id: 'gc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Goron City',
+                               short: 'GC',
+                       },
+                       {
+                               id: 'dmt',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Death Mountain Trail',
+                               short: 'DMT',
+                       },
+               ],
+       },
+       {
+               id: 'dmt',
+               bgColor: '#ff0000',
+               fgColor: '#000000',
+               name: 'Death Mountain Trail',
+               short: 'DMT',
+               entrances: [
+                       {
+                               id: 'dc',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Dodongo\'s Cavern',
+                               short: 'DC',
+                       },
+                       {
+                               id: 'fairy',
+                               bgColor: '#ea9999',
+                               fgColor: '#000000',
+                               name: 'Fairy',
+                               short: 'Fairy',
+                       },
+                       {
+                               id: 'storms',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Storms Grotto',
+                               short: 'Storms Grotto',
+                       },
+                       {
+                               id: 'cow',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Cow Grotto',
+                               short: 'Cow Grotto',
+                       },
+                       {
+                               id: 'kak',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Kakariko',
+                               short: 'Kak',
+                               spacer: true,
+                       },
+                       {
+                               id: 'gc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Goron City',
+                               short: 'GC',
+                       },
+                       {
+                               id: 'dmc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Death Mountain Crater',
+                               short: 'DMC',
+                       },
+               ],
+       },
+       {
+               id: 'gc',
+               bgColor: '#ff0000',
+               fgColor: '#000000',
+               name: 'Goron City',
+               short: 'GC',
+               entrances: [
+                       {
+                               id: 'shop',
+                               bgColor: '#ea9999',
+                               fgColor: '#000000',
+                               name: 'Shop',
+                               short: 'Shop',
+                       },
+                       {
+                               id: 'times',
+                               bgColor: '#b7b7b7',
+                               fgColor: '#000000',
+                               name: 'Times Grotto',
+                               short: 'Times Grotto',
+                       },
+                       {
+                               id: 'dmt',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Death Mountain Trail',
+                               short: 'DMT',
+                       },
+                       {
+                               id: 'lw',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Lost Woods',
+                               short: 'LW',
+                       },
+                       {
+                               id: 'dmc',
+                               bgColor: '#ff6d01',
+                               fgColor: '#000000',
+                               name: 'Death Mountain Crater',
+                               short: 'DMC',
+                       },
+               ],
+       },
+       {
+               id: 'tot',
+               bgColor: '#ffffff',
+               fgColor: '#000000',
+               name: 'Temple of Time',
+               short: 'ToT',
+               entrances: [
+                       {
+                               id: 'temple',
+                               bgColor: '#b4a7d6',
+                               fgColor: '#000000',
+                               name: 'Temple of Time',
+                               short: 'Temple',
+                       },
+                       {
+                               id: 'm1',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Market 1',
+                               short: 'M1',
+                       },
+               ],
+       },
+       {
+               id: 'deku',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'Deku Tree',
+               short: 'Deku',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'gohma',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'Gohma',
+               short: 'Gohma',
+       },
+       {
+               id: 'dc',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'Dodongo\'s Cavern',
+               short: 'DC',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'kd',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'King Dodongo',
+               short: 'Dodongo',
+       },
+       {
+               id: 'jabu',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'Jabu Jabu\'s Belly',
+               short: 'Jabu',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#ead1dc',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'barinade',
+               bgColor: '#ead1dc',
+               fgColor: '#000000',
+               name: 'Barinade',
+               short: 'Barinade',
+       },
+       {
+               id: 'forest',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Forest Temple',
+               short: 'Forest',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                               spacer: true,
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                               spacer: true,
+                       },
+               ],
+       },
+       {
+               id: 'pg',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Phantom Ganon',
+               short: 'PG',
+               spacer: true,
+       },
+       {
+               id: 'fire',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Fire Temple',
+               short: 'Fire',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'volvo',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Volvagia',
+               short: 'Volvagia',
+       },
+       {
+               id: 'water',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Water Temple',
+               short: 'Water',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'morpha',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Morpha',
+               short: 'Morpha',
+       },
+       {
+               id: 'shadow',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Shadow Temple',
+               short: 'Shadow',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'bongo',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Bongo Bongo',
+               short: 'Bongo Bongo',
+       },
+       {
+               id: 'spirit',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Spirit Temple',
+               short: 'Spirit',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#d5a6bd',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                       },
+               ],
+       },
+       {
+               id: 'tr',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Twinrova',
+               short: 'Twinrova',
+       },
+       {
+               id: 'igc',
+               bgColor: '#d5a6bd',
+               fgColor: '#000000',
+               name: 'Inside Ganon\'s Castle',
+               short: 'IGC',
+               entrances: [
+                       {
+                               id: 'main',
+                               bgColor: '#a64d79',
+                               fgColor: '#000000',
+                               name: 'Main',
+                               short: 'Main',
+                               spacer: true,
+                       },
+                       {
+                               id: 'end',
+                               bgColor: '#a64d79',
+                               fgColor: '#000000',
+                               name: 'End',
+                               short: 'End',
+                               spacer: true,
+                       },
+               ],
+       },
+       {
+               id: 'ganon',
+               bgColor: '#a64d79',
+               fgColor: '#000000',
+               name: 'Ganon',
+               short: 'Ganon',
+               spacer: true,
+       },
+       {
+               id: 'botw',
+               bgColor: '#a64d79',
+               fgColor: '#000000',
+               name: 'Bottom of the Well',
+               short: 'Bottom Well',
+       },
+       {
+               id: 'ice',
+               bgColor: '#a64d79',
+               fgColor: '#000000',
+               name: 'Ice Cavern',
+               short: 'Ice Cavern',
+       },
+       {
+               id: 'gtg',
+               bgColor: '#a64d79',
+               fgColor: '#000000',
+               name: 'Gerudo Training Grounds',
+               short: 'GTG',
+       },
+];
+
+const DUNGEONS = [
+       {
+               id: 'd1',
+               bgColor: '#ff00ff',
+               fgColor: '#000000',
+               name: 'Dungeon Entrances',
+               short: 'Dungeon Entrances',
+               type: 'main',
+               entrances: [
+                       'deku.main',
+                       'dc.main',
+                       'jabu.main',
+                       'forest.main',
+                       'fire.main',
+                       'water.main',
+                       'shadow.main',
+                       'spirit.main',
+                       'igc.main',
+                       'botw',
+                       'ice',
+                       'gtg',
+               ],
+       },
+       {
+               id: 'd2',
+               bgColor: '#ff00ff',
+               fgColor: '#000000',
+               name: 'Dungeon Exits',
+               short: 'Dungeon Exits',
+               type: 'end',
+               entrances: [
+                       'deku.end',
+                       'dc.end',
+                       'jabu.end',
+                       'forest.end',
+                       'fire.end',
+                       'water.end',
+                       'shadow.end',
+                       'spirit.end',
+                       'igc.end',
+               ],
+       },
+       {
+               id: 'boss',
+               bgColor: '#ff00ff',
+               fgColor: '#000000',
+               name: 'Bosses',
+               short: 'Bosses',
+               entrances: [
+                       'gohma',
+                       'kd',
+                       'barinade',
+                       'pg',
+                       'volvo',
+                       'morpha',
+                       'bongo',
+                       'tr',
+                       'ganon',
+               ],
+       },
+];
+
+const ROOMS = [
+       {
+               id: 'trash',
+               bgColor: '#333333',
+               fgColor: '#dddddd',
+               name: 'Trash',
+               short: 'Trash',
+               multi: true,
+       },
+       {
+               id: 'zlf',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'ZL Fairy',
+               short: 'ZL Fairy',
+               multi: true,
+       },
+       {
+               id: 'toti',
+               bgColor: '#ff0000',
+               fgColor: '#000000',
+               name: 'Temple of Time',
+               short: 'Temple of Time',
+       },
+       {
+               id: 'bcb',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Bombchu Bowling',
+               short: 'Bombchu Bow',
+       },
+       {
+               id: 'fish',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Fishing Game',
+               short: 'Fishing',
+       },
+       {
+               id: 'sling',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Slingshot Game',
+               short: 'Slingshot Game',
+       },
+       {
+               id: 'arch',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Archery Game',
+               short: 'Archery Game',
+       },
+       {
+               id: 'tow',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Lon Lon Ranch Tower',
+               short: 'Ranch Tower',
+       },
+       {
+               id: 'chick',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Chicken Game',
+               short: 'Chicken Game',
+       },
+       {
+               id: 'mill',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Windmill',
+               short: 'Windmill',
+       },
+       {
+               id: 'tomb',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Family Tomb',
+               short: 'Family Tomb',
+       },
+       {
+               id: 'ssg',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Sun Song Grave',
+               short: 'Sun Song Grave',
+       },
+       {
+               id: 'mask',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Mask Shop',
+               short: 'Mask Shop',
+       },
+       {
+               id: 'poe',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Big Poe',
+               short: 'Big Poe',
+       },
+       {
+               id: 'thtr',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Mask Theater',
+               short: 'Mask Theater',
+       },
+       {
+               id: 'skull',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Skulltula House',
+               short: 'Skulltula House',
+       },
+       {
+               id: 'tcg',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Treasute Chest Game',
+               short: 'TCG',
+       },
+       {
+               id: 'lab',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Lab Diving',
+               short: 'Lab Dive',
+       },
+       {
+               id: 'tek',
+               bgColor: '#fffd00',
+               fgColor: '#000000',
+               name: 'Tektite Grotto',
+               short: 'Tektite',
+       },
+];
+
+const CONTEXT = React.createContext({});
+
+const useTracker = () => React.useContext(CONTEXT);
+
+const mapEntrance = (area, entrance) => ({
+       ...entrance,
+       id: `${area.id}.${entrance.id}`,
+       area,
+});
+
+const getArea = (id) => {
+       return AREAS.find((area) => area.id === id);
+};
+
+const getEntranceOfArea = (area, entranceId) => {
+       if (!area) return null;
+       if (!area.entrances) return null;
+       const entrance = area.entrances.find((entrance) => entrance.id === entranceId);
+       return mapEntrance(area, entrance);
+};
+
+const getEntrance = (id) => {
+       if (!id) return null;
+       const dotPos = id.indexOf('.');
+       if (dotPos === -1) {
+               return getArea(id);
+       }
+       const areaId = id.substring(0, dotPos);
+       const entranceId = id.substring(dotPos + 1);
+       const area = getArea(areaId);
+       const entrance = getEntranceOfArea(area, entranceId);
+       return entrance;
+};
+
+const getRoom = (id) => {
+       return ROOMS.find((room) => room.id === id);
+};
+
+const entranceShort = (entrance) => {
+       if (!entrance) return null;
+       return entrance.area
+               ? `${entrance.area.short} (${entrance.short})`
+               : entrance.short;
+};
+
+const entranceName = (entrance) => {
+       if (!entrance) return null;
+       return entrance.area
+               ? `[${entrance.area.short}] ${entrance.name}`
+               : entrance.name;
+};
+
+const entranceFull = (entrance) => {
+       if (!entrance) return null;
+       return entrance.area
+               ? `${entrance.area.name} - ${entrance.name}`
+               : entrance.name;
+};
+
+const entranceStyle = (entrance) => {
+       if (!entrance) return null;
+       return {
+               backgroundColor: entrance.bgColor,
+               color: entrance.fgColor,
+       };
+};
+
+const SelectBox = ({ id, name, onChange, options, value }) => {
+       const [open, setOpen] = React.useState(false);
+       const [search, setSearch] = React.useState('');
+
+       const ref = React.useRef();
+       const searchRef = React.useRef();
+
+       const valueEntrance = React.useMemo(() => getEntrance(value) || getRoom(value), [value]);
+
+       const searcher = React.useMemo(() => {
+               return new FuzzySearch(options, ['id', 'name', 'short'], { sort: true });
+       }, [options]);
+
+       const results = React.useMemo(() => {
+               return searcher.search(search);
+       }, [search, searcher]);
+
+       React.useEffect(() => {
+               const handleEventOutside = e => {
+                       if (ref.current && !ref.current.contains(e.target)) {
+                               setOpen(false);
+                       }
+               };
+               document.addEventListener('mousedown', handleEventOutside, true);
+               document.addEventListener('focus', handleEventOutside, true);
+               return () => {
+                       document.removeEventListener('mousedown', handleEventOutside, true);
+                       document.removeEventListener('focus', handleEventOutside, true);
+               };
+       }, []);
+
+       const classNames = ['entrance-select'];
+       if (open) classNames.push('is-open');
+
+       return <div className={classNames.join(' ')} ref={ref}>
+               <input
+                       className="entrance-search"
+                       id={id}
+                       onChange={({ target: { value } }) => setSearch(value)}
+                       onFocus={() => setOpen(true)}
+                       ref={searchRef}
+                       type="search"
+                       value={search}
+               />
+               <div
+                       aria-controls={`${id}.options`}
+                       aria-expanded={open ? 'true' : 'false'}
+                       aria-haspopup={`${id}.options`}
+                       className="entrance-value"
+                       onClick={() => {
+                               setOpen(true);
+                               searchRef.current.focus();
+                               searchRef.current.select();
+                       }}
+                       onContextMenu={(e) => {
+                               if (value) {
+                                       onChange({ target: { name, value: null } });
+                               } else {
+                                       onChange({ target: { name, value: 'trash' } });
+                                       setSearch('');
+                               }
+                               e.preventDefault();
+                               e.stopPropagation();
+                       }}
+                       role="combobox"
+                       style={entranceStyle(valueEntrance)}
+                       title={entranceFull(valueEntrance)}
+               >
+                       {entranceShort(valueEntrance)}
+               </div>
+               <div className="entrance-options" id={`${id}.options`} role="listbox">
+                       {results.map((entrance) =>
+                               <div
+                                       className="entrance-option"
+                                       key={entrance.id}
+                                       onClick={() => {
+                                               onChange({ target: { name, value: entrance.id } });
+                                               setOpen(false);
+                                               setSearch('');
+                                       }}
+                                       role="option"
+                                       style={entranceStyle(entrance)}
+                               >
+                                       {entranceName(entrance)}
+                               </div>
+                       )}
+               </div>
+       </div>;
+};
+
+SelectBox.propTypes = {
+       className: PropTypes.string,
+       id: PropTypes.string,
+       name: PropTypes.string,
+       onChange: PropTypes.func,
+       options: PropTypes.arrayOf(PropTypes.shape({
+               id: PropTypes.string,
+               name: PropTypes.string,
+       })),
+       value: PropTypes.string,
+};
+
+const EntranceGroup = ({ checked = 0, children, group, total = 0 }) => {
+       return <div className="entrance-group">
+               <h2 style={entranceStyle(group)}>
+                       {entranceName(group)}
+                       {checked && checked !== total ?
+                               <span className="checks">{checked}/{total}</span>
+                       : null}
+               </h2>
+               {children}
+       </div>;
+};
+
+EntranceGroup.propTypes = {
+       checked: PropTypes.number,
+       children: PropTypes.node,
+       group: PropTypes.shape({
+               bgColor: PropTypes.string,
+               fgColor: PropTypes.string,
+               name: PropTypes.string,
+       }),
+       total: PropTypes.number,
+};
+
+const EntranceRow = ({ entranceId }) => {
+       const entrance = React.useMemo(() => getEntrance(entranceId), [entranceId]);
+
+       if (!entrance) {
+               return <div className="entrance-row empty" />;
+       }
+
+       const { connections, freeEntrances, setConnection } = useTracker();
+
+       const classNames = ['entrance-row'];
+       if (entrance.spacer) classNames.push('mt-2');
+       if (connections[entrance.id] === 'trash') classNames.push('is-trash');
+
+       return <div className={classNames.join(' ')}>
+               <label
+                       className="entrance-label"
+                       htmlFor={entranceId}
+                       style={entranceStyle(entrance)}
+                       title={entranceFull(entrance)}
+               >
+                       {entranceShort(entrance)}
+               </label>
+               <SelectBox
+                       id={entranceId}
+                       name={entranceId}
+                       onChange={({ target: { name, value } }) => setConnection(name, value)}
+                       options={freeEntrances}
+                       value={connections[entranceId]}
+               />
+       </div>;
+};
+
+EntranceRow.propTypes = {
+       entranceId: PropTypes.string,
+};
+
+const MixedPoolsTracker = () => {
+       const [connections, setConnections] = React.useState({});
+
+       const setConnection = React.useCallback((src, dst) => {
+               setConnections((c) => {
+                       const newConn = { ...c };
+                       if (getEntrance(c[src])) {
+                               // unset old connection
+                               newConn[c[src]] = null;
+                       }
+                       newConn[src] = dst;
+                       if (dst && getEntrance(src)) {
+                               newConn[dst] = src;
+                       }
+                       return newConn;
+               });
+       }, []);
+
+       const entrances = React.useMemo(() => {
+               const options = [];
+               ROOMS.forEach((room) => {
+                       options.push(room);
+               });
+               AREAS.forEach((area) => {
+                       if (area.entrances) {
+                               area.entrances.forEach((entrance) => {
+                                       if (entrance.oneway) return;
+                                       options.push(getEntranceOfArea(area, entrance.id));
+                               });
+                       } else {
+                               options.push(getEntrance(area.id));
+                       }
+               });
+               return options;
+       }, []);
+
+       const freeEntrances = React.useMemo(() => {
+               return entrances.filter((entrance) => {
+                       return entrance.multi ||
+                               !Object.entries(connections).find(([, b]) => b === entrance.id);
+               });
+       }, [connections, entrances]);
+
+       const context = React.useMemo(() => ({
+               connections, entrances, freeEntrances, setConnection,
+       }), [connections, entrances, freeEntrances, setConnection]);
+
+       const superGroups = React.useMemo(() => {
+               const sg = [
+                       {
+                               key: 'one',
+                               groups: ['kf', 'lw', 'sfm', 'gy'],
+                       },
+                       {
+                               key: 'two',
+                               groups: ['kak', 'm1', 'm2', 'hc'],
+                       },
+                       {
+                               key: 'three',
+                               groups: ['hf', 'zr', 'zd', 'zf'],
+                       },
+                       {
+                               key: 'four',
+                               groups: ['lh', 'llr', 'gv', 'gf', 'dcol', 'hw'],
+                       },
+                       {
+                               key: 'five',
+                               groups: ['dmc', 'dmt', 'gc', 'tot'],
+                       },
+               ];
+               sg.forEach((superGroup) => {
+                       superGroup.groups = superGroup.groups.map((areaId) => {
+                               const area = getArea(areaId);
+                               const entranceIds = area.entrances.map((entrance) => `${area.id}.${entrance.id}`);
+                               const checked = entranceIds.filter((entranceId) =>
+                                       Object.entries(connections).find(([a, b]) => b && a === entranceId)
+                               ).length;
+                               return {
+                                       area,
+                                       entranceIds,
+                                       checked,
+                                       total: area.entrances.length,
+                               };
+                       });
+               });
+               return sg;
+       }, [connections]);
+
+       return <CONTEXT.Provider value={context}>
+               <div className="mixed-pools-tracker">
+                       <div className="columns">
+                               {superGroups.map((sg) =>
+                                       <div className="column" key={sg.key}>
+                                               {sg.groups.map(group =>
+                                                       <EntranceGroup
+                                                               checked={group.checked}
+                                                               group={group.area}
+                                                               key={group.area.id}
+                                                               total={group.total}
+                                                       >
+                                                               {group.entranceIds.map((entranceId) =>
+                                                                       <EntranceRow entranceId={entranceId} key={entranceId} />
+                                                               )}
+                                                       </EntranceGroup>
+                                               )}
+                                       </div>
+                               )}
+                       </div>
+                       <div className="columns">
+                               {DUNGEONS.map((area) =>
+                                       <div className="column" key={area.id}>
+                                               <EntranceGroup group={area}>
+                                                       {area.entrances.map((entranceId) =>
+                                                               <EntranceRow
+                                                                       entranceId={entranceId}
+                                                                       key={entranceId}
+                                                               />
+                                                       )}
+                                               </EntranceGroup>
+                                       </div>
+                               )}
+                       </div>
+               </div>
+       </CONTEXT.Provider>;
+};
+
+export default MixedPoolsTracker;
diff --git a/resources/js/pages/ZootrMixedPoolsTracker.js b/resources/js/pages/ZootrMixedPoolsTracker.js
new file mode 100644 (file)
index 0000000..100af98
--- /dev/null
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Helmet } from 'react-helmet';
+
+import ErrorBoundary from '../components/common/ErrorBoundary';
+import Tracker from '../components/zootr/MixedPoolsTracker';
+
+export const Component = () => {
+       return <ErrorBoundary>
+               <Helmet>
+                       <title>ZOOTR Mixed Pools Tracker</title>
+               </Helmet>
+               <Tracker />
+       </ErrorBoundary>;
+};
index 910d3826eb05c6188c356afe90b199ba28d222a2..910838f82aaa17aad9e697123a8628407601c31a 100644 (file)
@@ -22,3 +22,4 @@
 @import 'tournaments';
 @import 'tracker';
 @import 'users';
+@import 'zootr';
diff --git a/resources/sass/zootr.scss b/resources/sass/zootr.scss
new file mode 100644 (file)
index 0000000..e8486d4
--- /dev/null
@@ -0,0 +1,96 @@
+.mixed-pools-tracker {
+       padding: 1ex;
+       font-size: 80%;
+
+       .columns {
+               display: flex;
+               flex-wrap: wrap;
+               gap: 1em;
+       }
+       .column {
+               width: 20em;
+       }
+
+       .entrance-group {
+               margin: 1em 0;
+
+               h2 {
+                       position: relative;
+                       font-size: 110%;
+                       margin: 1ex 0;
+                       padding: .25ex;
+                       text-align: center;
+               }
+               .checks {
+                       position: absolute;
+                       top: .5ex;
+                       right: .5ex;
+                       font-weight: normal;
+                       font-size: 80%;
+               }
+       }
+       .entrance-row {
+               display: flex;
+               align-items: stretch;
+       }
+       .entrance-label {
+               margin: 0;
+               padding: 0 .25ex;
+               border: thin solid black;
+               width: 50%;
+       }
+       .entrance-select {
+               position: relative;
+               border: thin solid black;
+               width: 50%;
+       }
+       .entrance-search {
+               border: none;
+               background: #ddd;
+               width: 100%;
+       }
+       .entrance-value {
+               position: absolute;
+               top: 0;
+               right: 0;
+               bottom: 0;
+               left: 0;
+               padding: 0 .25ex;
+               cursor: pointer;
+               user-select: none;
+       }
+       .entrance-row.is-trash {
+               .entrance-label, .entrance-value {
+                       opacity: 0.05;
+               }
+               .entrance-select {
+                       border-color: rgba(0, 0, 0, 0.05);
+               }
+               .entrance-search {
+                       background: transparent;
+               }
+       }
+       .entrance-options {
+               display: none;
+               position: absolute;
+               top: 100%;
+               right: 0;
+               width: 15em;
+               box-shadow: 0 .25ex 1ex black;
+               max-height: 20em;
+               overflow-y: auto;
+       }
+       .entrance-select.is-open {
+               .entrance-value {
+                       opacity: .2;
+               }
+               .entrance-options {
+                       display: block;
+                       z-index: 1;
+               }
+       }
+       .entrance-option {
+               border: thin solid grey;
+               cursor: pointer;
+       }
+}