initial commit of functioning opening tracking

This commit is contained in:
Sheldan
2024-01-06 23:29:25 +01:00
parent b72c68dfe5
commit 45e7982330
176 changed files with 37635 additions and 0 deletions

0
gw2-tools-ui/src/App.css Normal file
View File

55
gw2-tools-ui/src/App.js Normal file
View File

@@ -0,0 +1,55 @@
import './App.css';
import './navbar.css';
import {Link, Outlet, Route, Routes} from 'react-router-dom';
import {AddOpening} from "./components/creation/AddOpening";
import {ViewOpenings} from "./components/overview/ViewOpenings";
import {ItemRatesOverview} from "./components/rates/ItemRatesOverview";
function Layout() {
return <ul className="App-header">
<nav className="navbar">
<div className="container">
<div className="nav-elements">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/viewOpenings">
View openings
</Link>
</li>
<li>
<Link to="/addOpening">
Add new openings
</Link>
</li>
<li>
<Link to="/dropRates">
View drop rates
</Link>
</li>
</ul>
</div>
</div>
</nav>
<Outlet/>
</ul>;
}
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Layout/>}>
<Route path="addOpening" element={<AddOpening/>}/>
<Route path="viewOpenings" element={<ViewOpenings/>}/>
<Route path="dropRates" element={<ItemRatesOverview/>}/>
</Route>
</Routes>
</div>
);
}
export default App;

View File

@@ -0,0 +1,2 @@
import { render, screen } from '@testing-library/react';
import App from './App';

View File

@@ -0,0 +1,17 @@
import {useSelector, useDispatch } from 'react-redux'
import {setTo} from "../../redux/slices/apiKey";
export function ApiKeyInput() {
const apiKey = useSelector(state => state.apiKey.value)
const loadingState = useSelector(state => state.loadingState.value)
const dispatch = useDispatch()
return (
<>
<label>
API Key: <input name="apiKey" value={apiKey} disabled={loadingState} onChange={(event) => {
dispatch(setTo(event.target.value))
}}/>
</label>
</>
);
}

View File

@@ -0,0 +1,29 @@
import styles from "../creation/InventoryDifference.module.css";
import {useSelector} from "react-redux";
export const CurrencyDisplay = ({currency}) => {
const config = useSelector(state => state.config.value)
// TODO render gold coins differently
let classNames = `${styles.imageBottom} ${styles.numberDisplay} ${currency.changed > 0 ? styles.changedDisplayPositive : styles.changedDisplayNegative}`;
function displayItem() {
if(config.showChangedOnly) {
return currency.changed !== 0
} else {
return true
}
}
let currencyDisplay = <div className={styles.imageContainer}>
<img src={currency.iconUrl} title={currency.name} alt={currency.name} className={styles.currencyDisplay}/>
<span
className={`${styles.imageCentered} ${styles.numberDisplay}`}>{currency.amount > 1 ? `${currency.amount}x` : ''}</span>
<span className={classNames}>{currency.changed !== 0 ? `${currency.changed}x` : ''}</span>
</div>;
return (
<>
{displayItem() ? currencyDisplay : ''}
</>
)
}

View File

@@ -0,0 +1,38 @@
import styles from "../creation/InventoryDifference.module.css";
import ItemDisplay from "./ItemDisplay.module.css";
import {useSelector} from "react-redux";
export const InventoryItem = ({item}) => {
const config = useSelector(state => state.config.value)
let classNames = `${styles.imageBottom} ${styles.numberDisplay} ${item.changed > 0 ? styles.changedDisplayPositive : styles.changedDisplayNegative}`;
let rarityClasses = new Map()
rarityClasses['LEGENDARY'] = ItemDisplay.legendary;
rarityClasses['ASCENDED'] = ItemDisplay.ascended;
rarityClasses['EXOTIC'] = ItemDisplay.exotic;
rarityClasses['RARE'] = ItemDisplay.rare;
rarityClasses['MASTERWORK'] = ItemDisplay.masterwork;
rarityClasses['FINE'] = ItemDisplay.fine;
rarityClasses['BASIC'] = ItemDisplay.basic;
rarityClasses['JUNK'] = ItemDisplay.junk;
let itemDisplay = <div className={styles.imageContainer}>
<img src={item.iconUrl} title={item.name} alt={item.name} className={`${styles.itemDisplay} ${rarityClasses[item.rarity]}`}/>
<span
className={`${styles.imageCentered} ${styles.numberDisplay}`}>{item.count !== 1 && item.count !== undefined ? `${item.count}x` : ''}</span>
<span className={classNames}>{item.changed !== 0 && item.changed !== undefined ? `${item.changed}x` : ''}</span>
</div>;
function displayItem() {
if(config.showChangedOnly) {
return item.changed !== 0
} else {
return true
}
}
return (
<>
{displayItem() ? itemDisplay : ''}
</>
)
}

View File

@@ -0,0 +1,31 @@
.junk {
border: 3px solid #AAAAAA;
}
.basic {
border: 3px solid #0000;
}
.fine {
border: 3px solid #62A4DA;
}
.masterwork {
border: 3px solid #1a9306;
}
.rare {
border: 3px solid #fcd00b;
}
.exotic {
border: 3px solid #ffa405;
}
.legendary {
border: 3px solid #4C139D;
}
.ascended {
border: 3px solid #fb3e8d;
}

View File

@@ -0,0 +1,18 @@
import {ApiKeyInput} from "../common/ApiKeyInput";
import {Settings} from "./Settings";
import {OpeningSubmission} from "./OpeningSubmission";
import {ItemDifference} from "./ItemDifference";
import styles from "./AddOpening.module.css";
export function AddOpening() {
return (
<>
<div className={styles.openingConfig}>
<ApiKeyInput/>
<Settings/>
<OpeningSubmission/>
<ItemDifference/>
</div>
</>
);
}

View File

@@ -0,0 +1,5 @@
.openingConfig {
display: flex;
gap: 10px;
flex-direction: column;
}

View File

@@ -0,0 +1,80 @@
import {useDispatch, useSelector} from 'react-redux'
import {InventoryItem} from "../common/InventoryItem";
import fetcher from "../../utils/fetcher";
import {
calculateBankDifferences,
parseBank,
} from "../../utils/inventoryUtils";
import {useState} from "react";
import {setAddedBankSlots, setBank, setRemovedBankSlots, updateChangedBankSlots} from "../../redux/slices/bank";
import styles from "./InventoryDifference.module.css";
export function Bank() {
const bank = useSelector(state => state.bank.value)
const config = useSelector(state => state.config.value)
const apiKey = useSelector(state => state.apiKey.value)
const loadingState = useSelector(state => state.loadingState.value)
const [bankLoading, setBankLoading] = useState(false);
const dispatch = useDispatch()
let displayAdded = bank.addedSlots.length > 0
let displayRemoved = bank.removedSlots.length > 0
const removedItems =
<>
{displayAdded ? <span className={styles.spanNewLine}>Removed items</span> : ''}
{bank.removedSlots.map((item) => <InventoryItem item={item}/>)}
</>
const addedItems =
<>
{displayRemoved ? <span className={styles.spanNewLine}>Added Items</span> : ''}
{bank.addedSlots.map((item) => <InventoryItem item={item}/>)}
</>
async function fetchBank() {
const response = await fetcher("bank", {apiKey: apiKey})
const bankResponse = await response.json()
return parseBank(bankResponse);
}
async function reloadBank() {
setBankLoading(true)
const bankSlots = await fetchBank();
if(!config.locked) {
const bankState = {
slots: bankSlots,
addedSlots: [],
removedSlots: []
}
dispatch(setBank(bankState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateBankDifferences(bank, bankSlots, config.mocking);
dispatch(setRemovedBankSlots(slotsToRemove))
dispatch(setAddedBankSlots(slotsToAdd))
dispatch(updateChangedBankSlots(slotsToUpdate))
}
setBankLoading(false)
}
let reloadButton = <button onClick={reloadBank} disabled={loadingState || bankLoading}>Update bank</button>;
return (
<>
<div>
<h2>Bank</h2>
{bank.slots.length > 0 ? reloadButton : ''}
</div>
{bank.slots.map((item) =>
<InventoryItem item={item}/>
)}
<>
{config.locked ? addedItems : ''}
</>
<>
{config.locked ? removedItems : ''}
</>
</>
);
}

View File

@@ -0,0 +1,103 @@
import {useDispatch, useSelector} from "react-redux";
import {InventoryItem} from "../common/InventoryItem";
import {
replaceCharacterItems,
setAddedInventoryItemsForCharacter, setChangedInventoryItemsForCharacter,
setRemovedInventoryItemsForCharacter
} from "../../redux/slices/inventory";
import fetcher from "../../utils/fetcher";
import {
calculateInventoryDifferencesForCharacter,
calculateWalletDifference,
parseInventory,
parseWallet
} from "../../utils/inventoryUtils";
import {useState} from "react";
import {setAddedWalletCurrency, setWallet, updateChangedWalletCurrencies} from "../../redux/slices/wallet";
export const CharacterInventory = ({character}) => {
const loadingState = useSelector(state => state.loadingState.value)
const apiKey = useSelector(state => state.apiKey.value)
const wallet = useSelector(state => state.wallet.value)
const config = useSelector(state => state.config.value)
const [characterLoading, setCharacterLoading] = useState(false);
const dispatch = useDispatch()
let displayAdded = character.addedItems.length
let addedItemsElement = <>
{displayAdded ? <h2>Added items</h2> : ''}
{character.addedItems.map((item) =>
<InventoryItem item={item}/>
)}
</>;
async function reloadWallet() {
const walletResponse = await fetcher("wallet", {apiKey: apiKey})
const fullWallet = await walletResponse.json()
const accountCurrencies = parseWallet(fullWallet);
if(!config.locked) {
const walletState = {
currencies: accountCurrencies,
addedCurrencies: []
}
dispatch(setWallet(walletState))
} else {
const [currenciesToUpdate, currenciesToAdd] = calculateWalletDifference(wallet, accountCurrencies, config.mocking);
dispatch(setAddedWalletCurrency(currenciesToAdd))
dispatch(updateChangedWalletCurrencies(currenciesToUpdate))
}
}
async function reloadCharacterInventory() {
const response = await fetcher(`inventory/${character.name}`, {apiKey: apiKey})
const characterInventoryObj = await response.json()
if(!config.locked) {
const inventory = parseInventory(characterInventoryObj);
dispatch(replaceCharacterItems(inventory))
} else {
const parsedInventory = parseInventory(characterInventoryObj);
const [
itemsToAdd,
itemsToRemove,
itemsToUpdate
] = calculateInventoryDifferencesForCharacter(character, parsedInventory, config.mocking);
let itemsToAddPayload = {
name: character.name,
addedItems: itemsToAdd
};
let itemsToRemovePayload = {
name: character.name,
removedItems: itemsToRemove
};
let itemsToUpdatePayload = {
name: character.name,
items: itemsToUpdate
};
dispatch(setAddedInventoryItemsForCharacter(itemsToAddPayload))
dispatch(setRemovedInventoryItemsForCharacter(itemsToRemovePayload))
dispatch(setChangedInventoryItemsForCharacter(itemsToUpdatePayload))
}
}
async function updateCharacter() {
setCharacterLoading(true)
await reloadCharacterInventory();
await reloadWallet();
setCharacterLoading(false)
}
return (
<>
<div>
<h1>{character.name}</h1>
<button onClick={updateCharacter} disabled={loadingState || characterLoading}>Update character</button>
</div>
{character.items.map((item) =>
<InventoryItem item={item}/>
)}
{config.locked ? addedItemsElement : ''}
</>
)
}

View File

@@ -0,0 +1,30 @@
import {useSelector } from 'react-redux'
import {CharacterInventory} from "./CharacterInventory";
export function Inventories() {
const inventory = useSelector(state => state.inventory.value)
const config = useSelector(state => state.config.value)
function shouldShowCharacter(characterInventory) {
if(!config.showChangedOnly) { //
return true;
}
// quick exits
if(characterInventory.addedItems.length > 0) {
return true;
}
if(characterInventory.removedItems.length > 0) {
return true;
}
return characterInventory.items.filter(item => item.changed !== 0).length > 0;
}
return (
<>
<h2>Inventories</h2>
{inventory.map(characterInventory =>
shouldShowCharacter(characterInventory) ? <CharacterInventory character={characterInventory} /> : ''
)}
</>
);
}

View File

@@ -0,0 +1,48 @@
.itemDisplay {
width: 33px;
height: 33px;
}
.currencyDisplay {
width: 36px;
height: 36px;
}
.imageContainer {
position: relative;
text-align: center;
display: inline-block;
color: black;
text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff;
}
.imageCentered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.imageBottom {
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -100%);
}
.numberDisplay {
font-size: x-small;
}
.changedDisplayNegative {
color: #9C1A1C;
}
.changedDisplayPositive {
color: #3A7734;
}
.spanNewLine {
display: block;
}

View File

@@ -0,0 +1,213 @@
import {useDispatch, useSelector} from "react-redux";
import fetcher from "../../utils/fetcher";
import {setLoading} from "../../redux/slices/loadingState";
import {
setAddedInventoryItemsForCharacter,
setChangedInventoryItemsForCharacter,
setInventoryForCharacter,
setRemovedInventoryItemsForCharacter
} from "../../redux/slices/inventory";
import {setAddedWalletCurrency, setWallet, updateChangedWalletCurrencies} from "../../redux/slices/wallet";
import {setAddedBankSlots, setBank, setRemovedBankSlots, updateChangedBankSlots} from "../../redux/slices/bank";
import {
calculateBankDifferences,
calculateInventoryDifferencesForCharacter,
calculateMaterialsDifference, calculateSharedInventoryDifferences,
calculateWalletDifference,
parseBank,
parseInventory,
parseMaterials, parseSharedInventory,
parseWallet
} from "../../utils/inventoryUtils";
import {Bank} from "./Bank";
import {Wallet} from "./Wallet";
import {Inventories} from "./Inventories";
import {
setAddedMaterialSlots,
setMaterials,
setRemovedMaterialSlots,
updateChangedMaterialSlots
} from "../../redux/slices/materials";
import {Materials} from "./Materials";
import {SharedInventory} from "./SharedInventory";
import {
setAddedSharedInventorySlots,
setRemovedSharedInventorySlots,
setSharedInventory, updateChangedSharedInventorySlots
} from "../../redux/slices/sharedInventory";
export function ItemDifference() {
const apiKey = useSelector(state => state.apiKey.value)
const loadingState = useSelector(state => state.loadingState.value)
const inventory = useSelector(state => state.inventory.value)
const sharedInventory = useSelector(state => state.sharedInventory.value)
const wallet = useSelector(state => state.wallet.value)
const bank = useSelector(state => state.bank.value)
const materials = useSelector(state => state.materials.value)
const config = useSelector(state => state.config.value)
const dispatch = useDispatch()
async function fetchBank() {
const response = await fetcher("bank", {apiKey: apiKey})
const bank = await response.json()
return parseBank(bank);
}
async function updateBank() {
const bankSlots = await fetchBank();
if(!config.locked) {
const bankState = {
slots: bankSlots,
addedSlots: [],
removedSlots: []
}
dispatch(setBank(bankState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateBankDifferences(bank, bankSlots, config.mocking);
dispatch(setRemovedBankSlots(slotsToRemove))
dispatch(setAddedBankSlots(slotsToAdd))
dispatch(updateChangedBankSlots(slotsToUpdate))
}
}
async function fetchMaterials() {
const response = await fetcher("materials", {apiKey: apiKey})
const materials = await response.json()
return parseMaterials(materials);
}
async function updateMaterials() {
const materialSlots = await fetchMaterials();
if(!config.locked) {
const materialsState = {
slots: materialSlots,
addedSlots: [],
removedSlots: []
}
dispatch(setMaterials(materialsState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateMaterialsDifference(materials, materialSlots, config.mocking);
dispatch(setRemovedMaterialSlots(slotsToRemove))
dispatch(setAddedMaterialSlots(slotsToAdd))
dispatch(updateChangedMaterialSlots(slotsToUpdate))
}
}
async function fetchSharedInventory() {
const response = await fetcher("sharedInventory", {apiKey: apiKey})
const sharedInventoryResponse = await response.json()
return parseSharedInventory(sharedInventoryResponse);
}
async function updatedSharedInventory() {
const sharedInventoryResponse = await fetchSharedInventory();
if(!config.locked) {
const sharedInventoryState = {
slots: sharedInventoryResponse,
addedSlots: [],
removedSlots: []
}
dispatch(setSharedInventory(sharedInventoryState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateSharedInventoryDifferences(sharedInventory, sharedInventoryResponse, config.mocking);
dispatch(setRemovedSharedInventorySlots(slotsToRemove))
dispatch(setAddedSharedInventorySlots(slotsToAdd))
dispatch(updateChangedSharedInventorySlots(slotsToUpdate))
}
}
async function fetchInformation() {
dispatch(setLoading(true))
await updateWallet()
await updateBank()
await updateMaterials()
await updatedSharedInventory()
await updateInventoryCharacterSpecific()
dispatch(setLoading(false))
}
async function fetchWallet() {
const response = await fetcher("wallet", {apiKey: apiKey})
const fullWallet = await response.json()
return parseWallet(fullWallet);
}
async function updateWallet() {
const accountCurrencies = await fetchWallet();
if(!config.locked) {
const walletState = {
currencies: accountCurrencies,
addedCurrencies: []
}
dispatch(setWallet(walletState))
} else {
const [currenciesToUpdate, currenciesToAdd] = calculateWalletDifference(wallet, accountCurrencies, config.mocking);
dispatch(setAddedWalletCurrency(currenciesToAdd))
dispatch(updateChangedWalletCurrencies(currenciesToUpdate))
}
}
async function updateInventoryCharacterSpecific() {
const response = await fetcher("characters", {apiKey: apiKey})
const characters = await response.json()
if(!config.locked) {
for (const charName of characters) {
const parsedInventory = await fetchInventoryForCharacter(charName)
dispatch(setInventoryForCharacter(parsedInventory))
}
} else {
for (const charName of characters) {
const parsedInventory = await fetchInventoryForCharacter(charName)
const existingInventory = inventory.find((characterInventory => characterInventory.name === charName))
const [
itemsToAdd,
itemsToRemove,
itemsToUpdate
] = calculateInventoryDifferencesForCharacter(existingInventory, parsedInventory, config.mocking);
let itemsToAddPayload = {
name: charName,
addedItems: itemsToAdd
};
let itemsToRemovePayload = {
name: charName,
removedItems: itemsToRemove
};
let itemsToUpdatePayload = {
name: charName,
items: itemsToUpdate
};
dispatch(setAddedInventoryItemsForCharacter(itemsToAddPayload))
dispatch(setRemovedInventoryItemsForCharacter(itemsToRemovePayload))
dispatch(setChangedInventoryItemsForCharacter(itemsToUpdatePayload))
}
}
}
async function fetchInventoryForCharacter(characterName) {
const response = await fetcher(`inventory/${characterName}/`, {apiKey: apiKey})
const inventoryResponse = await response.json()
return parseInventory(inventoryResponse);
}
function noApiKeyProvided() {
return apiKey === undefined || apiKey.length === 0
}
return (
<>
<div>
<button onClick={fetchInformation} disabled={loadingState || noApiKeyProvided()}>Load information</button>
<Wallet/>
<Bank/>
<Materials/>
<SharedInventory/>
<Inventories/>
</div>
</>
);
}

View File

@@ -0,0 +1,19 @@
import {useSelector, useDispatch } from 'react-redux'
import {toggleLocked} from "../../redux/slices/config";
export function LockStateInput() {
const loadingState = useSelector(state => state.loadingState.value)
const config = useSelector(state => state.config.value)
const dispatch = useDispatch()
function toggleState() {
dispatch(toggleLocked())
}
return (
<>
<label>
Locked
<input type={"checkbox"} checked={config.locked} disabled={loadingState} onChange={toggleState}/>
</label>
</>
);
}

View File

@@ -0,0 +1,87 @@
import {useDispatch, useSelector} from 'react-redux'
import {InventoryItem} from "../common/InventoryItem";
import fetcher from "../../utils/fetcher";
import {calculateMaterialsDifference, parseMaterials} from "../../utils/inventoryUtils";
import {useState} from "react";
import {
setAddedMaterialSlots,
setMaterials,
setRemovedMaterialSlots,
updateChangedMaterialSlots
} from "../../redux/slices/materials";
import styles from "./InventoryDifference.module.css";
export function Materials() {
const materials = useSelector(state => state.materials.value)
const config = useSelector(state => state.config.value)
const apiKey = useSelector(state => state.apiKey.value)
const loadingState = useSelector(state => state.loadingState.value)
const [materialsLoading, setMaterialsLoading] = useState(false);
const dispatch = useDispatch()
let displayAdded = materials.addedSlots.length
let displayRemoved = materials.removedSlots.length
const removedItems =
<>
{displayAdded ? <span className={styles.spanNewLine}>Removed items</span> : ''}
{materials.removedSlots.map((item) =>
<InventoryItem item={item}/>
)}
</>
const addedItems =
<>
{displayRemoved ? <span className={styles.spanNewLine}>Added Items</span> : ''}
{materials.addedSlots.map((item) =>
<InventoryItem item={item}/>
)}
</>
async function fetchMaterials() {
const response = await fetcher("materials", {apiKey: apiKey})
const materials = await response.json()
return parseMaterials(materials);
}
async function reloadMaterials() {
setMaterialsLoading(true)
const materialSlots = await fetchMaterials();
if(!config.locked) {
const materialsState = {
slots: materialSlots,
addedSlots: [],
removedSlots: []
}
dispatch(setMaterials(materialsState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateMaterialsDifference(materials, materialSlots, config.mocking);
dispatch(setRemovedMaterialSlots(slotsToRemove))
dispatch(setAddedMaterialSlots(slotsToAdd))
dispatch(updateChangedMaterialSlots(slotsToUpdate))
}
setMaterialsLoading(false)
}
let reloadButton = <button onClick={reloadMaterials} disabled={loadingState || materialsLoading}>Update materials</button>;
return (
<>
<div>
<h2>Materials</h2>
{materials.slots.length > 0 ? reloadButton : ''}
</div>
{materials.slots.map((item) =>
<InventoryItem item={item}/>
)}
<>
{config.locked ? addedItems : ''}
</>
<>
{config.locked ? removedItems : ''}
</>
</>
);
}

View File

@@ -0,0 +1,137 @@
import {useSelector, useDispatch } from 'react-redux'
import fetcher from "../../utils/fetcher";
import {setLoading} from "../../redux/slices/loadingState";
import {useState} from "react";
import toast from 'react-simple-toasts';
import 'react-simple-toasts/dist/theme/dark.css';
export function OpeningSubmission() {
const apiKey = useSelector(state => state.apiKey.value)
const inventory = useSelector(state => state.inventory.value)
const sharedInventory = useSelector(state => state.sharedInventory.value)
const wallet = useSelector(state => state.wallet.value)
const bank = useSelector(state => state.bank.value)
const materials = useSelector(state => state.materials.value)
const loadingState = useSelector(state => state.loadingState.value)
const dispatch = useDispatch()
const [openingDescription, setOpeningDescription] = useState("");
function createItem(item) {
return {
itemId: item.id,
change: item.changed,
itemType: "ITEM"
};
}
function createCurrency(currency) {
return {
itemId: currency.id,
change: currency.changed,
itemType: "CURRENCY"
};
}
function getChanges() {
const changes = []
const currencyChanges = []
for (const charName in inventory) {
const charInventory = inventory[charName]
if (charInventory.addedItems) {
charInventory.addedItems.forEach((item) => {
changes.push(createItem(item))
})
}
if (charInventory.items) {
charInventory.items.forEach((item) => {
if (item.changed !== 0) {
changes.push(createItem(item))
}
})
}
}
sharedInventory.slots.forEach((slot => {
if (slot.changed !== 0) {
changes.push(createItem(slot))
}
}))
sharedInventory.addedSlots.forEach((slot => {
changes.push(createItem(slot))
}))
wallet.currencies.forEach((slot => {
if (slot.changed !== 0) {
currencyChanges.push(createCurrency(slot))
}
}))
wallet.addedCurrencies.forEach((slot => {
currencyChanges.push(createCurrency(slot))
}))
bank.slots.forEach((slot => {
if (slot.changed !== 0) {
changes.push(createItem(slot))
}
}))
bank.addedSlots.forEach((slot => {
changes.push(createItem(slot))
}))
materials.slots.forEach((slot => {
if (slot.changed !== 0) {
changes.push(createItem(slot))
}
}))
materials.addedSlots.forEach((slot => {
changes.push(createItem(slot))
}))
const changesMap = new Map()
// this is done, so that changes over different area cancel one another out
changes.forEach((change => {
if(changesMap.has(change.itemId)) {
changesMap.get(change.itemId).change += change.change;
} else {
changesMap.set(change.itemId, change)
}
}))
const finalChangesList = []
changesMap.forEach((value) => {
if(value.change !== 0) {
finalChangesList.push(value)
}
})
currencyChanges.forEach((currencyChange) => {
finalChangesList.push(currencyChange)
})
return finalChangesList;
}
async function submitOpening() {
dispatch(setLoading(true))
const submissionBody = {}
submissionBody.items = getChanges();
submissionBody.description = openingDescription;
await fetcher("openings", {apiKey: apiKey, method: "POST", body: JSON.stringify(submissionBody), headers: {"Content-Type": "application/json"}})
setOpeningDescription("")
toast('Opening has been submitted.')
dispatch(setLoading(false))
}
function hasChanges() {
return getChanges().length > 0
}
function updateDescription(newDescription) {
setOpeningDescription(newDescription)
}
return (
<>
<div>
<label>Description<textarea name="description" maxLength={1024} value={openingDescription} onChange={e => updateDescription(e.target.value)}/></label>
<label>
<button onClick={submitOpening} disabled={loadingState || !hasChanges()}>Submit opening</button>
</label>
</div>
</>
);
}

View File

@@ -0,0 +1,32 @@
import {useSelector, useDispatch } from 'react-redux'
import {toggleMocking, toggleShowChangedOnly} from "../../redux/slices/config";
import {LockStateInput} from "./LockStateInput";
export function Settings() {
const config = useSelector(state => state.config.value)
const dispatch = useDispatch()
function handleMocking() {
dispatch(toggleMocking());
}
function handleToggleChangedOnly() {
dispatch(toggleShowChangedOnly())
}
return (
<>
<div>
<LockStateInput />
<label>
Mocking
<input type={"checkbox"} checked={config.mocking} onChange={handleMocking}/>
</label>
<label>
Display changed only
<input type={"checkbox"} checked={config.showChangedOnly} onChange={handleToggleChangedOnly}/>
</label>
</div>
</>
);
}

View File

@@ -0,0 +1,86 @@
import {useDispatch, useSelector} from 'react-redux'
import {InventoryItem} from "../common/InventoryItem";
import fetcher from "../../utils/fetcher";
import {
calculateSharedInventoryDifferences,
parseSharedInventory
} from "../../utils/inventoryUtils";
import {useState} from "react";
import styles from "./InventoryDifference.module.css";
import {
setAddedSharedInventorySlots,
setRemovedSharedInventorySlots,
setSharedInventory, updateChangedSharedInventorySlots
} from "../../redux/slices/sharedInventory";
export function SharedInventory() {
const sharedInventory = useSelector(state => state.sharedInventory.value)
const config = useSelector(state => state.config.value)
const apiKey = useSelector(state => state.apiKey.value)
const loadingState = useSelector(state => state.loadingState.value)
const [sharedInventoryLoading, setSharedInventoryLoading] = useState(false);
const dispatch = useDispatch()
let displayAdded = sharedInventory.addedSlots.length > 0
let displayRemoved = sharedInventory.removedSlots.length > 0
const removedItems =
<>
{displayAdded ? <span className={styles.spanNewLine}>Removed items</span> : ''}
{sharedInventory.removedSlots.map((item) => <InventoryItem item={item}/>)}
</>
const addedItems =
<>
{displayRemoved ? <span className={styles.spanNewLine}>Added Items</span> : ''}
{sharedInventory.addedSlots.map((item) => <InventoryItem item={item}/>)}
</>
async function fetchSharedInventory() {
const response = await fetcher("sharedInventory", {apiKey: apiKey})
const sharedInventoryResponse = await response.json()
return parseSharedInventory(sharedInventoryResponse);
}
async function reloadSharedInventory() {
setSharedInventoryLoading(true)
const sharedInventoryValue = await fetchSharedInventory();
if(!config.locked) {
const sharedInventoryState = {
slots: sharedInventoryValue,
addedSlots: [],
removedSlots: []
}
dispatch(setSharedInventory(sharedInventoryState))
} else {
const [slotsToAdd, slotsToRemove, slotsToUpdate] = calculateSharedInventoryDifferences(sharedInventory, sharedInventoryValue, config.mocking);
dispatch(setRemovedSharedInventorySlots(slotsToRemove))
dispatch(setAddedSharedInventorySlots(slotsToAdd))
dispatch(updateChangedSharedInventorySlots(slotsToUpdate))
}
setSharedInventoryLoading(false)
}
let reloadButton = <button onClick={reloadSharedInventory} disabled={loadingState || sharedInventoryLoading}>Update shared inventory</button>;
return (
<>
<div>
<h2>Shared inventory</h2>
{sharedInventory.slots.length > 0 ? reloadButton : ''}
</div>
{sharedInventory.slots.map((item) =>
<InventoryItem item={item}/>
)}
<>
{config.locked ? addedItems : ''}
</>
<>
{config.locked ? removedItems : ''}
</>
</>
);
}

View File

@@ -0,0 +1,13 @@
import {useSelector } from 'react-redux'
import {CurrencyDisplay} from "../common/CurrencyDisplay";
export function Wallet() {
const wallet = useSelector(state => state.wallet.value)
return (
<>
<h2>Wallet</h2>
{wallet.currencies.map(currency => <CurrencyDisplay currency={currency}/>)}
{wallet.addedCurrencies.map(currency => <CurrencyDisplay currency={currency}/>)}
</>
);
}

View File

@@ -0,0 +1,49 @@
import {InventoryItem} from "../common/InventoryItem";
import styles from "./Openingdisplay.module.css";
import {CurrencyDisplay} from "../common/CurrencyDisplay";
import fetcher from "../../utils/fetcher";
import {useDispatch, useSelector} from "react-redux";
import {removeOpening} from "../../redux/slices/openings";
import toast from "react-simple-toasts";
export function OpeningDisplay({opening, onlyMine}) {
const apiKey = useSelector(state => state.apiKey.value)
const dispatch = useDispatch()
let increasedItems = <>
{opening.increasedItems.map((item) => <InventoryItem item={item}/>)}
</>;
let reducedItems = <>
{opening.reducedItems.map((item) => <InventoryItem item={item}/>)}
</>;
let increasedCurrencies = <>
{opening.increasedCurrencies.map((currency) => <CurrencyDisplay currency={currency}/>)}
</>;
let reducedCurrencies = <>
{opening.reducedCurrencies.map((currency) => <CurrencyDisplay currency={currency}/>)}
</>;
async function deleteOpeningClick() {
await fetcher(`openings/${opening.id}`, {apiKey: apiKey, method: "DELETE"})
dispatch(removeOpening({id: opening.id}))
toast('Opening has been deleted.')
}
let deleteOpeningButton = <button onClick={deleteOpeningClick}>Delete opening</button>;
return (
<>
Description: {opening.description !== '' && opening.description !== undefined ? opening.description : 'No description'}
: {opening.id} opened on: {opening.openingDate} {onlyMine && apiKey !== '' ? deleteOpeningButton : ''}
<div className={styles.container}>
<div className={styles.result}>
{reducedItems}
{reducedCurrencies}
</div>
<div className={styles.opened}>
{increasedItems}
{increasedCurrencies}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,101 @@
import {useDispatch, useSelector} from 'react-redux'
import fetcher from "../../utils/fetcher";
import {useState} from "react";
import {OpeningDisplay} from "./OpeningDisplay";
import {setOpenings} from "../../redux/slices/openings";
export function OpeningOverview() {
const apiKey = useSelector(state => state.apiKey.value)
const openings = useSelector(state => state.openings.value)
const [onlyMine, setOnlyMine] = useState(true);
const [openingsLoading, setOpeningsLoading] = useState(false);
const dispatch = useDispatch()
async function fetchOpenings() {
const onlyMineParameter = onlyMine ? '?showOwnOnly=true' : ''
const response = await fetcher(`openings${onlyMineParameter}`, {apiKey: apiKey})
const openingsObj = await response.json()
return parseOpenings(openingsObj);
}
function parseOpenings(openingsObj) {
const openings = []
openingsObj.openings.forEach((opening) => {
const parsedOpening = {}
const changedItems = []
const increasedItems = []
const reducedItems = []
opening.itemChanges.forEach((changedItem) => {
const newItem = {
id: changedItem.id,
changed: changedItem.count,
name: changedItem.name,
iconUrl: changedItem.iconUrl,
rarity: changedItem.rarity
}
changedItems.push(newItem)
if(newItem.changed > 0) {
increasedItems.push(newItem)
} else {
reducedItems.push(newItem)
}
})
const changedCurrencies = []
const increasedCurrencies = []
const reducedCurrencies = []
opening.currencyChanges.forEach((changedCurrency) => {
const newCurrency = {
id: changedCurrency.id,
name: changedCurrency.name,
changed: changedCurrency.amount,
iconUrl: changedCurrency.iconUrl
}
changedCurrencies.push(newCurrency)
if(newCurrency.changed > 0) {
increasedCurrencies.push(newCurrency)
} else {
reducedCurrencies.push(newCurrency)
}
})
parsedOpening.items = changedItems;
parsedOpening.currencies = changedCurrencies;
parsedOpening.increasedItems = increasedItems;
parsedOpening.increasedCurrencies = increasedCurrencies;
parsedOpening.reducedCurrencies = reducedCurrencies;
parsedOpening.reducedItems = reducedItems;
parsedOpening.openingDate = opening.openingDate;
parsedOpening.description = opening.description;
parsedOpening.id = opening.openingId;
openings.push(parsedOpening)
})
return openings;
}
async function loadOpenings() {
setOpeningsLoading(true)
const openings = await fetchOpenings();
dispatch(setOpenings(openings))
setOpeningsLoading(false)
}
function toggleShowMine() {
setOnlyMine(!onlyMine)
}
let mineFilter = <label>
Display only mine
<input type={"checkbox"} checked={onlyMine} onChange={toggleShowMine}/>
</label>;
return (
<>
{apiKey !== '' ? mineFilter : ''}
<label>
<button onClick={loadOpenings} disabled={openingsLoading}>Load openings</button>
</label>
<h2>Openings</h2>
{openings.map(opening =>
<OpeningDisplay opening={opening} onlyMine={onlyMine}/>
)}
</>
);
}

View File

@@ -0,0 +1,15 @@
.container {
display: flex;
}
.result {
width: 25%;
background: lightblue;
/* Just so it's visible */
}
.opened {
flex: 1;
/* Grow to rest of container */
background: lightgreen;
/* Just so it's visible */
}

View File

@@ -0,0 +1,9 @@
import {OpeningOverview} from "./OpeningOverview";
export function ViewOpenings() {
return (
<>
<OpeningOverview/>
</>
);
}

View File

@@ -0,0 +1,14 @@
.itemRateContainer {
display: flex;
justify-content: space-around;
}
.openedItem {
flex: 1;
background-color: palevioletred;
}
.resultingItems {
width: 90%;
background-color: darkseagreen;
}

View File

@@ -0,0 +1,25 @@
import styles from "../creation/InventoryDifference.module.css";
export const ItemRateCurrencyDisplay = ({currency, showRelative}) => {
function getChangedValue() {
if(showRelative) {
return currency.per
} else {
return currency.amount
}
}
let currencyDisplay = <div className={styles.imageContainer}>
<img src={currency.iconUrl} title={currency.name} alt={currency.name} className={styles.currencyDisplay}/>
<span
className={`${styles.imageCentered} ${styles.numberDisplay} ${styles.changedDisplayPositive}`}>{currency.per !== 0 && currency.per !== undefined
&& currency.amount !== 0 && currency.amount !== undefined ? `${getChangedValue()}x` : ''}</span>
</div>;
return (
<>
{currencyDisplay}
</>
)
}

View File

@@ -0,0 +1,28 @@
import {InventoryItem} from "../common/InventoryItem";
import styles from "./DropRates.module.css";
import {ItemRateItemDisplay} from "./ItemRateItemDisplay";
import {ItemRateCurrencyDisplay} from "./ItemRateCurrencyDisplay";
export function ItemRateDisplay({item, showRelative}) {
let receivedItems = <>
{item.receivedItems.map((receivedItem) => <ItemRateItemDisplay item={receivedItem} showRelative={showRelative}/>)}
</>;
let receivedCurrencies = <>
{item.receivedCurrencies.map((receivedCurrency) => <ItemRateCurrencyDisplay currency={receivedCurrency} showRelative={showRelative}/>)}
</>;
return (
<>
<div className={styles.itemRateContainer}>
<div className={styles.openedItem}>
<InventoryItem item={item.item}/>
</div>
<div className={styles.resultingItems}>
{receivedItems} {receivedCurrencies}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,37 @@
import styles from "../creation/InventoryDifference.module.css";
import ItemDisplay from "../common/ItemDisplay.module.css";
export const ItemRateItemDisplay = ({item, showRelative}) => {
let rarityClasses = new Map()
rarityClasses['LEGENDARY'] = ItemDisplay.legendary;
rarityClasses['ASCENDED'] = ItemDisplay.ascended;
rarityClasses['EXOTIC'] = ItemDisplay.exotic;
rarityClasses['RARE'] = ItemDisplay.rare;
rarityClasses['MASTERWORK'] = ItemDisplay.masterwork;
rarityClasses['FINE'] = ItemDisplay.fine;
rarityClasses['BASIC'] = ItemDisplay.basic;
rarityClasses['JUNK'] = ItemDisplay.junk;
function getChangedValue() {
if(showRelative) {
return item.per
} else {
return item.count
}
}
let itemDisplay = <div className={styles.imageContainer}>
<img src={item.iconUrl} title={item.name} alt={item.name}
className={`${styles.itemDisplay} ${rarityClasses[item.rarity]}`}/>
<span
className={`${styles.imageCentered} ${styles.numberDisplay} ${styles.changedDisplayPositive}`}>{item.per !== 0 && item.per !== undefined
&& item.count !== 0 && item.count !== undefined ? `${getChangedValue()}x` : ''}</span>
</div>;
return (
<>
{itemDisplay}
</>
)
}

View File

@@ -0,0 +1,95 @@
import fetcher from "../../utils/fetcher";
import {useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {setItemRates} from "../../redux/slices/itemRates";
import {ItemRateDisplay} from "./ItemRateDisplay";
export function ItemRates() {
const [itemRatesLoading, setItemRatesLoading] = useState(false);
const [relativeRate, setRelativeRate] = useState(false);
const itemRates = useSelector(state => state.itemRates.value)
const apiKey = useSelector(state => state.apiKey.value)
const dispatch = useDispatch()
function parseItemRates(itemRatesObj) {
const itemRates = []
itemRatesObj.itemRates.forEach((itemRate) => {
const item = itemRate.item;
const newItem = {
id: item.id,
changed: item.count,
name: item.name,
iconUrl: item.iconUrl,
rarity: item.rarity
}
const receivedItems = []
itemRate.receivedItems.forEach((receivedItem) => {
const newReceivedItem = {
id: receivedItem.id,
count: receivedItem.count, // we use count for display
name: receivedItem.name,
iconUrl: receivedItem.iconUrl,
rarity: receivedItem.rarity,
per: +(receivedItem.count / Math.abs(item.count)).toFixed(2)
}
receivedItems.push(newReceivedItem)
})
const receivedCurrencies = []
itemRate.receivedCurrencies.forEach((receivedCurrency) => {
const newReceivedCurrency = {
id: receivedCurrency.id,
amount: receivedCurrency.amount, // we use count for display
name: receivedCurrency.name,
iconUrl: receivedCurrency.iconUrl,
rarity: receivedCurrency.rarity,
per: +(receivedCurrency.amount / Math.abs(item.count)).toFixed(2)
}
receivedCurrencies.push(newReceivedCurrency)
})
const itemRateObj = {
item: newItem,
receivedItems: receivedItems,
receivedCurrencies: receivedCurrencies
}
itemRates.push(itemRateObj)
})
return itemRates;
}
async function fetchItemRates() {
const response = await fetcher(`itemRates`, {apiKey: apiKey})
const itemRatesObj = await response.json()
return parseItemRates(itemRatesObj);
}
async function loadItemRates() {
setItemRatesLoading(true)
const fetchedItemRates = await fetchItemRates()
dispatch(setItemRates(fetchedItemRates))
setItemRatesLoading(false)
}
function toggleRelativeRate() {
setRelativeRate(!relativeRate)
}
return (
<>
<label>
<button onClick={loadItemRates} disabled={itemRatesLoading}>Load item rates</button>
<label>
Show relative rate
<input type={"checkbox"} checked={relativeRate} onChange={toggleRelativeRate}/>
</label>
</label>
<h2>Item rates</h2>
{itemRates.map(item =>
<ItemRateDisplay item={item} showRelative={relativeRate}/>
)}
</>
);
}

View File

@@ -0,0 +1,9 @@
import {ItemRates} from "./ItemRates";
export function ItemRatesOverview() {
return (
<>
<ItemRates/>
</>
);
}

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

26
gw2-tools-ui/src/index.js Normal file
View File

@@ -0,0 +1,26 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux'
import { store } from './redux/store'
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from "react-router-dom";
import { toastConfig } from 'react-simple-toasts';
toastConfig({ theme: 'dark' });
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter basename={process.env.PUBLIC_URL}>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,53 @@
/* taken from https://www.codevertiser.com/reactjs-responsive-navbar/ */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 15px;
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.navbar {
height: 60px;
background-color: #fef7e5;
position: relative;
}
.nav-elements {
}
.nav-elements ul {
display: flex;
justify-content: space-between;
list-style-type: none;
}
.nav-elements ul li:not(:last-child) {
margin-right: 60px;
}
.nav-elements ul a {
font-size: 16px;
font-weight: 400;
color: #2f234f;
text-decoration: none;
}
.nav-elements ul a.active {
color: #574c4c;
font-weight: 500;
position: relative;
}
.nav-elements ul a.active::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
width: 100%;
height: 2px;
background-color: #574c4c;
}

View File

@@ -0,0 +1,10 @@
import { createListenerMiddleware, isAnyOf } from "@reduxjs/toolkit";
import {setTo} from "./slices/apiKey";
export const savingApiKeyMiddleware = createListenerMiddleware();
savingApiKeyMiddleware.startListening({
matcher: isAnyOf(setTo),
effect: (action, listenerApi) => {
localStorage.setItem("apiKey", JSON.stringify(listenerApi.getState().apiKey.value));
}
});

View File

@@ -0,0 +1,17 @@
import { createSlice } from '@reduxjs/toolkit'
export const apiKeySlice = createSlice({
name: 'apiKey',
initialState: {
value: ''
},
reducers: {
setTo: (state, action) => {
state.value = action.payload
}
}
})
export const { setTo } = apiKeySlice.actions
export default apiKeySlice.reducer

View File

@@ -0,0 +1,30 @@
import { createSlice } from '@reduxjs/toolkit'
export const bankSlice = createSlice({
name: 'bank',
initialState: {
value: {slots: [], addedSlots: [], removedSlots: []}
},
reducers: {
setBank: (state, action) => {
state.value = action.payload
},
setAddedBankSlots: (state, action) => {
state.value.addedSlots = action.payload
},
updateChangedBankSlots: (state, action) => {
let slotsToChange = action.payload;
Object.keys(slotsToChange).forEach(slotId => {
const existingSlot = state.value.slots.find(slot => slot.id === parseInt(slotId))
existingSlot.changed = slotsToChange[slotId]
})
},
setRemovedBankSlots: (state, action) => {
state.value.removedSlots = action.payload
}
}
})
export const { setBank, setAddedBankSlots, updateChangedBankSlots, setRemovedBankSlots } = bankSlice.actions
export default bankSlice.reducer

View File

@@ -0,0 +1,55 @@
import { createSlice } from '@reduxjs/toolkit'
export const configSlice = createSlice({
name: 'config',
initialState: {
value: {
showChangedOnly: false,
mocking: false,
locked: false
}
},
reducers: {
setShowChangedOnly: (state, action) => {
state.value.showChangedOnly = action.payload
},
toggleShowChangedOnly: state => {
state.value.showChangedOnly = !state.value.showChangedOnly
},
resetShowChangedOnly: state => {
state.value.showChangedOnly = false
},
setMocking: (state, action) => {
state.value.mocking = action.payload
},
toggleMocking: state => {
state.value.mocking = !state.value.mocking
},
resetMocking: state => {
state.value.mocking = false
},
setLocked: (state, action) => {
state.value.locked = action.payload
},
toggleLocked: state => {
state.value.locked = !state.value.locked
},
resetLocked: state => {
state.value.locked = false
}
}
})
export const {
setShowChangedOnly,
toggleShowChangedOnly,
resetShowChangedOnly,
setMocking,
toggleMocking,
resetMocking,
setLocked,
toggleLocked,
resetLocked
} = configSlice.actions
export default configSlice.reducer

View File

@@ -0,0 +1,101 @@
import { createSlice } from '@reduxjs/toolkit'
export const inventorySlice = createSlice({
name: 'inventory',
initialState: {
value: []
},
reducers: {
setInventory: (state, action) => {
state.value = action.payload
},
setInventoryForCharacter: (state, action) => {
const name = action.payload.name;
const character = state.value.find(inventory => inventory.name === name)
if(character !== undefined) {
character.items = action.payload.items;
character.addedItems = [];
character.removedItems = [];
} else {
state.value.push(action.payload)
}
},
replaceCharacterItems: (state, action) => {
const name = action.payload.name;
const character = state.value.find(inventory => inventory.name === name)
character.items = action.payload.items;
character.addedItems = [];
character.removedItems = [];
},
clearCharacterItems: (state, action) => {
const name = action.payload.name;
const character = state.value.find(inventory => inventory.name === name)
character.addedItems = [];
character.removedItems = [];
},
setAddedInventoryItems: (state, action) => {
let itemsToAdd = action.payload;
for(const charName in itemsToAdd) {
const itemsToAddForChar = itemsToAdd[charName];
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.addedItems = itemsToAddForChar
}
},
setRemovedInventoryItems: (state, action) => {
let itemsToRemove = action.payload;
for(const charName in itemsToRemove) {
const itemsToRemoveForChar = itemsToRemove[charName];
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.removedItems = itemsToRemoveForChar
}
},
setChangedInventoryItems: (state, action) => {
let itemsToChange = action.payload;
for(const charName in itemsToChange) {
const itemsToChangeForChar = itemsToChange[charName];
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.items.forEach(item => {
if(item.id in itemsToChangeForChar) {
item.changed = itemsToChangeForChar[item.id]
}
})
}
},
setAddedInventoryItemsForCharacter: (state, action) => {
let charName = action.payload.name;
let itemsToAdd = action.payload.addedItems;
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.addedItems = itemsToAdd
},
setRemovedInventoryItemsForCharacter: (state, action) => {
let charName = action.payload.name;
let itemsToRemove = action.payload.removedItems;
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.removedItems = itemsToRemove
},
setChangedInventoryItemsForCharacter: (state, action) => {
let charName = action.payload.name;
let itemsToUpdate = action.payload.items;
const existingCharInventory = state.value.find(inventory => inventory.name === charName)
existingCharInventory.items.forEach(item => {
if(item.id in itemsToUpdate) {
item.changed = itemsToUpdate[item.id]
}
})
}
}
})
export const { setInventory,
setInventoryForCharacter,
replaceCharacterItems,
clearCharacterItems ,
setAddedInventoryItems,
setRemovedInventoryItems,
setChangedInventoryItems,
setAddedInventoryItemsForCharacter,
setRemovedInventoryItemsForCharacter,
setChangedInventoryItemsForCharacter
} = inventorySlice.actions
export default inventorySlice.reducer

View File

@@ -0,0 +1,19 @@
import { createSlice } from '@reduxjs/toolkit'
export const itemRatesSlice = createSlice({
name: 'itemRates',
initialState: {
value: []
},
reducers: {
setItemRates: (state, action) => {
state.value = action.payload
}
}
})
export const {
setItemRates
} = itemRatesSlice.actions
export default itemRatesSlice.reducer

View File

@@ -0,0 +1,23 @@
import { createSlice } from '@reduxjs/toolkit'
export const loadingStateSlice = createSlice({
name: 'loadingState',
initialState: {
value: false
},
reducers: {
setLoading: (state, action) => {
state.value = action.payload
},
toggleLoading: state => {
state.value = !state.value
},
resetLoading: state => {
state.value = false
}
}
})
export const { setLoading, toggleLoading, resetLoading } = loadingStateSlice.actions
export default loadingStateSlice.reducer

View File

@@ -0,0 +1,30 @@
import { createSlice } from '@reduxjs/toolkit'
export const materialsSlice = createSlice({
name: 'materials',
initialState: {
value: {slots: [], addedSlots: [], removedSlots: []}
},
reducers: {
setMaterials: (state, action) => {
state.value = action.payload
},
setAddedMaterialSlots: (state, action) => {
state.value.addedSlots = action.payload
},
updateChangedMaterialSlots: (state, action) => {
let slotsToChange = action.payload;
Object.keys(slotsToChange).forEach(slotId => {
const existingSlot = state.value.slots.find(slot => slot.id === parseInt(slotId))
existingSlot.changed = slotsToChange[slotId]
})
},
setRemovedMaterialSlots: (state, action) => {
state.value.removedSlots = action.payload
}
}
})
export const { setMaterials, setRemovedMaterialSlots, setAddedMaterialSlots, updateChangedMaterialSlots, } = materialsSlice.actions
export default materialsSlice.reducer

View File

@@ -0,0 +1,23 @@
import { createSlice } from '@reduxjs/toolkit'
export const openingsSlice = createSlice({
name: 'openings',
initialState: {
value: []
},
reducers: {
setOpenings: (state, action) => {
state.value = action.payload
},
removeOpening: (state, action) => {
const openingId = action.payload.id;
state.value = state.value.filter(opening => opening.id !== openingId);
}
}
})
export const { setOpenings,
removeOpening
} = openingsSlice.actions
export default openingsSlice.reducer

View File

@@ -0,0 +1,34 @@
import { createSlice } from '@reduxjs/toolkit'
export const sharedInventorySlice = createSlice({
name: 'sharedInventory',
initialState: {
value: {slots: [], addedSlots: [], removedSlots: []}
},
reducers: {
setSharedInventory: (state, action) => {
state.value = action.payload
},
setAddedSharedInventorySlots: (state, action) => {
state.value.addedSlots = action.payload
},
updateChangedSharedInventorySlots: (state, action) => {
let slotsToChange = action.payload;
Object.keys(slotsToChange).forEach(slotId => {
const existingSlot = state.value.slots.find(slot => slot.id === parseInt(slotId))
existingSlot.changed = slotsToChange[slotId]
})
},
setRemovedSharedInventorySlots: (state, action) => {
state.value.removedSlots = action.payload
}
}
})
export const {
setSharedInventory,
setAddedSharedInventorySlots,
updateChangedSharedInventorySlots,
setRemovedSharedInventorySlots } = sharedInventorySlice.actions
export default sharedInventorySlice.reducer

View File

@@ -0,0 +1,27 @@
import { createSlice } from '@reduxjs/toolkit'
export const walletSlice = createSlice({
name: 'wallet',
initialState: {
value: {currencies: [], addedCurrencies: []}
},
reducers: {
setWallet: (state, action) => {
state.value = action.payload
},
setAddedWalletCurrency: (state, action) => {
state.value.addedCurrencies = action.payload
},
updateChangedWalletCurrencies: (state, action) => {
let currenciesToChange = action.payload;
Object.keys(currenciesToChange).forEach(currencyId => {
const existingCurrency = state.value.currencies.find(currency => currency.id === parseInt(currencyId))
existingCurrency.changed = currenciesToChange[currencyId]
})
}
}
})
export const { setWallet, setAddedWalletCurrency, updateChangedWalletCurrencies } = walletSlice.actions
export default walletSlice.reducer

View File

@@ -0,0 +1,38 @@
import { configureStore } from '@reduxjs/toolkit'
import apiKeyReducer from "./slices/apiKey";
import inventoryReducer from "./slices/inventory";
import loadingStateReducer from "./slices/loadingState";
import walletReducer from "./slices/wallet";
import bankReducer from "./slices/bank";
import itemRatesReducer from "./slices/itemRates";
import sharedInventoryReducer from "./slices/sharedInventory";
import materialsReducer from "./slices/materials";
import configReducer from "./slices/config";
import openingsReducer from "./slices/openings";
import {savingApiKeyMiddleware} from "./apiKeyMiddleware";
const apiKeyState = JSON.parse(localStorage.getItem("apiKey") || "null");
export const store = configureStore({
preloadedState: {
apiKey: {
value: apiKeyState === null ? "" : apiKeyState
}
},
reducer: {
apiKey: apiKeyReducer,
inventory: inventoryReducer,
loadingState: loadingStateReducer,
wallet: walletReducer,
bank: bankReducer,
materials: materialsReducer,
config: configReducer,
sharedInventory: sharedInventoryReducer,
openings: openingsReducer,
itemRates: itemRatesReducer
},
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware(),
savingApiKeyMiddleware.middleware
]
})

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,14 @@
function updateOptions(options, apiKey) {
const update = { ...options };
if (apiKey) {
update.headers = {
...update.headers,
"gw2-api-key": `${apiKey}`,
};
}
return update;
}
export default function fetcher(url, options) {
return fetch(url, updateOptions(options, options.apiKey));
}

View File

@@ -0,0 +1,307 @@
export function parseInventory(inventory) {
let charName = inventory.name;
let charInventory = {}
charInventory.name = charName
charInventory.items = []
charInventory.addedItems = []
charInventory.removedItems = []
const usedItems = new Map()
for (const bag of inventory.bags) {
for (const item of bag.items) {
if(!(item.id in usedItems)) {
let itemToAdd = {
id: item.id,
name: item.name,
iconUrl: item.iconUrl,
rarity: item.rarity,
count: item.count,
changed: 0
};
charInventory.items.push(itemToAdd)
usedItems[item.id] = itemToAdd
} else {
usedItems[item.id].count += item.count
}
}
}
return charInventory;
}
export function parseWallet(fullWallet) {
const currencies = fullWallet.currencies;
const accountCurrencies = []
for (const currency of currencies) {
accountCurrencies.push({
id: currency.id,
name: currency.name,
amount: currency.amount,
iconUrl: currency.iconUrl,
order: currency.order,
changed: 0
})
}
return accountCurrencies;
}
export function parseBank(bank) {
const slots = bank.slots;
const bankSlots = []
const usedItems = new Map()
for (const slot of slots) {
if(!(slot.id in usedItems)) {
let itemToAdd = {
id: slot.id,
name: slot.name,
count: slot.count,
rarity: slot.rarity,
iconUrl: slot.iconUrl,
changed: 0
};
bankSlots.push(itemToAdd)
usedItems[slot.id] = itemToAdd;
} else {
usedItems[slot.id].count += slot.count
}
}
return bankSlots;
}
export function parseSharedInventory(sharedInventory) {
const slots = sharedInventory.slots;
const sharedInventorySlots = []
const usedItems = new Map()
for (const slot of slots) {
if(!(slot.id in usedItems)) {
let itemToAdd = {
id: slot.id,
name: slot.name,
count: slot.count,
rarity: slot.rarity,
iconUrl: slot.iconUrl,
changed: 0
};
sharedInventorySlots.push(itemToAdd)
usedItems[slot.id] = itemToAdd;
} else {
usedItems[slot.id].count += slot.count
}
}
return sharedInventorySlots;
}
export function parseMaterials(materials) {
const slots = materials.slots;
const materialSlots = []
for (const slot of slots) {
materialSlots.push({
id: slot.id,
name: slot.name,
count: slot.count,
rarity: slot.rarity,
iconUrl: slot.iconUrl,
changed: 0
})
}
return materialSlots;
}
export function calculateBankDifferences(existingBank, incomingBankSlots, mocking) {
const bankMap = new Map()
existingBank.slots.forEach(slot => {
bankMap[slot.id] = slot
})
const slotsToAdd = []
const slotsAfterUpdate = new Map();
const slotsToRemove = []
const slotsToUpdate = new Map()
incomingBankSlots.forEach(slot => {
slotsAfterUpdate[slot.id] = slot;
let mockValue = ~~(Math.random() * 10) === 0 && mocking;
if (!(slot.id in bankMap) || mockValue) {
slotsToAdd.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: slot.count
})
} else {
const existingItem = bankMap[slot.id];
const mockingValue = mocking ? ~~(Math.random() * 25 - 12) : 0
slotsToUpdate[slot.id] = slot.count - existingItem.count + mockingValue;
}
})
existingBank.slots.forEach(slot => {
let mockValue = ~~(Math.random() * 20) === 0 && mocking;
if (!(slot.id in slotsAfterUpdate) || mockValue) {
slotsToRemove.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: -slot.count
})
}
})
return [slotsToAdd, slotsToRemove, slotsToUpdate];
}
export function calculateSharedInventoryDifferences(existingSharedInventory, incomingSharedInventorySlots, mocking) {
const sharedInventoryMap = new Map()
existingSharedInventory.slots.forEach(slot => {
sharedInventoryMap[slot.id] = slot
})
const slotsToAdd = []
const slotsAfterUpdate = new Map();
const slotsToRemove = []
const slotsToUpdate = new Map()
incomingSharedInventorySlots.forEach(slot => {
slotsAfterUpdate[slot.id] = slot;
let mockValue = ~~(Math.random() * 10) === 0 && mocking;
if (!(slot.id in sharedInventoryMap) || mockValue) {
slotsToAdd.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: slot.count
})
} else {
const existingItem = sharedInventoryMap[slot.id];
const mockingValue = mocking ? ~~(Math.random() * 25 - 12) : 0
slotsToUpdate[slot.id] = slot.count - existingItem.count + mockingValue;
}
})
existingSharedInventory.slots.forEach(slot => {
let mockValue = ~~(Math.random() * 20) === 0 && mocking;
if (!(slot.id in slotsAfterUpdate) || mockValue) {
slotsToRemove.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: -slot.count
})
}
})
return [slotsToAdd, slotsToRemove, slotsToUpdate];
}
export function calculateMaterialsDifference(materials, materialSlots, mocking) {
const materialMap = new Map()
materials.slots.forEach(slot => {
materialMap[slot.id] = slot
})
const slotsToAdd = []
const slotsAfterUpdate = new Map()
const slotsToRemove = []
const slotsToUpdate = new Map()
materialSlots.forEach(slot => {
slotsAfterUpdate[slot.id] = slot;
let mockValue = ~~(Math.random() * 10) === 0 && mocking;
if (!(slot.id in materialMap) || mockValue) {
slotsToAdd.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: slot.count
})
} else {
const existingItem = materialMap[slot.id];
const mockingValue = mocking ? ~~(Math.random() * 25 - 12) : 0
slotsToUpdate[slot.id] = slot.count - existingItem.count + mockingValue;
}
})
materials.slots.forEach(slot => {
let mockValue = ~~(Math.random() * 20) === 0 && mocking;
if (!(slot.id in slotsAfterUpdate) || mockValue) {
slotsToRemove.push({
id: slot.id,
name: slot.name,
iconUrl: slot.iconUrl,
rarity: slot.rarity,
count: 0,
changed: -slot.count
})
}
})
return [slotsToAdd, slotsToRemove, slotsToUpdate];
}
export function calculateWalletDifference(wallet, accountCurrencies, mocking) {
const walletMap = new Map()
const currenciesToUpdate = new Map()
wallet.currencies.forEach(currency => {
walletMap[currency.id] = currency
})
const currenciesToAdd = []
accountCurrencies.forEach(currency => {
let mockValue = ~~(Math.random() * 10) === 0 && mocking;
if (!(currency.id in walletMap) || mockValue) {
currenciesToAdd.push({
id: currency.id,
name: currency.name,
iconUrl: currency.iconUrl,
amount: 0,
changed: currency.amount
})
} else {
const existingItem = walletMap[currency.id];
const mockingValue = mocking ? ~~(Math.random() * 25 - 12) : 0
currenciesToUpdate[currency.id] = currency.amount - existingItem.amount + mockingValue;
}
})
return [currenciesToUpdate, currenciesToAdd];
}
export function calculateInventoryDifferencesForCharacter(existingInventory, incomingCharacterInventory, mocking) {
const itemMap = new Map()
existingInventory.items.forEach(item => {
itemMap[item.id] = item
})
const itemsAfterUpdate = new Map()
const itemsToUpdateForCharacter = {}
const itemsToAdd = []
const itemsToRemove = []
incomingCharacterInventory.items.forEach(item => {
itemsAfterUpdate[item.id] = item
let mockValue = ~~(Math.random() * 10) === 0 && mocking;
if (!(item.id in itemMap) || mockValue) {
itemsToAdd.push({
id: item.id,
name: item.name,
iconUrl: item.iconUrl,
rarity: item.rarity,
count: 0,
changed: item.count
})
} else { // completely removed items are not marked with - or completely removed
const existingItem = itemMap[item.id];
const mockingValue = mocking ? ~~(Math.random() * 25 - 12) : 0
itemsToUpdateForCharacter[item.id] = item.count - existingItem.count + mockingValue;
}
})
for (const itemIdBefore in itemMap) {
let mockValue = ~~(Math.random() * 20) === 0 && mocking;
if (!(itemIdBefore in itemsAfterUpdate) || mockValue) {
const itemBefore = itemMap[itemIdBefore]
itemsToRemove.push({
id: itemBefore.id,
name: itemBefore.name,
iconUrl: itemBefore.iconUrl,
rarity: itemBefore.rarity,
count: 0,
changed: -itemBefore.count
})
itemsToUpdateForCharacter[itemIdBefore] = -itemMap[itemIdBefore].count
}
}
return [itemsToAdd, itemsToRemove, itemsToUpdateForCharacter]
}