From 09ea05190dc554fbe1b61d1bcc25267c3926d6f9 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sun, 4 May 2025 22:21:04 +0200 Subject: [PATCH] zootr mp tracker prototype --- package-lock.json | 6 + package.json | 1 + resources/js/app/Routes.js | 7 + .../js/components/zootr/MixedPoolsTracker.js | 2051 +++++++++++++++++ resources/js/pages/ZootrMixedPoolsTracker.js | 14 + resources/sass/app.scss | 1 + resources/sass/zootr.scss | 96 + 7 files changed, 2176 insertions(+) create mode 100644 resources/js/components/zootr/MixedPoolsTracker.js create mode 100644 resources/js/pages/ZootrMixedPoolsTracker.js create mode 100644 resources/sass/zootr.scss diff --git a/package-lock.json b/package-lock.json index 0885cc4..736ff49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -8662,6 +8663,11 @@ "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", diff --git a/package.json b/package.json index 32f9e0d..2fb3fae 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,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", diff --git a/resources/js/app/Routes.js b/resources/js/app/Routes.js index 5e12004..abfeee4 100644 --- a/resources/js/app/Routes.js +++ b/resources/js/app/Routes.js @@ -164,6 +164,13 @@ const router = createBrowserRouter( '../pages/Tracker' )} /> + import( + /* webpackChunkName: "zootr" */ + '../pages/ZootrMixedPoolsTracker' + )} + /> ) ); diff --git a/resources/js/components/zootr/MixedPoolsTracker.js b/resources/js/components/zootr/MixedPoolsTracker.js new file mode 100644 index 0000000..b0a2fa9 --- /dev/null +++ b/resources/js/components/zootr/MixedPoolsTracker.js @@ -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
+ setSearch(value)} + onFocus={() => setOpen(true)} + ref={searchRef} + type="search" + value={search} + /> +
{ + 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)} +
+
+ {results.map((entrance) => +
{ + onChange({ target: { name, value: entrance.id } }); + setOpen(false); + setSearch(''); + }} + role="option" + style={entranceStyle(entrance)} + > + {entranceName(entrance)} +
+ )} +
+
; +}; + +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
+

+ {entranceName(group)} + {checked && checked !== total ? + {checked}/{total} + : null} +

+ {children} +
; +}; + +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
; + } + + 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
+ + setConnection(name, value)} + options={freeEntrances} + value={connections[entranceId]} + /> +
; +}; + +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 +
+
+ {superGroups.map((sg) => +
+ {sg.groups.map(group => + + {group.entranceIds.map((entranceId) => + + )} + + )} +
+ )} +
+
+ {DUNGEONS.map((area) => +
+ + {area.entrances.map((entranceId) => + + )} + +
+ )} +
+
+
; +}; + +export default MixedPoolsTracker; diff --git a/resources/js/pages/ZootrMixedPoolsTracker.js b/resources/js/pages/ZootrMixedPoolsTracker.js new file mode 100644 index 0000000..100af98 --- /dev/null +++ b/resources/js/pages/ZootrMixedPoolsTracker.js @@ -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 + + ZOOTR Mixed Pools Tracker + + + ; +}; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 910d382..910838f 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -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 index 0000000..e8486d4 --- /dev/null +++ b/resources/sass/zootr.scss @@ -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; + } +} -- 2.39.5