Showing preview only (2,609K chars total). Download the full file or copy to clipboard to get everything.
Repository: the-hideout/tarkov-dev
Branch: main
Commit: 5a9fd0cf37f2
Files: 399
Total size: 2.3 MB
Directory structure:
gitextract_ffd8hvvc/
├── .devcontainer/
│ ├── Dockerfile
│ ├── devcontainer.json
│ └── docker-compose.yml
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ ├── config.yml
│ │ └── feature-request.yml
│ ├── dependabot.yml
│ ├── exclude.txt
│ ├── new-pr-comment.md
│ ├── pull_request_template.md
│ └── workflows/
│ ├── branch-deploy.yml
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ ├── combine-prs.yml
│ ├── deploy.yml
│ ├── new-pr.yml
│ └── unlock-on-merge.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .node-version
├── .nvmrc
├── .prettierignore
├── .stylelintignore
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── additional.d.ts
├── dependabot.yml
├── i18next-parser.config.mjs
├── package.json
├── prettier.config.mjs
├── public/
│ ├── browserconfig.xml
│ ├── data/
│ │ └── .gitignore
│ ├── index.html
│ ├── robots.txt
│ └── site.webmanifest
├── rsbuild.config.ts
├── rstest.config.ts
├── rstest.setup.ts
├── scripts/
│ ├── build-redirects.mjs
│ ├── build-sitemap.mjs
│ ├── critical.mjs
│ ├── custom-loader.mjs
│ ├── generate-thumbnails.mjs
│ ├── generate_api-users_thumbs_macOS.sh
│ ├── generate_items_thumbs.mjs
│ ├── generate_items_thumbs_macOS.sh
│ ├── generate_known_icons_macOS.sh
│ ├── get-contributors.mjs
│ ├── get-supported-languages.mjs
│ ├── test-redirects.mjs
│ └── update-props.mjs
├── src/
│ ├── App.css
│ ├── App.jsx
│ ├── __tests__/
│ │ ├── App.test.jsx
│ │ ├── test-utils.js
│ │ └── tsconfig.json
│ ├── components/
│ │ ├── Debug.jsx
│ │ ├── FilterIcon.jsx
│ │ ├── FleaMarketLoadingIcon.jsx
│ │ ├── Graph.jsx
│ │ ├── GraphLabel.jsx
│ │ ├── SEO.jsx
│ │ ├── Symbol.jsx
│ │ ├── Time.jsx
│ │ ├── api-metrics-graph/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── barter-tooltip/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── barters-table/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── boss-list/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── canvas-grid/
│ │ │ └── index.jsx
│ │ ├── center-cell/
│ │ │ └── index.jsx
│ │ ├── cheeki-breeki-effect/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── contained-items-list/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── contributors/
│ │ │ └── index.jsx
│ │ ├── cost-items-cell/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── countdown/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── crafts-table/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── data-table/
│ │ │ ├── Arrow.tsx
│ │ │ ├── TableHead.tsx
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── em-item-tag/
│ │ │ └── index.jsx
│ │ ├── filter/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── flea-price-cell/
│ │ │ └── index.jsx
│ │ ├── footer/
│ │ │ ├── index.css
│ │ │ └── index.tsx
│ │ ├── item-cost/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item-grid/
│ │ │ ├── Item.jsx
│ │ │ ├── ItemIcon.jsx
│ │ │ ├── ItemTooltip.jsx
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item-icon-list/
│ │ │ └── index.jsx
│ │ ├── item-image/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item-name-cell/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item-search/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── items-for-hideout/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── items-summary-table/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── loading/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── loading-small/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── loyalty-level-icon/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── menu/
│ │ │ ├── CategoryMenu.jsx
│ │ │ ├── MenuItem.jsx
│ │ │ ├── alert-config.js
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ ├── menu-data.js
│ │ │ └── useMenuOverflow.js
│ │ ├── open-collective-button/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── patreon-button/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── points/
│ │ │ ├── Circle.jsx
│ │ │ ├── Diamond.jsx
│ │ │ ├── Plus.jsx
│ │ │ ├── Square.jsx
│ │ │ ├── TriangleDown.jsx
│ │ │ ├── TriangleUp.jsx
│ │ │ └── index.jsx
│ │ ├── preset-selector/
│ │ │ └── index.jsx
│ │ ├── price-graph/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── property-list/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── quest-items-cell/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── quest-table/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── remote-control-id/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── reward-cell/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── reward-image/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── scroll-to-top/
│ │ │ └── index.jsx
│ │ ├── server-status/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── small-item-table/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── station-skill-trader-setting/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── supporter/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── supporters-list/
│ │ │ └── index.jsx
│ │ ├── trader-image/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── trader-price-cell/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── trader-reset-time/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── ukraine-button/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ └── value-cell/
│ │ ├── index.css
│ │ └── index.jsx
│ ├── data/
│ │ ├── api-users.json
│ │ ├── bosses.json
│ │ ├── category-pages.json
│ │ ├── game-modes.json
│ │ ├── item-grids.json
│ │ ├── maps.json
│ │ ├── maps_static.json
│ │ ├── patreons.json
│ │ └── wipe-details.json
│ ├── features/
│ │ ├── barters/
│ │ │ ├── do-fetch-barters.mjs
│ │ │ └── index.js
│ │ ├── crafts/
│ │ │ ├── do-fetch-crafts.mjs
│ │ │ └── index.js
│ │ ├── hideout/
│ │ │ ├── do-fetch-hideout.mjs
│ │ │ └── index.js
│ │ ├── items/
│ │ │ ├── do-fetch-items.mjs
│ │ │ └── index.js
│ │ ├── maps/
│ │ │ ├── do-fetch-maps.mjs
│ │ │ └── index.js
│ │ ├── quests/
│ │ │ ├── do-fetch-quests.mjs
│ │ │ └── index.js
│ │ ├── settings/
│ │ │ └── settingsSlice.mjs
│ │ ├── sockets/
│ │ │ └── socketsSlice.js
│ │ ├── status/
│ │ │ ├── do-fetch-status.mjs
│ │ │ └── index.mjs
│ │ └── traders/
│ │ ├── do-fetch-traders.mjs
│ │ └── index.js
│ ├── hooks/
│ │ ├── useDate.jsx
│ │ ├── useKeyPress.jsx
│ │ ├── useObserver.jsx
│ │ ├── useRepositoryContributors.js
│ │ └── useStateWithLocalStorage.jsx
│ ├── i18n.js
│ ├── index.jsx
│ ├── modules/
│ │ ├── api-query.mjs
│ │ ├── api-request.mjs
│ │ ├── best-price.js
│ │ ├── camelcase-to-dashes.js
│ │ ├── capitalize-first.js
│ │ ├── dogtags.js
│ │ ├── flea-market-fee.mjs
│ │ ├── format-ammo.mjs
│ │ ├── format-category-name.js
│ │ ├── format-cost-items.js
│ │ ├── format-duration.js
│ │ ├── format-price.js
│ │ ├── graphql-request.mjs
│ │ ├── item-can-contain.js
│ │ ├── item-search.js
│ │ ├── lang-helpers.js
│ │ ├── leaflet-control-coordinates.js
│ │ ├── leaflet-control-groupedlayer.js
│ │ ├── leaflet-control-map-search.js
│ │ ├── leaflet-control-map-settings.js
│ │ ├── leaflet-control-raid-info.js
│ │ ├── leaflet-control-remote.js
│ │ ├── make-id.js
│ │ ├── mui-theme.mjs
│ │ ├── player-stats.mjs
│ │ ├── polyfills.js
│ │ ├── property-format.js
│ │ ├── queue-browser-task.js
│ │ ├── remote-websocket.mjs
│ │ ├── task-elements.css
│ │ ├── task-elements.mjs
│ │ ├── window-focus-handler.mjs
│ │ └── wipe-length.js
│ ├── pages/
│ │ ├── about/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── achievements/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── ammo/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── api-docs/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── api-users/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── barters/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── bitcoin-farm-calculator/
│ │ │ ├── data.js
│ │ │ ├── graph.jsx
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ ├── profit-info.jsx
│ │ │ └── profitable-graph.jsx
│ │ ├── boss/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── bosses/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── control/
│ │ │ ├── Connect.jsx
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── converter/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── crafts/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── error-page/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── hideout/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── item-tracker/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── items/
│ │ │ ├── armors/
│ │ │ │ └── index.jsx
│ │ │ ├── backpacks/
│ │ │ │ └── index.jsx
│ │ │ ├── barter-items/
│ │ │ │ └── index.jsx
│ │ │ ├── bsg-category/
│ │ │ │ └── index.jsx
│ │ │ ├── containers/
│ │ │ │ └── index.jsx
│ │ │ ├── glasses/
│ │ │ │ └── index.jsx
│ │ │ ├── grenades/
│ │ │ │ └── index.jsx
│ │ │ ├── guns/
│ │ │ │ └── index.jsx
│ │ │ ├── handbook-category/
│ │ │ │ └── index.jsx
│ │ │ ├── headsets/
│ │ │ │ └── index.jsx
│ │ │ ├── helmets/
│ │ │ │ └── index.jsx
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ ├── keys/
│ │ │ │ └── index.jsx
│ │ │ ├── mods/
│ │ │ │ └── index.jsx
│ │ │ ├── pistol-grips/
│ │ │ │ └── index.jsx
│ │ │ ├── provisions/
│ │ │ │ └── index.jsx
│ │ │ ├── rigs/
│ │ │ │ └── index.jsx
│ │ │ └── suppressors/
│ │ │ └── index.jsx
│ │ ├── loot-tiers/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── map/
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ └── map-images.mjs
│ │ ├── maps/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── moobot/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── nightbot/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── other-tools/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── player/
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ └── player-forward.jsx
│ │ ├── players/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── prestige/
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ └── list.jsx
│ │ ├── quest/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── quests/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── settings/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── start/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── stash-bot/
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ ├── stream-elements/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── tarkov-monitor/
│ │ │ ├── index.css
│ │ │ └── index.js
│ │ ├── trader/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── traders/
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ └── wipe-length/
│ │ ├── index.css
│ │ └── index.jsx
│ ├── serviceWorker.js
│ ├── setupTests.js
│ ├── store.js
│ ├── styles/
│ │ ├── mapRemote.css
│ │ ├── mapSearch.css
│ │ ├── mapSettings.css
│ │ └── singleEntity.css
│ └── translations/
│ ├── de/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── en/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── es/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── fr/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── it/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── ja/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── pl/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── pt/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── remove_equal_key_value.py
│ ├── ru/
│ │ ├── bosses.json
│ │ ├── maps.json
│ │ ├── properties.json
│ │ └── translation.json
│ ├── sync_key_value.py
│ └── zh/
│ ├── bosses.json
│ ├── maps.json
│ ├── properties.json
│ └── translation.json
├── stylelint.config.mjs
├── tsconfig.json
├── workers-site/
│ ├── .cargo-ok
│ ├── .gitignore
│ ├── index-template.js
│ └── package.json
└── wrangler.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
FROM node:20
EXPOSE 3000
ENV APP_ROOT /tarkov-dev
RUN mkdir -p $APP_ROOT
WORKDIR $APP_ROOT
CMD ["/bin/bash", "-c", "npm install && npm start && while :; do sleep 1; done"]
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "Tarkov-Dev Local Dev",
"dockerComposeFile": "docker-compose.yml",
"service": "dev",
"workspaceFolder": "/tarkov-dev",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {
"installDirectlyFromGitHubRelease": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "24.11.1", // Defaults to 'lts'
"nvmVersion": "latest", // Default value
"installYarnUsingApt": false
},
"common": {
"username": "automatic",
"uid": "automatic",
"gid": "automatic",
"installZsh": true,
"configureZshAsDefaultShell": true,
"installOhMyZsh": true,
"upgradePackages": true,
"nonFreePackages": false
}
},
"customizations": { // jetbrains devcontainer compatability for zsh
"jetbrains": {
"settings": {
"org.jetbrains.plugins.terminal:app:TerminalOptionsProvider.myShellPath": "/bin/zsh"
}
},
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"bash": {
"path": "bash",
"icon": "terminal-bash"
},
"zsh": {
"path": "zsh",
"icon": "terminal-bash"
}
}
}
},
"extensions": [ // adds to @recommended exentions to install into devcontainer
"jasonnutter.search-node-modules",
"eamodio.gitlens",
"dbaeumer.vscode-eslint"
]
}
}
================================================
FILE: .devcontainer/docker-compose.yml
================================================
services:
dev:
build:
context: ..
dockerfile: ./.devcontainer/Dockerfile
volumes:
- ..:/tarkov-dev
ports:
- "3000:3000"
================================================
FILE: .github/CODEOWNERS
================================================
# Default reviewers for all files in the repo
* @the-hideout/reviewers
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
open_collective: tarkov-dev
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: Bug Report
description: File a bug/issue report
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
# Bug Report 🐛
Thanks for taking the time to fill out this bug report!
Please answer each question below to your best ability. It is okay to leave questions blank if you have to!
- type: textarea
id: description
attributes:
label: Describe the Issue
description: Please describe the bug/issue in detail
placeholder: Something is wrong with X when going to page Y
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen
placeholder: I expected X to happen when going to page Y
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To Reproduce
description: If you know how to reproduce the issue, please provided detailed steps below
placeholder: Go to page Y and click on button Z. Look at the console and see error XYZ
validations:
required: true
- type: dropdown
id: client
attributes:
label: Client
description: What type of client are you using to reproduce this error?
options:
- Desktop
- Mobile
- Other
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser
description: What type of web browser are you using to reproduce this error?
options:
- Chrome
- Brave
- Firefox
- Opera
- Safari
- Edge
- Chromium
- Other
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Console Log Output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: console
- type: textarea
id: extra
attributes:
label: Extra Information
description: Any extra information, links to issues, screenshots, etc
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yml
================================================
name: Feature Request
description: Suggest a new feature or enhancement
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
# Feature Request 🏆
Thanks for taking the time to fill out this feature request!
Please answer each question below to your best ability. It is okay to leave questions blank if you have to!
- type: textarea
id: description
attributes:
label: Describe the Feature Request
description: Please describe the feature request in detail
placeholder: I would like feature XYZ because ABC
================================================
FILE: .github/dependabot.yml
================================================
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
---
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
groups:
github-actions:
patterns:
- "*"
schedule:
interval: monthly
- package-ecosystem: npm
directory: "/"
groups:
npm-dependencies:
patterns:
- "*"
schedule:
interval: monthly
cooldown:
default-days: 90
ignore:
- dependency-name: "i18next"
update-types: ["version-update:semver-major"]
- dependency-name: "react"
update-types: ["version-update:semver-major"]
- dependency-name: "react-i18next"
update-types: ["version-update:semver-minor"]
- dependency-name: "react-router"
update-types: ["version-update:semver-major"]
- dependency-name: "react-router-dom"
update-types: ["version-update:semver-major"]
- dependency-name: "eslint"
update-types: ["version-update:semver-major"]
================================================
FILE: .github/exclude.txt
================================================
# custom exclude file for the GrantBirki/json-yaml-validate@vX.X.X Actions workflow
.devcontainer/devcontainer.json
================================================
FILE: .github/new-pr-comment.md
================================================
### 👋 Thanks for opening a pull request!
If you are new, please check out the trimmed down summary of our deployment process below:
1. 👀 Observe the CI jobs and tests to ensure they are passing
1. ✔️ Obtain an approval/review on this pull request
1. 🚀 Deploy your pull request to the **development** environment with `.deploy to development`
1. 🚀 Deploy your pull request to the **production** environment with `.deploy`
> If anything goes wrong, rollback with `.deploy main`
1. 🎉 Merge!
> Note: If you have a larger change and want to block deployments, you can run `.lock --reason <reason>` to lock all other deployments (remove with `.unlock`)
You can view the branch deploy [usage guide](https://github.com/github/branch-deploy/blob/main/docs/usage.md) for additional information
================================================
FILE: .github/pull_request_template.md
================================================
<!--
⚠️ IMPORTANT ⚠️
- Please fill in all sections [in brackets]
- Please read the collapsible sections if you need help
- All comments in fenced brackets like this one are comments and will not be displayed
Please delete any sections below which you do not need to fill out. You may keep the collapsible "help" section
-->
# [title here]
<!-- REQUIRED: Place a short description of your change here -->
## Description 🗒️
<!-- OPTIONAL: Place a long form description of your change here if necessary - describe why your change is needed -->
## Examples 📸
<!-- OPTIONAL: Screenshots with examples for your changes if they are UI related -->
## Related Issues 🔗
<!-- OPTIONAL: If this PR fixes, closes, or resolves an issue; please link that issue here -->
---
<details>
<summary> Expand for Help </summary>
- Have questions about the review or deployment process? View our [contributing docs](https://github.com/the-hideout/tarkov-dev/blob/main/CONTRIBUTING.md)
- Need additional help and want to chat in real time? Join our [community Discord](https://discord.gg/WwTvNe356u)
> By submitting this pull request, you agree to our [code of conduct](https://github.com/the-hideout/tarkov-dev/blob/main/CODE_OF_CONDUCT.md)
</details>
================================================
FILE: .github/workflows/branch-deploy.yml
================================================
name: branch-deploy
on:
issue_comment:
types: [ created ]
# Permissions needed for reacting and adding comments for IssueOps commands
permissions:
pull-requests: write
deployments: write
contents: write
checks: read
statuses: read
jobs:
deploy:
environment: secrets
if: ${{ github.event.issue.pull_request }} # only run on pull request comments
runs-on: ubuntu-latest
steps:
- uses: github/branch-deploy@v11
id: branch-deploy
with:
admins: the-hideout/core-contributors
admins_pat: ${{ secrets.BRANCH_DEPLOY_ADMINS_PAT }}
environment_targets: production,development
environment_urls: production|https://tarkov.dev,development|disabled
sticky_locks: "true"
- name: checkout
if: ${{ steps.branch-deploy.outputs.continue == 'true' }}
uses: actions/checkout@v6
with:
ref: ${{ steps.branch-deploy.outputs.sha }}
- uses: actions/setup-node@v6.4.0
if: ${{ steps.branch-deploy.outputs.continue == 'true' }}
with:
node-version-file: .node-version
cache: 'npm'
- name: install dependencies
if: ${{ steps.branch-deploy.outputs.continue == 'true' }}
run: npm ci
- name: env setup
if: ${{ steps.branch-deploy.outputs.continue == 'true' }}
env:
PUBLIC_URL: ''
run: |
touch .env
echo PUBLIC_URL=$PUBLIC_URL >> .env
- name: build
if: ${{ steps.branch-deploy.outputs.continue == 'true' }}
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.HIDEOUT_BOT_TOKEN }}
# deploy to the dev env and also save the stdout to a file
- name: deploy - dev
id: dev-deploy
if: ${{ steps.branch-deploy.outputs.continue == 'true' &&
steps.branch-deploy.outputs.noop != 'true' &&
steps.branch-deploy.outputs.environment == 'development' }}
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # pin@v3.15.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
wranglerVersion: '2.13.0'
command: pages publish build/ --project-name=tarkov-dev --branch=preview
# fetch the dev url from stdout and save add it to the deploy message
- name: add development url to deploy message
if: ${{ steps.branch-deploy.outputs.continue == 'true' &&
steps.branch-deploy.outputs.noop != 'true' &&
steps.branch-deploy.outputs.environment == 'development' }}
env:
DEPLOYMENT_URL: ${{ steps.dev-deploy.outputs.deployment-url }}
CMD_OUTPUT: ${{ steps.dev-deploy.outputs.command-output }}
run: |
echo "for debugging (cmd output): ${CMD_OUTPUT}"
echo ""
echo "DEPLOY_MESSAGE=${DEPLOYMENT_URL}" >> $GITHUB_ENV
echo "DEPLOY_MESSAGE=${DEPLOYMENT_URL}"
- name: deploy - prod
id: prod-deploy
if: ${{ steps.branch-deploy.outputs.continue == 'true' &&
steps.branch-deploy.outputs.noop != 'true' &&
steps.branch-deploy.outputs.environment == 'production' }}
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # pin@v3.15.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
wranglerVersion: '2.13.0'
command: pages publish build/ --project-name=tarkov-dev --branch=main
================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
on:
push:
branches:
- main
pull_request:
branches: [ main ]
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
# check to ensure all JSON files are valid in the repository
- name: json-yaml-validate
uses: GrantBirki/json-yaml-validate@v4.0.0
with:
comment: "true"
exclude_file: .github/exclude.txt
- uses: actions/setup-node@v6.4.0
with:
node-version-file: .node-version
cache: 'npm'
- name: install dependencies
run: npm ci
- name: env setup
env:
PUBLIC_URL: ''
run: |
touch .env
echo PUBLIC_URL=$PUBLIC_URL >> .env
- name: Get files needed
run: npm run prebuild
env:
GITHUB_TOKEN: ${{ secrets.HIDEOUT_BOT_TOKEN }}
- name: test
run: npm run test
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
name: CodeQL
on:
push:
branches: [ main ]
schedule:
- cron: '29 10 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: checkout
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pin@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pin@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pin@v2
================================================
FILE: .github/workflows/combine-prs.yml
================================================
name: Combine PRs
on:
schedule:
- cron: "0 1 * * 3" # Wednesday at 01:00
workflow_dispatch:
jobs:
combine-prs:
uses: the-hideout/reusable-workflows/.github/workflows/combine-prs.yml@main
secrets:
COMBINE_PRS_APP_ID: ${{ secrets.COMBINE_PRS_APP_ID }}
COMBINE_PRS_PRIVATE_KEY: ${{ secrets.COMBINE_PRS_PRIVATE_KEY }}
fallback: ${{ secrets.GITHUB_TOKEN }} # fall back to the default token if the app token is not available
================================================
FILE: .github/workflows/deploy.yml
================================================
name: deploy
on:
push:
branches:
- main
permissions:
contents: read
jobs:
deploy:
if: github.event_name == 'push'
environment: production
runs-on: ubuntu-latest
steps:
- name: deployment check
uses: github/branch-deploy@v11
id: deployment-check
with:
merge_deploy_mode: "true" # tells the Action to use the merge commit workflow strategy
environment: production
- name: checkout
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
uses: actions/checkout@v6
with:
ref: ${{ steps.deployment-check.outputs.sha }}
# check to ensure all JSON files are valid in the repository
- name: json-yaml-validate
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
uses: GrantBirki/json-yaml-validate@v4.0.0
- uses: actions/setup-node@v6.4.0
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
with:
node-version-file: .node-version
cache: 'npm'
- name: install dependencies
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
run: npm ci
- name: env setup
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
env:
PUBLIC_URL: ''
run: |
touch .env
echo PUBLIC_URL=$PUBLIC_URL >> .env
- name: build
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.HIDEOUT_BOT_TOKEN }}
PUBLIC_URL: ''
- name: test
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
run: npm run test
- name: deploy
if: ${{ steps.deployment-check.outputs.continue == 'true' }}
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # pin@v3.15.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
wranglerVersion: '2.13.0'
command: pages publish build/ --project-name=tarkov-dev --branch=main
# Uncomment to enable Sentry releases via CI
# - name: Create Sentry release
# uses: getsentry/action-release@744e4b262278339b79fb39c8922efcae71e98e39 # pin@v1.1.6
# env:
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# SENTRY_ORG: tarkov-dev
# SENTRY_PROJECT: tarkovdev
# with:
# environment: production
# sourcemaps: ./build/static/
# Always run this step on push to main
- name: CDN Purge
# if: ${{ steps.deployment-check.outputs.continue == 'true' }}
uses: jakejarvis/cloudflare-purge-action@eee6dba0236093358f25bb1581bd615dc8b3d8e3 # pin@v0.3.0
env:
CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_PURGE_TOKEN }}
PURGE_URLS: '["https://tarkov.dev/data/item-grids.min.json", "https://tarkov.dev/data/item-props.min.json"]'
================================================
FILE: .github/workflows/new-pr.yml
================================================
name: New Pull Request
on:
pull_request:
branches:
- main
permissions:
pull-requests: write
jobs:
comment:
if: github.event_name == 'pull_request' && github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
# Comment on new PR requests with deployment instructions
- uses: actions/checkout@v6
- name: comment
uses: GrantBirki/comment@v2.1.1
continue-on-error: true
with:
file: .github/new-pr-comment.md
================================================
FILE: .github/workflows/unlock-on-merge.yml
================================================
name: Unlock On Merge
on:
pull_request:
types: [closed]
permissions:
contents: write
jobs:
unlock-on-merge:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
steps:
- name: unlock on merge
uses: github/branch-deploy@v11
id: unlock-on-merge
with:
unlock_on_merge_mode: "true" # <-- indicates that this is the "Unlock on Merge Mode" workflow
environment_targets: production,development
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,react,windows,jetbrains,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,react,windows,jetbrains,visualstudiocode
### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
*.sublime*
psd
thumb
sketch
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,macos,react,windows,jetbrains,visualstudiocode
build/
public/maps/*_thumb.jpg
src/data/contributors.json
src/data/supported-languages.json
# Cache
src/data/barters_cached.json
src/data/bosses_cached.json
src/data/bosses_locale.json
src/data/crafts_cached.json
src/data/hideout_cached.json
src/data/items_cached.json
src/data/items_locale.json
src/data/maps_cached.json
src/data/maps_locale.json
src/data/meta_cached.json
src/data/quests_cached.json
src/data/quests_locale.json
src/data/traders_cached.json
src/data/traders_locale.json
src/data/project-contributors.json
# Sitemap
public/sitemap*.xml
public/sitemap*.xml.gz
# Workers
workers-site/index.js
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged
================================================
FILE: .node-version
================================================
24.11.1
================================================
FILE: .nvmrc
================================================
24.11.1
================================================
FILE: .prettierignore
================================================
# Ignore artifacts:
build
dist
# Ignore all HTML files:
**/*.html
================================================
FILE: .stylelintignore
================================================
# Ignore artifacts:
build
dist
# Ignore all HTML files:
**/*.html
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": [
"Tarkov"
]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
our community Discord server.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute to tarkov-dev 💻
> This contributing guide is specfic to the [tarkov-dev](https://github.com/the-hideout/tarkov-dev) repo but many of its practices are shared with other repos in [the-hideout](https://github.com/the-hideout)
## Reporting a Bug 🐛
- Do not open up a GitHub issue if the bug is a security vulnerability, and instead to refer to our [security policy](SECURITY.md)
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/the-hideout/tarkov-dev/issues)
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/the-hideout/tarkov-dev/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring (if possible)
## Opening a Pull Request 🌟
If you have a fix for a bug or a feature request, follow the flow below to propose your change
> If you are new to creating pull requests from a repository fork, check out this [guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
### Pull Request - TL;DR
If you don't want to read the detailed section below (you should), here is a TL;DR for our PR process:
1. Open a PR with changes
2. A team member will run CI, review, and deploy (first to dev, then prod)
3. If all looks good, we merge the PR
## Pull Request - Detailed
1. Fork the [tarkov-dev](https://github.com/the-hideout/tarkov-dev) repo
2. Clone your forked repo
3. Make your changes and ensure they work locally
4. Push your changes to your forked repo
5. Open a pull request on GitHub with the `tarkov-dev` repo and the `main` branch as the target
6. Ensure your pull request has a meaningful title, description, and links to any related issues
Hooray! You have opened a PR with your changes. Now a member from [the-hideout/reviewers](https://github.com/orgs/the-hideout/teams/reviewers) will step in and follow the process below:
1. A [the-hideout/reviewers](https://github.com/orgs/the-hideout/teams/reviewers) member will review your PR
2. They will run the GitHub Actions CI suite on your PR
3. If CI is passing, they will comment `.deploy to development` to ship your changes to our development environment
4. At this point, the reviewer will review the changes live in development. You should also test your changes as well in this environment to ensure they are working as expected
5. If all looks good, the reviewer will run `.deploy` to ship your changes to our production environment
6. If nothing goes wrong, the reviewer will merge the PR
7. 🎉
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Oskar Risberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# tarkov.dev 💻
[](https://github.com/the-hideout/tarkov-dev/actions/workflows/deploy.yml) [](https://github.com/the-hideout/tarkov-dev/actions/workflows/ci.yml) [](https://github.com/the-hideout/tarkov-dev/actions/workflows/codeql-analysis.yml)  [](https://discord.gg/WwTvNe356u)
 

This is the source code for the official [tarkov.dev](https://tarkov.dev) website.
View Escape from Tarkov information about items, barters, trades, flea market prices, quests, maps, hideout profits, and so much more!

## Local Development 🔨
To build and test the site locally just follow the steps below:
0. Install Node.js
```bash
# use nvm to install the correct version of Node.js
nvm use
```
1. Install dependencies:
```bash
npm install
```
1. Copy .env.example to .env (no values need to be changed)
1. Start development server:
```bash
npm start
```
1. Access the site: [localhost:3000](http://localhost:3000/) 🎉
> Note: You can update data with: `npm run prebuild`
## VS Code Dev Container
1. Open VS Code command palette:
```
cmd + shift + p / ctrl + shift + p
```
2. Start the dev container:
```
> Dev Containers: open folder in container...
```
3. Select local path to tarkov-dev repo
4. After the container builds and starts it will auto run `npm install && npm start`
5. Access the site: [localhost:3000](http://localhost:3000/) 🎉
## History 📚
This project ([tarkov-dev](https://github.com/the-hideout/tarkov-dev)) is a fork of [tarkov-tools.com](https://github.com/kokarn/tarkov-tools). The original creator [@kokarn](https://github.com/kokarn) decided to shut the site down. In the spirit of open source, a group of developers came together to revive the site in order to continue providing a great website for the Tarkov community and an API to power further development for creators. This project is now 100% open source (see infrastructure section below) and developer first. Our GitHub Organization ([the-hideout](https://github.com/the-hideout)) contains all the repos which power the API, this website, the community Discord bot, server infrastructure, and much more! We are passionate about open source and love pull requests to improve our ecosystem for all.
## We ❤️ Pull Requests
We love pull requests and contributors looking to improve this project! Anything from simple spelling errors, icon updates, fixes for small css bugs or just posting issues to keep track of what needs to be done is greatly appreciated.
## Deployment 🚀
Deploying your changes to production is easy! Just do the following:
1. Open a pull request with your changes
1. Make sure CI is passing (a core member of [the-hideout](https://github.com/orgs/the-hideout/teams/core-contributors) will run CI for you)
1. A core member of [the-hideout](https://github.com/orgs/the-hideout/teams/core-contributors) will run `.deploy to development` to deploy your changes to the development environment for final validation
1. A review will be recieved from a [reviewer](https://github.com/orgs/the-hideout/teams/reviewers) if all looks good
1. A core member of [the-hideout](https://github.com/orgs/the-hideout/teams/core-contributors) will run `.deploy` on your pull request to branch deploy your changes to production
1. If everything goes okay, your PR will be merged and your changes will be auto-deployed to production! ✨
## Updating Languages 🌐
There are two _ways_ to update languages on the site:
- Updating the core translations (most common)
- Updating the language that the GraphQL API uses (least common)
### Language Translations
Rather than go into detail here, we have opened a great guide in a GitHub issue for how you can provide translation contributions to tarkov.dev!
> Check out the guide [here](https://github.com/the-hideout/tarkov-dev/issues/175)
### GraphQL API Language Support
To update the supported languages used by the site with the **GraphQL API**, you will need to edit the following file: [`supported-languages.json`](https://github.com/the-hideout/tarkov-dev/blob/main/src/data/supported-languages.json)
> See this [pull request](https://github.com/the-hideout/tarkov-dev/pull/123) for additional context
## Other Parts of the Ecosystem 🌎
- [Stash](https://github.com/the-hideout/stash) - The official tarkov.dev Discord bot
- [Tarkov API](https://github.com/the-hideout/tarkov-api) - The GraphQL API that powers everything
- [Tarkov Data](https://github.com/TarkovTracker/tarkovdata/) - Open source structured data for Escape from Tarkov
- [Tarkov Image Generator](https://github.com/the-hideout/tarkov-image-generator) - Tool to generate images from the local icon cache
- [Tarkov Data Manager](https://github.com/the-hideout/tarkov-data-manager) - Data manager that core contributors to the project can use to update items in the database. It also contains cron jobs that sync database information to our Cloudflare workers for the GraphQL API
- [Cache](https://github.com/the-hideout/cache) - A bespoke caching service to cache frequent GraphQL API queries
- [Status](https://github.com/the-hideout/status) - The official status page for tarkov.dev, api.tarkov.dev, and much more
## Infrastructure 🧱
To learn more about the infrastructure, components, and open source pieces of this project, check out our [infrastructure documentation](https://github.com/the-hideout/.github/blob/main/profile/docs/infrastructure.md#opensource-notice-)
## Contributors 🧑🤝🧑
Thank you to all of our awesome contributors! ❤️
<a href="https://github.com/the-hideout/tarkov-dev/graphs/contributors">
<img src="https://contrib.rocks/image?repo=the-hideout/tarkov-dev" />
</a>
================================================
FILE: SECURITY.md
================================================
# Security Policy 🔒
## Supported Versions
The `main` branch of this repo is considered active and supported for all security concerns
## Reporting a Vulnerability
If you discover a security vulnerability, please reach out in our [community Discord](https://discord.gg/WwTvNe356u) to report it.
================================================
FILE: additional.d.ts
================================================
declare const __COMMIT_HASH__: string;
declare const __BRANCH_NAME__: string;
declare module "*.svg" {
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
const content: string;
export default content;
}
declare module "*.svg?react" {
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export default ReactComponent;
}
================================================
FILE: dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
ignore:
- dependency-name: "i18next"
update-types: ["version-update:semver-major"]
- dependency-name: "react"
update-types: ["version-update:semver-major"]
- dependency-name: "react-i18next"
update-types: ["version-update:semver-minor"]
- dependency-name: "react-router"
update-types: ["version-update:semver-major"]
- dependency-name: "react-router-dom"
update-types: ["version-update:semver-major"]
================================================
FILE: i18next-parser.config.mjs
================================================
// i18next-parser.config.mjs
const config = {
contextSeparator: "_",
// Key separator used in your translation keys
createOldCatalogs: true,
// Save the \_old files
defaultNamespace: "translation",
// Default namespace used in your i18next config
defaultValue: (locale, namespace, key, value) => (value ? value : key),
// Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 4,
// Indentation of the catalog files
keepRemoved: false,
// Keep keys from the catalog that are no longer in code
keySeparator: false,
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
hbs: ["HandlebarsLexer"],
handlebars: ["HandlebarsLexer"],
htm: ["HTMLLexer"],
html: ["HTMLLexer"],
mjs: ["JavascriptLexer"],
js: ["JsxLexer"], // if you're writing jsx inside .mjs files, change this to JsxLexer
ts: ["JavascriptLexer"],
jsx: ["JsxLexer"],
tsx: ["JsxLexer"],
default: ["JavascriptLexer"],
},
lineEnding: "auto",
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ["de", "en", "fr", "it", "ja", "pl", "pt", "ru", "zh"],
// An array of the locales in your applications
namespaceSeparator: false,
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
//output: 'public/translations/$LOCALE/$NAMESPACE.json',
output: "src/translations/$LOCALE/$NAMESPACE.json",
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: "_",
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
input: ["src/App.js", "src/pages/**/*.{js,jsx}", "src/components/**/*.{js,jsx}"],
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: false,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: false,
// Display info about the parsing including some stats
failOnWarnings: false,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
i18nextOptions: null,
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
};
export default config;
================================================
FILE: package.json
================================================
{
"name": "tarkov.dev",
"version": "0.0.0",
"private": true,
"type": "module",
"imports": {
"#src/*": "./src/*",
"#scripts/*": "./scripts/*",
"#public/*": "./public/*"
},
"exports": "./index.js",
"engines": {
"node": ">=20"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"prestart": "npm run prebuild",
"start": "rsbuild start --open",
"build": "rsbuild build",
"test": "rstest",
"prettier": "prettier --write \"src/**/*.{js,jsx,mjs,ts,tsx,json,css,scss,md}\"",
"prebuild": "node scripts/update-props.mjs && node scripts/get-supported-languages.mjs && node scripts/get-contributors.mjs && node scripts/build-sitemap.mjs && node scripts/generate-thumbnails.mjs",
"postbuild": "node scripts/build-redirects.mjs && node scripts/critical.mjs",
"stage": "npx rimraf build && npm run build && npm run preview",
"preview": "npx serve build -l 3001 -s",
"critical": "node scripts/critical.mjs",
"lint": "eslint src",
"prepare": "husky"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@rsbuild/core": "^1.7.3",
"@rsbuild/plugin-react": "^1.4.5",
"@rsbuild/plugin-svgr": "^1.3.0",
"@rstest/core": "^0.9.0",
"@types/react-table": "^7.7.20",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.8",
"eslint-config-react-app": "^7.0.1",
"husky": "^9.1.7",
"lint-staged": "^16.3.2",
"prettier": "^3.8.1",
"sharp": "^0.34.5",
"stylelint": "^17.4.0",
"stylelint-config-standard": "^40.0.0",
"typescript": "^5.9.3"
},
"dependencies": {
"@emotion/styled": "^11.14.1",
"@marsidev/react-turnstile": "^1.4.2",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/lab": "^7.0.0-beta.17",
"@mui/material": "^7.3.9",
"@mui/x-tree-view": "^8.27.2",
"@reduxjs/toolkit": "^2.11.0",
"@tanstack/react-query": "^5.90.21",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"clsx": "^2.1.1",
"critical": "^7.2.0",
"dayjs": "^1.11.19",
"dotenv": "^17.3.1",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.35.0",
"fuse.js": "^7.1.0",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.5",
"intersection-observer": "^0.12.2",
"jsonpath-plus": "^10.4.0",
"leaflet": "^1.9.4",
"leaflet-fullscreen": "^1.0.2",
"lodash.debounce": "^4.0.8",
"lz-string": "^1.5.0",
"react": "^18.3.1",
"react-cookie-consent": "^10.0.1",
"react-countdown": "^2.3.6",
"react-dom": "^18.3.1",
"react-error-boundary": "^6.1.1",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^5.2.4",
"react-i18next": "^15.4.1",
"react-intersection-observer": "^10.0.3",
"react-redux": "^9.2.0",
"react-router-dom": "^6.30.3",
"react-router-hash-link": "^2.4.3",
"react-select": "^5.10.2",
"react-simple-image-viewer": "1.2.2",
"react-spinners": "^0.17.0",
"react-syntax-highlighter": "^16.1.1",
"react-table": "^7.8.0",
"react-zoom-pan-pinch": "^3.7.0",
"resize-observer-polyfill": "^1.5.1",
"source-map-explorer": "^2.5.3",
"victory": "^37.3.6"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest",
"prettier"
],
"ignorePatterns": [
"*.html",
"public/**/*.html",
"build/**/*.html"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"lint-staged": {
"*.css": [
"stylelint --fix",
"prettier --write --ignore-unknown"
],
"*.{js,jsx,mjs,ts,tsx}": [
"eslint --fix",
"prettier --write --ignore-unknown"
]
}
}
================================================
FILE: prettier.config.mjs
================================================
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
printWidth: 120,
singleQuote: false,
trailingComma: "all",
tabWidth: 4,
quoteProps: "consistent",
};
export default config;
================================================
FILE: public/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<TileColor>#2d2d2f</TileColor>
</tile>
</msapplication>
</browserconfig>
================================================
FILE: public/data/.gitignore
================================================
*
!.gitignore
================================================
FILE: public/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Tarkov.dev</title>
<meta
name="description"
content="Checkout all information for items, crafts, barters, maps, loot tiers, hideout profits, trader details, a free API, and more with tarkov.dev! A free, community made, and open source ecosystem of Escape from Tarkov tools and guides."
data-react-helmet="true"
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" data-react-helmet="true" />
<meta property="og:url" content="https://tarkov.dev/" data-react-helmet="true" />
<meta property="og:title" content="Tarkov.dev" data-react-helmet="true" />
<meta
property="og:description"
content="Checkout all information for items, crafts, barters, maps, loot tiers, hideout profits, trader details, a free API, and more with tarkov.dev! A free, community made, and open source ecosystem of Escape from Tarkov tools and guides."
data-react-helmet="true"
/>
<meta
property="og:image"
content="<%= process.env.PUBLIC_URL %>/tarkov-dev-logo.svg"
data-react-helmet="true"
/>
<!-- Twitter -->
<meta property="twitter:card" content="summary" data-react-helmet="true" />
<meta property="twitter:url" content="https://tarkov.dev/" data-react-helmet="true" />
<meta property="twitter:title" content="Tarkov.dev" data-react-helmet="true" />
<meta
property="twitter:description"
content="Checkout all information for items, crafts, barters, maps, loot tiers, hideout profits, trader details, a free API, and more with tarkov.dev! A free, community made, and open source ecosystem of Escape from Tarkov tools and guides."
data-react-helmet="true"
/>
<meta
property="twitter:image"
content="<%= process.env.PUBLIC_URL %>/tarkov-dev-logo.svg"
data-react-helmet="true"
/>
<link
rel="preload"
href="<%= process.env.PUBLIC_URL %>/fonts/bender.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<style>
@font-face {
font-family: 'bender';
font-display: block;
font-style: normal;
font-weight: 400;
src:
local('Bender Regular'),
url('<%= process.env.PUBLIC_URL %>/fonts/bender.woff2') format('woff2');
}
@font-face {
font-family: 'bender';
font-display: swap;
font-style: normal;
font-weight: 700;
src:
local('Bender Bold'),
url('<%= process.env.PUBLIC_URL %>/fonts/bender-bold.woff2') format('woff2');
}
@font-face {
font-family: 'bender';
font-display: swap;
font-style: italic;
font-weight: 400;
src:
local('Bender Italic'),
url('<%= process.env.PUBLIC_URL %>/fonts/bender-italic.woff2') format('woff2');
}
@font-face {
font-family: 'bender';
font-display: swap;
font-style: italic;
font-weight: 700;
src:
local('Bender Bold Italic'),
url('<%= process.env.PUBLIC_URL %>/fonts/bender-bold-italic.woff2') format('woff2');
}
</style>
<link
rel="apple-touch-icon"
sizes="180x180"
href="<%= process.env.PUBLIC_URL %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="<%= process.env.PUBLIC_URL %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="<%= process.env.PUBLIC_URL %>/favicon-16x16.png"
/>
<link rel="mask-icon" href="<%= process.env.PUBLIC_URL %>/safari-pinned-tab.svg" color="#2d2d2f" />
<meta name="apple-mobile-web-app-title" content="Tarkov.dev" />
<meta name="application-name" content="Tarkov.dev" />
<meta name="msapplication-TileColor" content="#2d2d2f" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="theme-color" content="#2d2d2f" />
<!--
site.webmanifest provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="<%= process.env.PUBLIC_URL %>/site.webmanifest" />
<!--
Notice the use of <%= process.env.PUBLIC_URL %> in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "<%= process.env.PUBLIC_URL %>/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="dns-prefetch" href="https://assets.tarkov.dev" />
<link rel="preconnect" href="https://assets.tarkov.dev" crossorigin />
<link rel="dns-prefetch" href="https://api.tarkov.dev" />
<link rel="preconnect" href="https://api.tarkov.dev" crossorigin />
<link rel="dns-prefetch" href="https://status.tarkov.dev" />
<link rel="dns-prefetch" href="https://challenges.cloudflare.com" />
</head>
<body>
<div id="root">
<noscript>
<div
style="
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: center;
"
>
<a href="/">
<img
alt="Tarkov.dev"
width="300"
height="48"
loading="lazy"
src="<%= process.env.PUBLIC_URL %>/tarkov-dev-logo.svg"
/>
</a>
<h1>tarkov.dev is an open source tool kit for Escape from Tarkov.</h1>
<h2>
It is designed and maintained by the community to help you with quests, flea
market trading, and improving your game!
</h2>
<h2>
The API is also freely available for you to build your own tools and
services related to EFT.
</h2>
<h3 style="color: red">
Enable JavaScript and start using tarkov.dev right now!
</h3>
</div>
</noscript>
</div>
</body>
</html>
================================================
FILE: public/robots.txt
================================================
User-agent: *
Allow: /
Sitemap: https://tarkov.dev/sitemap_index.xml
================================================
FILE: public/site.webmanifest
================================================
{
"name": "Tarkov.dev",
"short_name": "Tarkov.dev",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#2d2d2f",
"background_color": "#2d2d2f",
"start_url": "https://tarkov.dev",
"display": "standalone"
}
================================================
FILE: rsbuild.config.ts
================================================
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginSvgr } from "@rsbuild/plugin-svgr";
import { execSync } from "node:child_process";
const commitHash = execSync("git rev-parse --short HEAD").toString().trim();
export default defineConfig({
plugins: [pluginReact(), pluginSvgr({ mixedImport: true })],
server: {
base: process.env.PUBLIC_URL,
},
source: {
define: {
"process.env.RSTEST": process.env.RSTEST,
"__COMMIT_HASH__": JSON.stringify(commitHash),
},
},
html: {
template: "./public/index.html",
},
output: {
distPath: {
root: "build",
},
},
});
================================================
FILE: rstest.config.ts
================================================
import { defineConfig } from "@rstest/core";
import rsbuildConfig from "./rsbuild.config";
export default defineConfig({
...rsbuildConfig,
testEnvironment: "jsdom",
globals: true,
setupFiles: ["./rstest.setup.ts"],
});
================================================
FILE: rstest.setup.ts
================================================
import "dotenv/config";
import "@testing-library/jest-dom";
import { resetIntersectionMocking, setupIntersectionMocking } from "react-intersection-observer/test-utils";
import { afterEach, beforeEach, rstest } from "@rstest/core";
beforeEach(() => {
setupIntersectionMocking(rstest.fn);
});
afterEach(() => {
resetIntersectionMocking();
});
================================================
FILE: scripts/build-redirects.mjs
================================================
import fs from "fs";
import path from "path";
import url from "url";
(async () => {
let redirects;
try {
redirects = await fetch("https://manager.tarkov.dev/data/redirects.json").then((response) => response.json());
} catch (redirectsError) {
console.error(redirectsError);
process.exit(1);
}
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
let indexTemplate = fs.readFileSync(path.join(__dirname, "..", "workers-site", "index-template.js"), "utf8");
indexTemplate = indexTemplate.replace("REDIRECTS_DATA", JSON.stringify(redirects, null, 4));
console.time("Write new data");
fs.writeFileSync(path.join(__dirname, "..", "workers-site", "index.js"), indexTemplate);
console.timeEnd("Write new data");
})();
================================================
FILE: scripts/build-sitemap.mjs
================================================
import { writeFileSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { createGzip } from "zlib";
import { pipeline } from "stream";
import { createReadStream, createWriteStream, unlink } from "fs";
import maps from "../src/data/maps.json" with { type: "json" };
import categoryPages from "../src/data/category-pages.json" with { type: "json" };
import { caliberArrayWithSplit } from "../src/modules/format-ammo.mjs";
//import apiRequest from "../src/modules/api-request.mjs";
const standardPaths = [
"",
"/ammo",
"/barters",
"/hideout-profit",
"/loot-tier",
"/trader/prapor",
"/trader/therapist",
"/trader/skier",
"/trader/fence",
"/trader/peacekeeper",
"/trader/mechanic",
"/trader/ragman",
"/trader/jaeger",
"/trader/lightkeeper",
"/wipe-length",
"/bitcoin-farm-calculator",
];
const standardPathsWeekly = [
"/about",
"/api",
"/api-users",
"/control",
"/items",
"/maps",
"/moobot",
"/nightbot",
"/settings",
"/streamelements",
"/traders",
"/bosses",
"/tasks",
"/hideout",
];
const languages = ["de", "en", "fr", "it", "ja", "pl", "pt", "ru"];
const addPath = (sitemap, url, change = "hourly") => {
for (const lang in languages) {
sitemap = `${sitemap}
<url>`;
if (Object.hasOwnProperty.call(languages, lang)) {
const loclang = languages[lang];
if (loclang === "en") {
sitemap = `${sitemap}
<loc>https://tarkov.dev${url}</loc>`;
} else {
sitemap = `${sitemap}
<loc>https://tarkov.dev${url}?lng=${loclang}</loc>`;
}
for (const lang in languages) {
if (Object.hasOwnProperty.call(languages, lang)) {
const hreflang = languages[lang];
if (hreflang === "en") {
sitemap = `${sitemap}
<xhtml:link rel="alternate" hreflang="${hreflang}" href="https://tarkov.dev${url}"/>`;
} else {
sitemap = `${sitemap}
<xhtml:link rel="alternate" hreflang="${hreflang}" href="https://tarkov.dev${url}?lng=${hreflang}"/>`;
}
}
}
}
sitemap = `${sitemap}
<changefreq>${change}</changefreq>
</url>`;
}
return sitemap;
};
const apiRequest = (path) => {
return fetch(`https://json.tarkov.dev/${path}`, {
cache: "no-cache",
headers: {
Accept: "application/json",
},
}).then((response) => {
if (!response.ok) {
return Promise.reject(new Error(`${response.status} ${response.statusText}`));
}
return response.json().then((json) => json.data);
});
};
async function build_sitemap() {
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">`;
for (const path of standardPaths) {
sitemap = addPath(sitemap, path);
}
for (const path of standardPathsWeekly) {
sitemap = addPath(sitemap, path, "weekly");
}
for (const mapsGroup of maps) {
for (const map of mapsGroup.maps) {
sitemap = addPath(sitemap, `/map/${map.key}`, "weekly");
}
}
const itemResponse = await apiRequest("regular/items");
const itemCategories = Object.values(itemResponse.itemCategories);
for (const itemCategory of itemCategories) {
sitemap = addPath(sitemap, `/items/${itemCategory.normalizedName}`);
}
const itemHandbookCategories = Object.values(itemResponse.handbookCategories);
for (const itemCategory of itemHandbookCategories) {
sitemap = addPath(sitemap, `/items/handbook/${itemCategory.normalizedName}`);
}
for (const categoryPage of categoryPages) {
sitemap = addPath(sitemap, `/items/${categoryPage.key}`);
}
const mapsResponse = await apiRequest("regular/maps");
const allBosses = Object.values(mapsResponse.mobs);
for (const boss of allBosses) {
sitemap = addPath(sitemap, `/boss/${boss.normalizedName}`);
}
const tasksResponse = await apiRequest("regular/tasks");
const allTasks = Object.values(tasksResponse.tasks);
for (const task of allTasks) {
sitemap = addPath(sitemap, `/task/${task.normalizedName}`, "weekly");
}
const ammoTypes = caliberArrayWithSplit();
for (const ammoType of ammoTypes) {
sitemap = addPath(sitemap, `/ammo/${ammoType.replace(/ /g, "%20")}`);
}
sitemap = `${sitemap}
</urlset>`;
const __dirname = fileURLToPath(new URL(".", import.meta.url));
writeFileSync(path.join(__dirname, "..", "public", "sitemap.xml"), sitemap);
}
async function build_sitemap_items() {
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">`;
const itemsResponse = await apiRequest("regular/items");
const allItems = Object.values(itemsResponse.items);
for (const item of allItems) {
sitemap = addPath(sitemap, `/item/${item.normalizedName}`);
}
sitemap = `${sitemap}
</urlset>`;
const __dirname = fileURLToPath(new URL(".", import.meta.url));
writeFileSync(path.join(__dirname, "..", "public", "sitemap_items.xml"), sitemap);
const gzip = createGzip();
const source = createReadStream(path.join(__dirname, "..", "public", "sitemap_items.xml"));
const destination = createWriteStream(path.join(__dirname, "..", "public", "sitemap_items.xml.gz"));
pipeline(source, gzip, destination, (err) => {
if (err) {
console.error("An error occurred:", err);
process.exitCode = 1;
}
unlink(path.join(__dirname, "..", "public", "sitemap_items.xml"), (err) => {
if (err) {
throw err;
}
console.log("successfully deleted sitemap_items.xml");
});
});
}
async function build_sitemap_index() {
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://tarkov.dev/sitemap.xml</loc>
</sitemap>
<sitemap>
<loc>https://tarkov.dev/sitemap_items.xml.gz</loc>
</sitemap>
</sitemapindex>`;
const __dirname = fileURLToPath(new URL(".", import.meta.url));
writeFileSync(path.join(__dirname, "..", "public", "sitemap_index.xml"), sitemap);
}
(async () => {
try {
console.time("build-sitemap");
await build_sitemap();
await build_sitemap_items();
await build_sitemap_index();
console.timeEnd("build-sitemap");
} catch (error) {
console.error(error);
console.log("trying to use pre-built sitemap (offline mode?)");
}
})();
================================================
FILE: scripts/critical.mjs
================================================
import * as critical from "critical";
critical.generate(
{
base: "build/",
src: "./index.html",
inline: true,
css: ["build/static/css/*.css"],
target: "index.html",
},
(err, output) => {
if (err) {
console.error(err);
} else if (output) {
console.log("Generated critical CSS");
}
},
);
================================================
FILE: scripts/custom-loader.mjs
================================================
import { URL } from "url";
import { readFile } from "fs/promises";
/**
* This function forces .mjs files to be loades as ES modules
* so the default export is a string containing the CSS stylesheet.
*/
export async function load(url, context, defaultLoad) {
if (context.format !== "commonjs") {
return defaultLoad(url, context, defaultLoad);
}
const forceConvert = [
"do-fetch-items.mjs",
"do-fetch-barters.mjs",
"do-fetch-crafts.mjs",
"do-fetch-hideout.mjs",
"do-fetch-maps.mjs",
"do-fetch-meta.mjs",
"do-fetch-traders.mjs",
"do-fetch-quests.mjs",
"do-fetch-bosses.mjs",
"flea-market-fee.mjs",
"camelcase-to-dashes.mjs",
"graphql-request.mjs",
"api-query.mjs",
];
for (const fileName of forceConvert) {
if (url.endsWith(fileName)) {
const content = await readFile(new URL(url));
return {
format: "module",
source: content,
shortCircuit: true,
};
}
}
return defaultLoad(url, context, defaultLoad);
}
================================================
FILE: scripts/generate-thumbnails.mjs
================================================
import fs from "fs/promises";
import sharp from "sharp";
(async () => {
console.time("Generating thumbnails");
// Max height from css ".map-wrapper img"
const maxHeight = 200;
const mapsPath = "./public/maps/";
const files = await fs.readdir(mapsPath);
for (const fileName of files) {
if (!fileName.endsWith(".jpg")) {
continue;
}
if (fileName.endsWith("_thumb.jpg")) {
continue;
}
const thumbName = fileName.replace(".jpg", "_thumb.jpg");
console.log(`Generating ${thumbName}`);
const image = sharp(mapsPath + fileName)
.resize(null, maxHeight)
.jpeg({ mozjpeg: true, quality: 90 });
await image.toFile(mapsPath + thumbName);
}
/*const mapGroups = JSON.parse(await fs.readFile('./src/data/maps.json'));
for (const group of mapGroups) {
for (const map of group.maps) {
if (map.projection !== 'interactive')
continue;
let path = map.tilePath || map.svgPath || `https://assets.tarkov.dev/maps/${group.normalizedName}/{z}/{x}/{y}.png`;
path = path.replace(/{[xyz]}/g, '0');
const thumbName = `${group.normalizedName}_thumb.jpg`;
try {
const imageRequest = await fetch(path);
console.log(`Generating ${thumbName}`);
const image = sharp(await imageRequest.arrayBuffer()).trim('#00000000').resize(null, maxHeight).jpeg({mozjpeg: true, quality: 90});
await image.toFile(mapsPath+thumbName);
if (map.altMaps) {
for (const altKey of map.altMaps) {
await image.toFile(mapsPath+`${altKey}_thumb.jpg`);
}
}
} catch (error) {
console.error(error)
console.log(`Asset for ${thumbName} unavailable`)
}
}
}*/
console.timeEnd("Generating thumbnails");
})();
================================================
FILE: scripts/generate_api-users_thumbs_macOS.sh
================================================
#!/bin/bash
# Max height from css ".api-users-page-wrapper img"
HEIGHT=150
cd ../public/images/api-users/
# Remove old thumbs
rm *_thumb.jpg
rm *_thumb.png
for IMAGE in ./*.jpg ./*.png; do
ORIG_HEIGHT=$(sips -g pixelHeight "$IMAGE" | grep -o '[0-9]*$')
# New name for the thumb
ORIGINAL=$(basename "$IMAGE")
EXTENSION="${ORIGINAL##*.}"
FILENAME="${ORIGINAL%.*}"
NEW_FILENAME="./${FILENAME}_thumb.${EXTENSION}"
if [[ $ORIG_HEIGHT -le $HEIGHT ]]
then
#copy the original
cp "$IMAGE" "$NEW_FILENAME"
else
#resizing to max height
sips --resampleHeight $HEIGHT "$IMAGE" --out "$NEW_FILENAME"
fi
done
================================================
FILE: scripts/generate_items_thumbs.mjs
================================================
import fs from "fs";
import path from "path";
import sharp from "sharp";
import { exit } from "process";
//import apiRequest from "../src/modules/api-request.mjs";
import categoryPages from "../src/data/category-pages.json" with { type: "json" };
const ignoredCategories = [
"headsets",
"helmets",
"glasses",
"armors",
"rigs",
"backpacks",
"guns",
// 'mods',
"pistol-grips",
"suppressors",
"grenades",
"containers",
"barter-items",
"keys",
"provisions",
];
const apiRequest = (path) => {
return fetch(`https://json.tarkov.dev/${path}`, {
cache: "no-cache",
headers: {
Accept: "application/json",
},
}).then((response) => {
if (!response.ok) {
return Promise.reject(new Error(`${response.status} ${response.statusText}`));
}
return response.json().then((json) => json.data);
});
};
function shuffle(array) {
let currentIndex = array.length;
let randomIndex;
// While there remain elements to shuffle.
while (currentIndex > 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array;
}
(async () => {
try {
console.time("Generating thumbnails");
const maxWidth = 256;
const maxHeight = 144;
const mapsPath = "./public/images/items/";
const allItems = await apiRequest("regular/items");
for (const categoryPage of categoryPages) {
if (ignoredCategories.includes(categoryPage.key)) {
continue;
}
const originalImg = await sharp(mapsPath + categoryPage.key + "-table.png");
const metadata = await originalImg.metadata();
const scale = metadata.width / maxWidth;
const cropHeight = Math.ceil(scale * maxHeight);
const itemWidth = metadata.width / 2;
const itemHeight = cropHeight / 2;
const croppedImage = originalImg.extract({ left: 0, top: 0, width: metadata.width, height: cropHeight });
// const croppedImage = await sharp({
// create: {
// width: metadata.width,
// height: cropHeight,
// channels: 4,
// background: { r: 45, g: 45, b: 47, alpha: 1.0 }
// }
// }).png();
const items = Object.values(allItems.items).filter((i) => i.types.includes(categoryPage.type));
shuffle(items);
const itemResize = {
width: itemWidth,
height: itemHeight,
fit: sharp.fit.contain,
background: { r: 255, g: 255, b: 255, alpha: 0.0 },
};
const itemRotate = 0; //7 + Math.random()*4-2;
const tlImageFetch = await fetch(items[0].image512pxLink);
const tlImageBuffer = await tlImageFetch.buffer();
const tlImage = await sharp(tlImageBuffer).resize(itemResize).rotate(-itemRotate).toBuffer();
const trImageFetch = await fetch(items[1].image512pxLink);
const trImageBuffer = await trImageFetch.buffer();
const trImage = await sharp(trImageBuffer).resize(itemResize).rotate(itemRotate).toBuffer();
const blImageFetch = await fetch(items[2].image512pxLink);
const blImageBuffer = await blImageFetch.buffer();
const blImage = await sharp(blImageBuffer).resize(itemResize).rotate(itemRotate).toBuffer();
const brImageFetch = await fetch(items[3].image512pxLink);
const brImageBuffer = await brImageFetch.buffer();
const brImage = await sharp(brImageBuffer).resize(itemResize).rotate(-itemRotate).toBuffer();
const cImageFetch = await fetch(items[4].image512pxLink);
const cImageBuffer = await cImageFetch.buffer();
const cImage = await sharp(cImageBuffer).resize(itemResize).toBuffer();
const composedImage = await croppedImage
.blur(6)
.composite([
{ input: tlImage, gravity: "northwest", blend: "over" },
{ input: trImage, gravity: "northeast", blend: "over" },
{ input: blImage, gravity: "southwest", blend: "over" },
{ input: brImage, gravity: "southeast", blend: "over" },
{ input: cImage, gravity: "centre", blend: "over" },
])
.toBuffer();
const finalImage = await sharp(composedImage)
.resize(maxWidth, maxHeight)
.jpeg({ mozjpeg: true, quality: 100 });
// const finalImage = await sharp(composedImage).jpeg({mozjpeg: true, quality: 90});
await finalImage.toFile(mapsPath + categoryPage.key + "-table_thumb.jpg");
console.log(`Generated thumbnail for ${categoryPage.key}`);
// return;
}
console.timeEnd("Generating thumbnails");
} catch (error) {
console.error(error);
console.log("error generating thumbnail (offline mode?)");
}
})();
================================================
FILE: scripts/generate_items_thumbs_macOS.sh
================================================
#!/bin/bash
# Max height and width
HEIGHT=144
WIDTH=256
cd ../public/images/items/
# Remove old thumbs
rm *_thumb.jpg
rm *_thumb.png
for IMAGE in ./*.png
do
ORIG_HEIGHT=$(sips -g pixelHeight "$IMAGE" | grep -o '[0-9]*$')
ORIG_WIDTH=$(sips -g pixelWidth "$IMAGE" | grep -o '[0-9]*$')
# New name for the thumb
ORIGINAL=$(basename "$IMAGE")
EXTENSION="${ORIGINAL##*.}"
FILENAME="${ORIGINAL%.*}"
NEW_FILENAME="./${FILENAME}_thumb.jpg"
# Resizing to max height
sips -s format jpeg -s formatOptions 70 --resampleWidth $WIDTH "$IMAGE" --out "$NEW_FILENAME"
# Leave here as a reminder:
# Without cropOffset it crops at center, but with 0 0 it also crops at center.
# But if you try to offset from center with something like this:
#RESIZED_HEIGHT=$(sips -g pixelHeight "$NEW_FILENAME" | grep -o '[0-9]*$')
#CROP_OFFSET_Y=$(($HEIGHT - $RESIZED_HEIGHT))
# it now crops from top-left...
# So a true crop from top-left is impossible, you always need to lose at least one pixel...
sips --cropOffset 1 0 --cropToHeightWidth $HEIGHT $WIDTH "$NEW_FILENAME" --out "$NEW_FILENAME"
done
================================================
FILE: scripts/generate_known_icons_macOS.sh
================================================
#!/bin/bash
# Max height and width
HEIGHT=64
WIDTH=64
CWD="$(pwd)"
cd ../public/images/traders/
# Remove old icons
rm *-icon.jpg
for IMAGE in ./*portrait.png
do
ORIG_HEIGHT=$(sips -g pixelHeight "$IMAGE" | grep -o '[0-9]*$')
ORIG_WIDTH=$(sips -g pixelWidth "$IMAGE" | grep -o '[0-9]*$')
# New name for the icon
ORIGINAL=$(basename "$IMAGE")
EXTENSION="${ORIGINAL##*.}"
FILENAME="${ORIGINAL%.*}"
FILENAME="${FILENAME//-portrait/}"
NEW_FILENAME="./$FILENAME-icon.jpg"
# Resizing to max height
sips -s format jpeg -s formatOptions 100 --resampleWidth $WIDTH "$IMAGE" --out "$NEW_FILENAME"
done
cd $CWD
cd ../public/images/bosses/
# Remove old icons
rm *-icon.jpg
for IMAGE in ./*portrait.png
do
ORIG_HEIGHT=$(sips -g pixelHeight "$IMAGE" | grep -o '[0-9]*$')
ORIG_WIDTH=$(sips -g pixelWidth "$IMAGE" | grep -o '[0-9]*$')
# New name for the icon
ORIGINAL=$(basename "$IMAGE")
EXTENSION="${ORIGINAL##*.}"
FILENAME="${ORIGINAL%.*}"
FILENAME="${FILENAME//-portrait/}"
NEW_FILENAME="./$FILENAME-icon.jpg"
# Resizing to max height
sips -s format jpeg -s formatOptions 100 --resampleWidth $WIDTH "$IMAGE" --out "$NEW_FILENAME"
done
================================================
FILE: scripts/get-contributors.mjs
================================================
import fs from "fs";
import path from "path";
import url from "url";
const repositories = [
"the-hideout/tarkov-dev",
"the-hideout/stash",
"the-hideout/tarkov-api",
"the-hideout/cloudflare",
"the-hideout/tarkov-data-manager",
"the-hideout/cache",
"the-hideout/status",
"the-hideout/tarkov-dev-image-generator",
"the-hideout/tarkov-dev-svg-maps",
];
// If a GitHub token is provided, use it to increase the rate limit
const token = process.env.GITHUB_TOKEN;
const headers = {};
if (token) {
headers.authorization = `token ${token}`;
console.log("Using provided GitHub token to increase rate limit");
} else {
console.log("No GitHub token provided, rate limit may be reached");
console.warn("To increase the rate limit, provide a GitHub token via the GITHUB_TOKEN environment variable");
}
async function getContributors(repository) {
console.time(`get contributors to ${repository}`);
let responseJson = [];
let contributors = [];
try {
const response = await fetch(`https://api.github.com/repos/${repository}/contributors`, {
headers,
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
console.error(`Error! status: ${response.status} message: ${response.statusText}`);
} else {
responseJson = await response.json();
}
for (const contributor of responseJson) {
if (contributor.type !== "User") {
continue;
}
contributors.push({
login: contributor.login,
html_url: contributor.html_url,
avatar_url: contributor.avatar_url,
contributions: contributor.contributions,
});
}
} catch (responseError) {
console.error(`Error: ${responseError.message}`);
}
console.timeEnd(`get contributors to ${repository}`);
return contributors;
}
(async () => {
console.log("Loading contributors");
let allContributors = [];
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const contributorsPath = path.join(__dirname, "..", "src", "data", "contributors.json");
try {
let allRepContributors = [];
for (const repository of repositories) {
const contributosArr = await getContributors(repository);
if (!contributosArr) {
console.log(`Error fetching contributors of ${repository}`);
continue;
}
allRepContributors.push(...contributosArr);
}
// Calculate total contributions by user
const totalRepContributors = allRepContributors.reduce((acc, { login, contributions }) => {
if (!acc[login]) {
acc[login] = 0;
}
acc[login] += contributions;
return acc;
}, {});
// Add total contributions field to each object
allContributors = Object.entries(totalRepContributors)
.map(([login, totalContributions]) => {
const { html_url, avatar_url } = allRepContributors.find((contributor) => contributor.login === login);
return {
login,
html_url,
avatar_url,
totalContributions,
};
})
.sort((a, b) => {
let compare = b.totalContributions - a.totalContributions;
if (compare !== 0) {
return compare;
}
return a.login.localeCompare(b.login);
});
} catch (error) {
// If we're running in CI and a failure occurs, use fallback data for contributors
if (process.env.CI === "true") {
console.log(`error in CI: ${error}`);
} else {
console.log(`error fetching contributors: ${error}`);
}
}
if (allContributors.length === 0) {
console.log("using fallback contributors.json (offline mode?)");
try {
const existing = JSON.parse(fs.readFileSync(contributorsPath, "utf-8"));
if (Array.isArray(existing) && existing.length > 0) {
allContributors = existing;
}
} catch (readError) {
console.warn(`Unable to read existing contributors.json fallback: ${readError}`);
}
if (allContributors.length === 0) {
allContributors = [
{
login: "hideout-bot",
html_url: "https://github.com/hideout-bot",
avatar_url: "https://avatars.githubusercontent.com/u/121582168?v=4",
totalContributions: 9000,
},
];
}
} else {
console.log(`Total contributors: ${allContributors.length}`);
console.time("Write new data");
const stringifyed = JSON.stringify(allContributors, null, 4);
fs.writeFileSync(contributorsPath, stringifyed);
console.timeEnd("Write new data");
}
})();
================================================
FILE: scripts/get-supported-languages.mjs
================================================
import fs from "fs";
import apiRequest from "../src/modules/api-request.mjs";
try {
const endpoints = await apiRequest("endpoints");
const allLangs = endpoints.languages;
fs.writeFileSync("./src/data/supported-languages.json", JSON.stringify(allLangs, null, 4));
} catch (error) {
if (process.env.CI) {
throw error;
} else {
console.log(error);
console.log("attempting to get supported languages (offline mode?)");
}
}
================================================
FILE: scripts/test-redirects.mjs
================================================
import fs from "fs";
import path from "path";
import url from "url";
import redirects from "../workers-site/redirects.json";
(async () => {
let liveNames = [];
try {
const response = await fetch("https://json.tarkov.dev/regular/items", {
method: "POST",
cache: "no-store",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({
query: `{
itemsByType(type: any){
normalizedName
}
}`,
}),
}).then((response) => response.json());
liveNames = Object.values(response.data.items).map((item) => item.normalizedName);
} catch (loadError) {
console.error(loadError);
return false;
}
const keys = Object.keys(redirects);
for (const key of keys) {
const itemName = key.replace("/item/", "");
if (!liveNames.includes(itemName)) {
continue;
}
console.log(`${key} `);
Reflect.deleteProperty(redirects, key);
}
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
fs.writeFileSync(path.join(__dirname, "..", "workers-site", "redirects.json"), JSON.stringify(redirects, null, 4));
})();
================================================
FILE: scripts/update-props.mjs
================================================
import fs from "fs";
import path from "path";
import url from "url";
const files = [
//'item-props',
"item-grids",
//'globals',
//'item_presets',
];
for (const file of files) {
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const props = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "src", "data", `${file}.json`)));
fs.writeFileSync(path.join(__dirname, "..", "public", "data", `${file}.min.json`), JSON.stringify(props));
}
================================================
FILE: src/App.css
================================================
@import url("./styles/singleEntity.css");
@import url("./styles/mapSearch.css");
/* Variables start */
:root {
--color-black: #000;
--color-black-light: #1b1919;
--color-white: #fff;
--color-gunmetal: #383945;
--color-gunmetal-dark: #2d2d2f;
--color-gold-one: #c7c5b3;
--color-gold-two: #9a8866;
--color-blue-light: #0292c0;
--color-gray: #424242;
--color-gray-light: #636363;
--color-green: #00a700;
--color-green-light: #6a9a66;
--color-orange: #ca8a00;
--color-red: #cd1e2f;
--color-red-light: #9a6666;
--color-purple: #8c6edf;
--color-yellow: #ffe084;
--color-yellow-light: #e0dfd6;
}
.desc-line-break {
margin-top: 2rem;
margin-bottom: 2rem;
width: 50%;
}
body {
background-color: var(--color-gunmetal-dark);
background-image: url("images/background-1.png");
color: var(--color-gold-one);
margin: 0;
padding: 0;
height: 100%;
font-family:
"bender",
-apple-system,
system-ui,
BlinkMacSystemFont,
"Segoe UI",
"Roboto",
"Helvetica Neue",
"Arial",
sans-serif;
font-style: normal;
font-weight: 400;
font-size: 16px;
word-spacing: 1px;
text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
code {
font-family: "source-code-pro", "Menlo", "Monaco", "Consolas", "Courier New", monospace;
}
body * {
box-sizing: border-box;
}
iframe {
border: 0;
height: 100%;
width: 100%;
}
input,
select,
button {
border-radius: 0;
font-family: inherit;
}
button,
input[type="submit"] {
background-color: var(--color-gold-two);
border: 0;
color: var(--color-black);
height: 40px;
padding: 0;
}
input[type="text"],
input[type="number"] {
padding: 12px;
max-height: 40px;
border: 2px solid var(--color-gold-two);
background-color: var(--color-gunmetal-dark);
color: var(--color-gold-one);
}
input[type="text"]:focus,
input[type="number"]:focus {
outline: none;
border: 2px solid var(--color-gold-one);
}
input[type="text"].number {
width: 80px;
}
input[name="session-id"] {
padding-left: 20px;
}
select {
padding: 10px;
margin-bottom: 2vh;
}
a {
color: var(--color-gold-two);
text-decoration: none;
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
cite {
display: block;
font-size: 14px;
}
.display-wrapper {
height: var(--display-height);
min-height: 80vh;
margin: 0 10px;
position: relative;
}
.page-wrapper {
margin: 0 10px;
max-width: 1200px;
/* min-height: 80vh; */
}
.updated-label {
color: var(--color-gold-one);
font-size: 10px;
left: 4px;
position: absolute;
top: 2px;
}
.time-wrapper {
background-color: rgb(from var(--color-black) r g b / 0.5);
padding: 10px 20px;
position: absolute;
right: -10px;
top: 0;
z-index: 1005;
text-align: right;
}
.time-wrapper a {
color: inherit;
}
.time-wrapper div {
font-family: "source-code-pro", "Menlo", "Monaco", "Consolas", "Courier New", monospace;
}
.map-image-wrapper {
height: var(--display-height);
width: 98vw;
display: flex;
align-items: center;
justify-content: center;
}
.map-image {
max-height: 100%;
max-width: 100%;
object-fit: contain;
}
.icon-with-text {
vertical-align: middle;
margin-right: 8px;
margin-left: 8px;
}
.icon-with-text-hidden {
visibility: hidden;
}
.center-title {
text-align: center;
}
.screen-link {
margin-left: 8px;
margin-right: 8px;
display: flex;
flex-direction: column;
align-items: center;
}
.screen-link-icon {
vertical-align: middle;
margin-right: 8px;
}
.price-wrapper {
color: var(--color-gold-one);
font-size: 14px;
}
.price-wrapper-tool {
color: var(--color-blue-light);
font-size: 14px;
}
.page-headline-wrapper {
display: flex;
align-items: center;
max-width: 1200px;
margin: auto;
white-space: nowrap;
}
.page-headline-wrapper h1 {
flex-grow: 1;
text-align: center;
white-space: initial;
display: inline-flex;
}
.wiki-link-wrapper {
font-size: larger;
}
.hr-muted {
margin-top: 2rem;
border-bottom: 1px solid var(--color-gold-two);
width: 80%;
opacity: 0.4;
}
.hr-muted-full {
margin-top: 2rem;
border-bottom: 1px solid var(--color-gold-two);
width: 100%;
opacity: 0.4;
}
/* Global reuseable styles, specific to the Tarkov.Dev style */
.information-section {
background: rgb(from var(--color-black) r g b / 0.1);
border: 1px solid rgb(from var(--color-white) r g b/ 0.1);
border-radius: 0 20px 0 20px;
margin-bottom: 50px;
overflow: hidden;
}
.information-section.has-table {
border-radius: 0 20px 0 0;
}
.information-section h2 {
display: flex;
align-items: center;
font-size: 24px;
margin: 0;
padding: 15px 20px;
color: var(--color-gold-one);
background: rgb(from var(--color-black) r g b / 0.3);
border-bottom: 1px solid rgb(from var(--color-white) r g b / 0.1);
}
.information-section h2 svg {
width: 1.6rem !important;
height: auto !important;
margin: 0 12px 0 0;
}
.information-section .content {
padding: 20px;
}
.information-section .content p {
margin: 0;
}
.filter-wrapper.open {
z-index: 2;
}
.level-locked {
color: var(--color-red);
}
@media screen and (width >= 800px) {
.control-wrapper {
display: none;
}
.page-headline-wrapper h1 {
flex-grow: 1;
text-align: left;
}
}
@media screen and (width >= 1280px) {
.page-wrapper {
margin: 0 auto;
}
}
@media screen and (width >= 1920px) {
.page-wrapper {
margin: 0 auto;
max-width: 1600px;
}
}
================================================
FILE: src/App.jsx
================================================
/* eslint-disable no-restricted-globals */
import React, { useEffect, useCallback, useRef, Suspense } from "react";
import { Routes, Route, useNavigate, Navigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useDispatch, useSelector } from "react-redux";
import CookieConsent from "react-cookie-consent";
import { ErrorBoundary } from "react-error-boundary";
import { ThemeProvider } from "@mui/material/styles";
import "./App.css";
import theme from "./modules/mui-theme.mjs";
import i18n from "./i18n.js";
import loadPolyfills from "./modules/polyfills.js";
import RemoteControlId from "./components/remote-control-id/index.jsx";
import { fetchTarkovTrackerProgress, setPlayerPosition } from "./features/settings/settingsSlice.mjs";
import { setConnectionStatus, enableConnection } from "./features/sockets/socketsSlice.js";
import useStateWithLocalStorage from "./hooks/useStateWithLocalStorage.jsx";
import makeID from "./modules/make-id.js";
import WindowFocusHandler from "./modules/window-focus-handler.mjs";
import RemoteWebSocket from "./modules/remote-websocket.mjs";
import Loading from "./components/loading/index.jsx";
import supportedLanguages from "./data/supported-languages.json";
import Menu from "./components/menu/index.jsx";
import Footer from "./components/footer/index.tsx";
const Map = React.lazy(() => import("./pages/map/index.jsx"));
const ErrorPage = React.lazy(() => import("./pages/error-page/index.jsx"));
const Debug = React.lazy(() => import("./components/Debug.jsx"));
const Ammo = React.lazy(() => import("./pages/ammo/index.jsx"));
const Control = React.lazy(() => import("./pages/control/index.jsx"));
const LootTiers = React.lazy(() => import("./pages/loot-tiers/index.jsx"));
const Barters = React.lazy(() => import("./pages/barters/index.jsx"));
const Maps = React.lazy(() => import("./pages/maps/index.jsx"));
const Crafts = React.lazy(() => import("./pages/crafts/index.jsx"));
const Item = React.lazy(() => import("./pages/item/index.jsx"));
const Start = React.lazy(() => import("./pages/start/index.jsx"));
const Settings = React.lazy(() => import("./pages/settings/index.jsx"));
const Nightbot = React.lazy(() => import("./pages/nightbot/index.jsx"));
const StreamElements = React.lazy(() => import("./pages/stream-elements/index.jsx"));
const ApiUsers = React.lazy(() => import("./pages/api-users/index.jsx"));
const Moobot = React.lazy(() => import("./pages/moobot/index.jsx"));
const Items = React.lazy(() => import("./pages/items/index.jsx"));
const Armors = React.lazy(() => import("./pages/items/armors/index.jsx"));
const Backpacks = React.lazy(() => import("./pages/items/backpacks/index.jsx"));
const BarterItems = React.lazy(() => import("./pages/items/barter-items/index.jsx"));
const Containers = React.lazy(() => import("./pages/items/containers/index.jsx"));
const Glasses = React.lazy(() => import("./pages/items/glasses/index.jsx"));
const Grenades = React.lazy(() => import("./pages/items/grenades/index.jsx"));
const Guns = React.lazy(() => import("./pages/items/guns/index.jsx"));
const Headsets = React.lazy(() => import("./pages/items/headsets/index.jsx"));
const Helmets = React.lazy(() => import("./pages/items/helmets/index.jsx"));
const Keys = React.lazy(() => import("./pages/items/keys/index.jsx"));
const Mods = React.lazy(() => import("./pages/items/mods/index.jsx"));
const PistolGrips = React.lazy(() => import("./pages/items/pistol-grips/index.jsx"));
const Provisions = React.lazy(() => import("./pages/items/provisions/index.jsx"));
const Rigs = React.lazy(() => import("./pages/items/rigs/index.jsx"));
const Suppressors = React.lazy(() => import("./pages/items/suppressors/index.jsx"));
const BsgCategory = React.lazy(() => import("./pages/items/bsg-category/index.jsx"));
const HandbookCategory = React.lazy(() => import("./pages/items/handbook-category/index.jsx"));
const BitcoinFarmCalculator = React.lazy(() => import("./pages/bitcoin-farm-calculator/index.jsx"));
const Quests = React.lazy(() => import("./pages/quests/index.jsx"));
const Quest = React.lazy(() => import("./pages/quest/index.jsx"));
const Prestiges = React.lazy(() => import("./pages/prestige/list.jsx"));
const Prestige = React.lazy(() => import("./pages/prestige/index.jsx"));
const Bosses = React.lazy(() => import("./pages/bosses/index.jsx"));
const Boss = React.lazy(() => import("./pages/boss/index.jsx"));
const Traders = React.lazy(() => import("./pages/traders/index.jsx"));
const Trader = React.lazy(() => import("./pages/trader/index.jsx"));
const ItemTracker = React.lazy(() => import("./pages/item-tracker/index.jsx"));
const Hideout = React.lazy(() => import("./pages/hideout/index.jsx"));
const WipeLength = React.lazy(() => import("./pages/wipe-length/index.jsx"));
const Achievements = React.lazy(() => import("./pages/achievements/index.jsx"));
const Players = React.lazy(() => import("./pages/players/index.jsx"));
const Player = React.lazy(() => import("./pages/player/index.jsx"));
const PlayerForward = React.lazy(() => import("./pages/player/player-forward.jsx"));
const Converter = React.lazy(() => import("./pages/converter/index.jsx"));
const About = React.lazy(() => import("./pages/about/index.jsx"));
const OtherTools = React.lazy(() => import("./pages/other-tools/index.jsx"));
const TarkovMonitorPage = React.lazy(() => import("./pages/tarkov-monitor/index.js"));
const StashBotPage = React.lazy(() => import("./pages/stash-bot/index.js"));
const APIDocs = React.lazy(() => import("./pages/api-docs/index.jsx"));
let socket = false;
let socketMonitorInterval = false;
loadPolyfills();
function Fallback({ error, resetErrorBoundary }) {
let loadingChunkErrorMessage = "";
if (error.message.toLowerCase().includes("loading") && error.message.toLowerCase().includes("chunk")) {
loadingChunkErrorMessage = (
<div>
This error is often caused by caching issues and can usually be resolved by{" "}
<button
style={{ padding: ".2rem", borderRadius: "4px" }}
onClick={() => {
location.reload(true);
}}
>
reloading the page
</button>
.
</div>
);
}
return (
<div className="display-wrapper" style={{ minHeight: "40vh" }} key="fallback-wrapper">
<h1 className="center-title">Something went wrong.</h1>
<div className="page-wrapper" style={{ minHeight: "40vh" }}>
<details style={{ whiteSpace: "pre-wrap" }}>
<pre style={{ color: "red" }}>{error.message}</pre>
<pre>{error.stack}</pre>
<pre>{`${window.location}`}</pre>
{loadingChunkErrorMessage}
You can{" "}
<button style={{ padding: ".2rem", borderRadius: "4px" }} onClick={resetErrorBoundary}>
try again
</button>{" "}
or report the issue by joining our{" "}
<a href="https://discord.gg/WwTvNe356u" target="_blank" rel="noopener noreferrer">
Discord
</a>{" "}
server and copy/paste the above error and some details in{" "}
<a
href="https://discord.com/channels/956236955815907388/956239773742288896"
target="_blank"
rel="noopener noreferrer"
>
#🐞bugs-issues
</a>{" "}
channel.
</details>
</div>
</div>
);
}
function App() {
const connectToId = new URLSearchParams(window.location.search).get("connection");
if (connectToId) {
localStorage.setItem("sessionId", JSON.stringify(connectToId));
}
const [sessionID] = useStateWithLocalStorage("sessionId", makeID(4));
const socketEnabled = useSelector((state) => state.sockets.enabled);
const controlId = useSelector((state) => state.sockets.controlId);
let navigate = useNavigate();
const dispatch = useDispatch();
const retrievedTarkovTrackerToken = useRef(false);
const tarkovTrackerProgressInterval = useRef(false);
const tarkovTrackerUpdatePending = useRef(false);
const tabHasFocus = useRef(true);
if (connectToId) {
dispatch(enableConnection());
}
const useTarkovTracker = useSelector((state) => state.settings[state.settings.gameMode].useTarkovTracker);
const progressStatus = useSelector((state) => {
return state.settings.progressStatus;
});
const tarkovTrackerAPIKey = useSelector((state) => state.settings[state.settings.gameMode].tarkovTrackerAPIKey);
const updateTarkovTrackerData = useCallback(() => {
tarkovTrackerUpdatePending.current = false;
retrievedTarkovTrackerToken.current = tarkovTrackerAPIKey;
dispatch(fetchTarkovTrackerProgress(tarkovTrackerAPIKey));
}, [dispatch, tarkovTrackerAPIKey]);
const scheduleTarkovTrackerUpdate = useCallback(() => {
clearInterval(tarkovTrackerProgressInterval.current);
tarkovTrackerProgressInterval.current = setInterval(
() => {
if (!tabHasFocus.current) {
// window doesn't have focus, so postpone the update until it does
tarkovTrackerUpdatePending.current = true;
return;
}
updateTarkovTrackerData();
},
1000 * 60 * 5,
);
}, [updateTarkovTrackerData]);
// monitor window focus for Tarkov Tracker updates
useEffect(() => {
const handleFocus = () => {
tabHasFocus.current = true;
if (!tarkovTrackerUpdatePending.current) {
return;
}
scheduleTarkovTrackerUpdate();
updateTarkovTrackerData();
};
const handleBlur = () => {
tabHasFocus.current = false;
};
window.addEventListener("focus", handleFocus);
window.addEventListener("blur", handleBlur);
// Clean up
return () => {
window.removeEventListener("focus", handleFocus);
window.removeEventListener("blur", handleBlur);
};
}, [scheduleTarkovTrackerUpdate, updateTarkovTrackerData]);
useEffect(() => {
if (!tarkovTrackerProgressInterval.current && useTarkovTracker) {
scheduleTarkovTrackerUpdate();
}
if (
useTarkovTracker &&
progressStatus !== "loading" &&
retrievedTarkovTrackerToken.current !== tarkovTrackerAPIKey
) {
updateTarkovTrackerData();
}
if (tarkovTrackerProgressInterval.current && !useTarkovTracker) {
clearInterval(tarkovTrackerProgressInterval.current);
tarkovTrackerProgressInterval.current = false;
}
return () => {
clearInterval(tarkovTrackerProgressInterval.current);
tarkovTrackerProgressInterval.current = false;
};
}, [progressStatus, scheduleTarkovTrackerUpdate, updateTarkovTrackerData, tarkovTrackerAPIKey, useTarkovTracker]);
useEffect(() => {
const connect = function connect() {
dispatch(setConnectionStatus("connecting"));
clearInterval(socketMonitorInterval);
socket = new RemoteWebSocket(sessionID);
socket.addEventListener("message", (rawMessage) => {
const message = JSON.parse(rawMessage.data);
if (message.type !== "command") {
return;
}
if (message.data.type === "playerPosition") {
dispatch(setPlayerPosition(message.data));
return;
}
navigate(`/${message.data.type}/${message.data.value}`);
});
socket.addEventListener("open", () => {
console.log("Connected to socket server");
//console.log(socket);
dispatch(setConnectionStatus("connected"));
});
socket.addEventListener("close", () => {
console.log("Disconnected from socket server");
dispatch(setConnectionStatus("idle"));
});
socketMonitorInterval = setInterval(() => {
if (socket.readyState === 3 && socketEnabled) {
console.log("Trying to re-connect to socket server");
connect();
}
}, 5000);
};
if (socket === false && socketEnabled) {
connect();
}
return () => {
// socket.close();
// clearInterval(socketMonitorInterval);
};
}, [socketEnabled, sessionID, navigate, dispatch]);
const send = useCallback(
(messageData) => {
if (socket.readyState !== 1) {
// Wait a bit if we're not connected
setTimeout(() => {
socket.send(
JSON.stringify({
sessionID: controlId,
...messageData,
}),
);
}, 500);
return true;
}
socket.send(
JSON.stringify({
sessionID: controlId,
...messageData,
}),
);
},
[controlId],
);
const hideRemoteControlId = useSelector((state) => state.settings.hideRemoteControl);
const remoteControlSessionElement = hideRemoteControlId ? null : (
<Suspense fallback={<Loading />} key="suspense-connection-wrapper">
<RemoteControlId
key="connection-wrapper"
sessionID={sessionID}
socketEnabled={socketEnabled}
onClick={(e) => dispatch(enableConnection())}
/>
</Suspense>
);
const alternateLangs = supportedLanguages.filter((lang) => lang !== i18n.language);
return (
<ThemeProvider theme={theme}>
<div className="App">
<Helmet htmlAttributes={{ lang: i18n.language }}>
<meta property="og:locale" content={i18n.language} key="meta-locale" />
{alternateLangs.map((lang) => (
<meta property="og:locale:alternate" content={lang} key={`meta-locale-alt-${lang}`} />
))}
</Helmet>
<Menu />
<CookieConsent buttonText={i18n.t("I understand")}>{i18n.t("cookie-consent")}</CookieConsent>
<WindowFocusHandler />
<ErrorBoundary FallbackComponent={Fallback}>
<Routes>
<Route
path={"/"}
key="start-route"
element={[
<Suspense fallback={<Loading />} key="suspense-start-wrapper">
<Start key="start-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/ammo"}
key="ammo-route"
element={[
<Suspense fallback={<Loading />} key="suspense-ammo-wrapper">
<Ammo key="ammo-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/ammo/:currentAmmo"}
key="ammo-current-route"
element={[
<Suspense fallback={<Loading />} key="suspense-ammo-wrapper">
<Ammo key="ammo-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/maps/"}
key="maps-route"
element={[
<Suspense fallback={<Loading />} key="suspense-maps-wrapper">
<Maps key="maps-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/map/:currentMap"}
key="map-current-route"
element={[
<Suspense fallback={<Loading />} key="suspense-map-wrapper">
<Map key="map-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/loot-tier"}
key="loot-tier-route"
element={[
<Suspense fallback={<Loading />} key="suspense-loot-tier-wrapper">
<LootTiers key="loot-tier-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/barters"}
key="barters-route"
element={[
<Suspense fallback={<Loading />} key="suspense-barters-wrapper">
<Barters key="barters-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/barter"}
key="barter-route"
element={[<Navigate to="/barters" />, remoteControlSessionElement]}
/>
<Route
path={"/items"}
key="items-route"
element={[
<Suspense fallback={<Loading />} key="suspense-items-wrapper">
<Items key="items-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/item"}
key="item-route"
element={[<Navigate to="/items" />, remoteControlSessionElement]}
/>
<Route
path={"/items/ammo"}
key="items-ammo-route"
element={[<Navigate to="/ammo" />, remoteControlSessionElement]}
/>
<Route
path={"/items/helmets"}
key="helmets-route"
element={[
<Suspense fallback={<Loading />} key="suspense-helmets-wrapper">
<Helmets key="helmets-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/glasses"}
key="glasses-route"
element={[
<Suspense fallback={<Loading />} key="suspense-glasses-wrapper">
<Glasses key="glasses-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/armors"}
key="armors-route"
element={[
<Suspense fallback={<Loading />} key="suspense-armors-wrapper">
<Armors key="armors-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/backpacks"}
key="backpacks-route"
element={[
<Suspense fallback={<Loading />} key="suspense-backpacks-wrapper">
<Backpacks key="backpacks-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/backpack"}
key="backpack-route"
element={[<Navigate to="/items/backpacks" />, remoteControlSessionElement]}
/>
<Route
path={"/items/rigs"}
key="rigs-route"
element={[
<Suspense fallback={<Loading />} key="suspense-rigs-wrapper">
<Rigs key="rigs-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/chest-rig"}
key="chest-rig-route"
element={[<Navigate to="/items/rigs" />, remoteControlSessionElement]}
/>
<Route
path={"/items/suppressors"}
key="suppressors-route"
element={[
<Suspense fallback={<Loading />} key="suspense-suppressors-wrapper">
<Suppressors key="suppressors-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/silencer"}
key="silencer-route"
element={[<Navigate to="/items/suppressors" />, remoteControlSessionElement]}
/>
<Route
path={"/items/guns"}
key="guns-route"
element={[
<Suspense fallback={<Loading />} key="suspense-guns-wrapper">
<Guns key="guns-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/mods"}
key="mods-route"
element={[
<Suspense fallback={<Loading />} key="suspense-mods-wrapper">
<Mods key="mods-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/weapon-mod"}
key="weapon-mod-route"
element={[<Navigate to="/items/mods" />, remoteControlSessionElement]}
/>
<Route
path={"/items/pistol-grips"}
key="pistol-grips-route"
element={[
<Suspense fallback={<Loading />} key="suspense-pistol-grips-wrapper">
<PistolGrips key="pistol-grips-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/barter-items"}
key="barter-items-route"
element={[
<Suspense fallback={<Loading />} key="suspense-barter-items-wrapper">
<BarterItems key="barter-items-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/containers"}
key="containers-route"
element={[
<Suspense fallback={<Loading />} key="suspense-containers-wrapper">
<Containers key="containers-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/common-container"}
key="common-container-route"
element={[<Navigate to="/items/containers" />, remoteControlSessionElement]}
/>
<Route
path={"/items/grenades"}
key="grenades-route"
element={[
<Suspense fallback={<Loading />} key="suspense-grenades-wrapper">
<Grenades key="grenades-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/throwable-weapon"}
key="throwable-weapon-route"
element={[<Navigate to="/items/grenades" />, remoteControlSessionElement]}
/>
<Route
path={"/items/headsets"}
key="headsets-route"
element={[
<Suspense fallback={<Loading />} key="suspense-headsets-wrapper">
<Headsets key="headsets-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/headphones"}
key="headphones-route"
element={[<Navigate to="/items/headsets" />, remoteControlSessionElement]}
/>
<Route
path={"/items/keys"}
key="keys-route"
element={[
<Suspense fallback={<Loading />} key="suspense-keys-wrapper">
<Keys key="keys-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/key"}
key="key-route"
element={[<Navigate to="/items/keys" />, remoteControlSessionElement]}
/>
<Route
path={"/items/provisions"}
key="provisions-route"
element={[
<Suspense fallback={<Loading />} key="suspense-provisions-wrapper">
<Provisions key="provisions-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/items/food-and-drink"}
key="food-and-drink-route"
element={[<Navigate to="/items/provisions" />, remoteControlSessionElement]}
/>
<Route
path="/items/:bsgCategoryName"
key="items-category-route"
element={[
<Suspense fallback={<Loading />} key="suspense-items-category-wrapper">
<BsgCategory />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path="/items/handbook/:handbookCategoryName"
key="items-handbook-category-route"
element={[
<Suspense fallback={<Loading />} key="suspense-items-category-wrapper">
<HandbookCategory />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/item/:itemName"}
key="item-route"
element={[
<Suspense fallback={<Loading />} key="suspense-item-wrapper">
<Item key="item-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/bosses"}
key="bosses-route"
element={[
<Suspense fallback={<Loading />} key="suspense-bosses-wrapper">
<Bosses key="bosses-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/boss"}
key="boss-route"
element={[<Navigate to="/bosses" />, remoteControlSessionElement]}
/>
<Route
path={"/boss/:bossName"}
key="boss-name-route"
element={[
<Suspense fallback={<Loading />} key="suspense-boss-wrapper">
<Boss key="boss-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/traders"}
key="traders-route"
element={[
<Suspense fallback={<Loading />} key="suspense-traders-wrapper">
<Traders key="traders-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/trader"}
key="trader-route"
element={[<Navigate to="/traders" />, remoteControlSessionElement]}
/>
<Route
path={"/trader/:traderName"}
key="trader-name-route"
element={[
<Suspense fallback={<Loading />} key="suspense-trader-wrapper">
<Trader key="trader-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/hideout-profit/"}
key="hideout-profit-route"
element={[
<Suspense fallback={<Loading />} key="suspense-hideout-profit-wrapper">
<Crafts key="hideout-profit-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/item-tracker/"}
key="item-tracker-route"
element={[
<Suspense fallback={<Loading />} key="suspense-item-tracker-wrapper">
<ItemTracker key="item-tracker-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/debug/"}
key="debug-route"
element={[
<Suspense fallback={<Loading />} key="suspense-debug-wrapper">
<Debug key="debug-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/about"}
key="about-route"
element={[
<Suspense fallback={<Loading />} key="suspense-about-wrapper">
<About key="about-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/api/"}
key="api-route"
element={[
<Suspense fallback={<Loading />} key="suspense-api-docs-wrapper">
<APIDocs key="api-docs-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/nightbot/"}
key="nightbot-route"
element={[
<Suspense fallback={<Loading />} key="suspense-nightbot-wrapper">
<Nightbot />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/streamelements/"}
key="streamelements-route"
element={[
<Suspense fallback={<Loading />} key="suspense-streamelements-wrapper">
<StreamElements />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/moobot"}
key="moobot-route"
element={[
<Suspense fallback={<Loading />} key="suspense-moobot-wrapper">
<Moobot />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/api-users/"}
key="api-users-route"
element={[
<Suspense fallback={<Loading />} key="suspense-api-users-wrapper">
<ApiUsers />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/hideout"}
key="hideout-route"
element={[
<Suspense fallback={<Loading />} key="suspense-hideout-wrapper">
<Hideout key="hideout-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/wipe-length"}
key="wipe-length-route"
element={[
<Suspense fallback={<Loading />} key="suspense-wipe-length-wrapper">
<WipeLength key="wipe-length-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/bitcoin-farm-calculator"}
key="bitcoin-farm-calculator-route"
element={[
<Suspense fallback={<Loading />} key="suspense-bitcoin-farm-wrapper">
<BitcoinFarmCalculator key="bitcoin-farm-calculator" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/settings/"}
key="settings-route"
element={[
<Suspense fallback={<Loading />} key="suspense-settings-wrapper">
<Settings key="settings-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/control"}
key="control-route"
element={[
<Suspense fallback={<Loading />} key="suspense-control-wrapper">
<Control send={send} />
</Suspense>,
]}
/>
<Route
path={"/tasks/"}
key="tasks-route"
element={[
<Suspense fallback={<Loading />} key="suspense-tasks-wrapper">
<Quests key="tasks-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/task/:taskIdentifier"}
key="task-route"
element={[
<Suspense fallback={<Loading />} key="suspense-task-wrapper">
<Quest key="task-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/prestige/"}
key="prestige-list-route"
element={[
<Suspense fallback={<Loading />} key="suspense-tasks-wrapper">
<Prestiges key="prestige-list-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/prestige/:prestigeLevel"}
key="prestige-route"
element={[
<Suspense fallback={<Loading />} key="suspense-task-wrapper">
<Prestige key="rpestige-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/achievements"}
key="achievements-route"
element={[
<Suspense fallback={<Loading />} key="suspense-achievements-wrapper">
<Achievements key="achievements-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/players"}
key="players-route"
element={[
<Suspense fallback={<Loading />} key="suspense-players-wrapper">
<Players key="players-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path="/players/:gameMode/:accountId"
key="player-route"
element={[
<Suspense fallback={<Loading />} key="suspense-player-wrapper">
<Player />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path="/player/:accountId"
key="player-regular-route"
element={[
<Suspense fallback={<Loading />} key="suspense-player-forward-wrapper">
<PlayerForward />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/converter"}
key="converter-route"
element={[
<Suspense fallback={<Loading />} key="suspense-converter-wrapper">
<Converter key="converter-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/other-tools"}
key="other-tools"
element={[
<Suspense fallback={<Loading />} key="suspense-other-tools-wrapper">
<OtherTools key="other-tools-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/tarkov-monitor"}
key="tarkov-monitor"
element={[
<Suspense fallback={<Loading />} key="suspense-tarkov-monitor-wrapper">
<TarkovMonitorPage key="tarkov-monitor-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path={"/stash-discord-bot"}
key="stash-bot"
element={[
<Suspense fallback={<Loading />} key="suspense-stash-wrapper">
<StashBotPage key="stash-wrapper" />
</Suspense>,
remoteControlSessionElement,
]}
/>
<Route
path="*"
element={[
<Suspense fallback={<Loading />} key="suspense-errorpage-wrapper">
<ErrorPage />
</Suspense>,
remoteControlSessionElement,
]}
/>
</Routes>
</ErrorBoundary>
<Footer />
</div>
</ThemeProvider>
);
}
export default App;
================================================
FILE: src/__tests__/App.test.jsx
================================================
import { expect, it } from "@rstest/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { waitFor, screen } from "@testing-library/react";
import React from "react";
import { mockAllIsIntersecting } from "react-intersection-observer/test-utils";
import App from "#src/App.jsx";
import { renderWithProviders } from "#src/__tests__/test-utils.js";
const queryClient = new QueryClient();
it("renders without crashing", async () => {
mockAllIsIntersecting(true);
renderWithProviders(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByRole("navigation")).toHaveClass("navigation");
});
});
================================================
FILE: src/__tests__/test-utils.js
================================================
// https://redux.js.org/usage/writing-tests#components
import React from "react";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import defaultStore from "#src/store";
export function renderWithProviders(
ui,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = defaultStore,
...renderOptions
} = {},
) {
function Wrapper({ children }) {
return (
<Provider store={store}>
<Router>{children}</Router>
</Provider>
);
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}
================================================
FILE: src/__tests__/tsconfig.json
================================================
{
"compilerOptions": {
"types": ["@testing-library/jest-dom", "@rstest/core/globals"]
},
"extends": "../../tsconfig.json",
"include": ["./"]
}
================================================
FILE: src/components/Debug.jsx
================================================
import { useState } from "react";
function Debug() {
const [itemId, setItemId] = useState("5eff09cd30a7dc22fd1ddfed");
return (
<div>
<input type="text" value={itemId} onChange={(e) => setItemId(e.target.value)} />
<pre>Nothing here</pre>
</div>
);
}
export default Debug;
================================================
FILE: src/components/FilterIcon.jsx
================================================
import { Component } from "react";
class FilterIcon extends Component {
render() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path d="M4 10h7a6 6 0 0012 0h21a1 1 0 000-2H23a6 6 0 00-12 0H4a1 1 0 000 2zm13-5a4 4 0 11-4 4 4 4 0 014-4zm27 18h-7a6 6 0 00-12 0H4a1 1 0 000 2h21a6 6 0 0012 0h7a1 1 0 000-2zm-13 5a4 4 0 114-4 4 4 0 01-4 4zm13 10H23a6 6 0 00-12 0H4a1 1 0 000 2h7a6 6 0 0012 0h21a1 1 0 000-2zm-27 5a4 4 0 114-4 4 4 0 01-4 4z" />
</svg>
);
}
}
export default FilterIcon;
================================================
FILE: src/components/FleaMarketLoadingIcon.jsx
================================================
import { useTranslation } from "react-i18next";
import { Icon } from "@mdi/react";
import { mdiTimerSand } from "@mdi/js";
import { Tooltip } from "@mui/material";
function FleaMarketLoadingIcon({ size = 1, tooltip }) {
const { t } = useTranslation();
if (!tooltip) {
tooltip = t("Flea market prices loading");
}
return (
<Tooltip placement="bottom" title={tooltip} arrow>
<Icon path={mdiTimerSand} size={size} className="icon-with-text" />
</Tooltip>
);
}
export default FleaMarketLoadingIcon;
================================================
FILE: src/components/Graph.jsx
================================================
import { useCallback, useMemo } from "react";
import {
VictoryChart,
VictoryScatter,
VictoryTheme,
VictoryLegend,
VictoryLine,
VictoryLabel,
VictoryAxis,
// VictoryContainer,
// VictoryTooltip,
// VictoryVoronoiContainer,
VictoryContainer,
} from "victory";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Symbol from "./Symbol.jsx";
// import GraphLabel from './GraphLabel';
const MAX_DAMAGE = 170;
const MAX_PENETRATION = 70;
const styles = {
classLabel: {
fontSize: 3,
fill: "#ddd",
strokeWidth: 1,
},
xaxis: {
tickLabels: {
fontSize: 5,
},
grid: {
stroke: "#555",
},
axisLabel: {
fontSize: 4,
padding: 5,
fill: "#ccc",
},
},
yaxis: {
tickLabels: {
fill: "#fff",
fontSize: 4,
},
grid: {
stroke: "#555",
},
axisLabel: {
fontSize: 4,
padding: 5,
fill: "#ccc",
},
ticks: {
size: 0,
},
},
scatter: {
labels: {
fontSize: 2.5,
fill: "#ccc",
},
},
legend: {
border: {
stroke: "black",
fill: "#2d2d2f",
width: 37,
},
labels: {
fill: "#ccc",
fontSize: 3,
cursor: "pointer",
},
title: {
fill: "#ccc",
fontSize: 4,
padding: 2,
},
},
annotionLine: {
data: {
stroke: "#888",
strokeWidth: 0.5,
strokeDasharray: 1,
},
labels: {
angle: -90,
fill: "#ccc",
fontSize: 3,
},
},
};
const LegendLabel = (props) => {
const { selectedDatumName, datum } = props;
const style = useMemo(() => {
let style = props.style;
if (selectedDatumName.includes(datum.name)) {
style = {
...props.style,
textDecoration: "underline",
fill: "#fff",
};
}
return style;
}, [selectedDatumName, datum.name, props.style]);
return <VictoryLabel {...props} style={style} />;
};
export const getMarkerLine = (xMax, xTarget, label) => {
if (xMax < xTarget + 1) {
return null;
}
return (
<VictoryLine
style={styles.annotionLine}
labels={[label]}
key={label}
labelComponent={<VictoryLabel textAnchor="middle" verticalAnchor="middle" dx={xMax - 40} dy={-3} />}
x={() => xTarget}
/>
);
};
// const getArmorLabel = (tier, yMax, xMax) => {
// if(tier * 10 > yMax){
// return null;
// }
// return <VictoryLabel
// key = {`class-${tier}-label`}
// text = {`Class ${tier}`}
// style = {styles.classLabel}
// datum = {{
// x: xMax / 100,
// y: tier * 10 + 1,
// }}
// textAnchor = "start"
// verticalAnchor = "end"
// />
// };
const xTickValues = [
10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240,
];
const yTickValues = [10, 20, 30, 40, 50, 60, 70];
const chartAnimate = { duration: 500 };
const chartPadding = { top: 10, bottom: 20, right: 50, left: 10 };
// Use provided max domains if passed, otherwise fallback to defaults
// Note: evaluated inside component to access props
const Graph = (props) => {
const { xMax, listState } = props;
const navigate = useNavigate();
const handleLabelClick = useCallback(
(event, data) => {
navigate(`/item/${data.datum.id.toString()}`);
},
[navigate],
);
const { t } = useTranslation();
const chartMinDomain = { y: props.yMin ?? 0, x: props.xMin ?? 0 };
const markerLines = useMemo(() => {
return [
getMarkerLine(xMax, 85, t("PMC & Scav Thorax HP")),
getMarkerLine(xMax, 145, t("Reshala Thorax HP")),
getMarkerLine(xMax, 160, t("Raider Thorax HP")),
getMarkerLine(xMax, 180, t("Shturman Thorax HP")),
getMarkerLine(xMax, 200, t("Cultist Priest Thorax HP")),
getMarkerLine(xMax, 220, t("Cultist Warrior Thorax HP")),
// getArmorLabel(1, yMax, xMax),
// getArmorLabel(2, yMax, xMax),
// getArmorLabel(3, yMax, xMax),
// getArmorLabel(4, yMax, xMax),
// getArmorLabel(5, yMax, xMax),
// getArmorLabel(6, yMax, xMax),
].filter(Boolean);
}, [xMax, t]);
const scatterData = useMemo(() => {
return listState.map((ls) => {
return {
x: ls.displayDamage,
y: ls.displayPenetration,
label: ls.chartName,
symbol: ls.symbol,
id: ls.id,
};
});
}, [listState]);
const chartMaxDomain = { y: props.yMax ?? MAX_PENETRATION, x: props.xMax ?? MAX_DAMAGE };
return (
<VictoryChart
domainPadding={10}
padding={chartPadding}
height={Math.max(props.legendData.length * 7 + 17, 180)}
theme={VictoryTheme.material}
minDomain={chartMinDomain}
maxDomain={chartMaxDomain}
containerComponent={
<VictoryContainer
style={{
touchAction: "auto",
}}
/>
}
>
<VictoryAxis
axisLabelComponent={<VictoryLabel x={177} />}
label={t("Damage")}
tickValues={xTickValues}
style={styles.xaxis}
/>
<VictoryAxis
dependentAxis
tickFormat={(tick) => (tick === 70 ? "" : t("Class {{tier}}", { tier: tick / 10 }))}
tickLabelComponent={<VictoryLabel dx={24} dy={-4} />}
label={t("Penetration")}
tickValues={yTickValues}
style={styles.yaxis}
/>
<VictoryScatter
dataComponent={<Symbol link={true} />}
_animate={chartAnimate}
events={[
{
target: "labels",
eventHandlers: {
onClick: handleLabelClick,
},
},
]}
style={styles.scatter}
// labelComponent={<GraphLabel
// dy={-3}
// />}
labelComponent={<VictoryLabel dy={-3} />}
size={1}
activeSize={5}
data={scatterData}
/>
{/* <VictoryScatter
dataComponent = {<Symbol />}
style={styles.scatter}
labelComponent={<VictoryLabel dy={-3} />}
// labelComponent={<VictoryTooltip
// labelComponent = {<VictoryLabel dy={-3} />}
// />}
labels={({ datum }) => {
return datum.name;
}}
size={1}
activeSize={5}
data={props.listState}
x="damage"
y="penetration"
/> */}
<VictoryLegend
data={props.legendData}
dataComponent={<Symbol link={false} />}
title={t("Filter by caliber")}
labelComponent={<LegendLabel selectedDatumName={props.selectedLegendName} />}
events={[
{
target: "labels",
eventHandlers: {
onClick: props.handleLegendClick,
},
},
]}
gutter={10}
orientation="vertical"
style={styles.legend}
x={312}
y={9}
/>
{markerLines}
</VictoryChart>
);
};
export default Graph;
================================================
FILE: src/components/GraphLabel.jsx
================================================
import React from "react";
import { VictoryLabel, VictoryTooltip } from "victory";
class GraphLabel extends React.Component {
static defaultEvents = VictoryTooltip.defaultEvents;
render() {
return (
<g>
<VictoryLabel {...this.props} />
<VictoryTooltip
{...this.props}
// x={0}
// y={50}
text={`# ${this.props.text}`}
orientation="top"
pointerLength={0}
cornerRadius={5}
width={10}
height={10}
flyoutStyle={{
fill: "black",
}}
/>
</g>
);
}
}
export default GraphLabel;
================================================
FILE: src/components/SEO.jsx
================================================
import React from "react";
import { Helmet } from "react-helmet";
export default function SEO({ title, description, url, image, type = "article", card = "summary" }) {
let urlPath = url ? url : window.location.href;
return (
<Helmet>
{/* Standard metadata tags */}
<title>{title}</title>
<meta name="description" content={description} />
{/* End standard metadata tags */}
{/* OpenGraph / Facebook tags */}
<meta property="og:type" content={type} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={urlPath} />
<meta property="og:image" content={image} />
{/* End Facebook tags */}
{/* Twitter tags */}
<meta name="twitter:card" content={card} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:url" content={urlPath} />
<meta name="twitter:image" content={image} />
{/* End Twitter tags */}
</Helmet>
);
}
================================================
FILE: src/components/Symbol.jsx
================================================
import { Component } from "react";
import { Navigate } from "react-router-dom";
import * as shapes from "./points/index.jsx";
const SIZE = 2;
class Symbol extends Component {
constructor() {
super();
this.state = {
redirect: false,
};
this.handleOnClick = () => {
if (this.props.link === false) {
return true;
}
this.setState({
redirect: true,
});
};
}
render() {
if (this.state.redirect) {
return <Navigate replace to={`/item/${this.props.datum.id}`} />;
}
const { x, y, datum } = this.props;
const PointComponent = shapes[datum.symbol.type];
return (
<PointComponent
onClick={this.handleOnClick}
width={SIZE}
height={SIZE}
fill={datum.symbol.fill}
x={x - SIZE / 2}
y={y - SIZE / 2}
/>
);
}
}
export default Symbol;
================================================
FILE: src/components/Time.jsx
================================================
import dayjs from "dayjs";
import dayjsUtc from "dayjs/plugin/utc";
import { useTranslation } from "react-i18next";
import useDate from "../hooks/useDate.jsx";
/*
Huge thanks to Adam Burgess
https://github.com/adamburgess/tarkov-time
as most of the code is his.
Thanks a bunch!
*/
dayjs.extend(dayjsUtc);
// 1 second real time = 7 seconds tarkov time
const tarkovRatio = 7;
export function hrs(num) {
return 1000 * 60 * 60 * num;
}
export function realTimeToTarkovTime(time, left) {
// tarkov time moves at 7 seconds per second.
// surprisingly, 00:00:00 does not equal unix 0... but it equals unix 10,800,000.
// Which is 3 hours. What's also +3? Yep, Russia. UTC+3.
// therefore, to convert real time to tarkov time,
// tarkov time = (real time * 7 % 24 hr) + 3 hour
const oneDay = hrs(24);
const russia = hrs(3);
const offset = russia + (left ? 0 : hrs(12));
const tarkovTime = new Date((offset + time.getTime() * tarkovRatio) % oneDay);
return tarkovTime;
}
export function timeUntilRelative(until, left, date) {
const tarkovTime = realTimeToTarkovTime(date, left);
if (until < tarkovTime.getTime()) {
until += hrs(24);
}
const diffTarkov = until - tarkovTime.getTime();
const diffRT = diffTarkov / tarkovRatio;
return diffRT;
}
export function formattedTarkovTime(left = true) {
const time = new Date();
const tarkovTime = realTimeToTarkovTime(time, left);
return dayjs.utc(tarkovTime).format("HH:mm:ss");
}
export function formatFuture(ms) {
const time = dayjs.utc(ms);
const hour = time.hour();
const min = time.minute();
const sec = time.second();
let text = "";
if (hour !== 0) {
text = hour + "hr";
}
text += min + "min";
if (hour === 0 && min === 0) {
text = sec + "s";
}
return text;
}
function MapDetails(props) {
const { t } = useTranslation();
const overlayItem = [
<div key={`${props.currentMap}-duration`}>
{t("Duration")}: {props.duration}
</div>,
<div key={`${props.currentMap}-players`}>
{t("Players")}: {props.players}
</div>,
];
if (props.author) {
overlayItem.push(
<div key={`${props.currentMap}-attribution`}>
{t("By")}
<span>:</span>{" "}
<a href={props.authorLink} target="_blank" rel="noopener noreferrer">
{props.author}
</a>
</div>,
);
}
return overlayItem;
}
function Time(props) {
const time = useDate(new Date(), 50);
if (props?.normalizedName === "factory") {
return (
<div className="time-wrapper">
<div>15:28:00</div>
<div>03:28:00</div>
<MapDetails {...props} />
</div>
);
}
if (props?.normalizedName === "the-lab") {
return (
<div className="time-wrapper">
<MapDetails {...props} />
</div>
);
}
const tarkovTime1 = realTimeToTarkovTime(time, true);
const tarkovTime2 = realTimeToTarkovTime(time);
return (
<div className="time-wrapper">
<div>{dayjs.utc(tarkovTime1).format("HH:mm:ss")}</div>
<div>{dayjs.utc(tarkovTime2).format("HH:mm:ss")}</div>
<MapDetails {...props} />
</div>
);
}
export default Time;
================================================
FILE: src/components/api-metrics-graph/index.css
================================================
.api-metrics-wrapper {
height: 300px;
margin-bottom: 100px;
}
================================================
FILE: src/components/api-metrics-graph/index.jsx
================================================
import { useQuery } from "@tanstack/react-query";
import { VictoryChart, VictoryLine, VictoryTheme, VictoryVoronoiContainer } from "victory";
import { useTranslation } from "react-i18next";
import "./index.css";
const API_METRICS_ENDPOINT = "https://status.tarkov.dev/api/status-page/heartbeat/api";
const fetchApiData = async () => {
const res = await fetch(API_METRICS_ENDPOINT);
return res.json();
};
function ApiMetricsGraph({ graph }) {
const { t } = useTranslation();
const { status, data } = useQuery({
queryKey: `api-metrics`,
queryFn: fetchApiData,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
let height = VictoryTheme.material.height;
if (window.innerWidth < 760) {
height = 1280;
}
if (status === "error") {
return "⚠️ Error Fetching API Metrics";
}
if (status !== "success") {
return null;
}
if (status === "success" && data.heartbeatList["1"] === 0) {
return `⚠️ ${t("No data")}`;
}
let max = 0;
data.heartbeatList["1"].map((heartbeat) => {
if (heartbeat.ping > max) {
max = heartbeat.ping;
}
return true;
});
// Loop through each heartbeat and add the latency to a total that is rounded
let total = 0;
for (const heartbeat of data.heartbeatList["1"]) {
total += heartbeat.ping;
}
const average = Math.round(total / data.heartbeatList["1"].length);
// If the graph param was used, return the graph and the latency average as a div
if (graph === true) {
return (
<div className="api-metrics-wrapper">
<p>
{t("Current Average Latency")}: {average}ms
</p>
<p>{t("API Latency in milliseconds")}:</p>
<VictoryChart
height={height}
width={900}
padding={{ top: 20, left: 15, right: -100, bottom: 30 }}
minDomain={{ y: 0 }}
maxDomain={{ y: max + max * 0.1 }}
theme={VictoryTheme.material}
containerComponent={<VictoryVoronoiContainer labels={({ datum }) => `${datum.y}`} />}
>
<VictoryLine
animate={{
duration: 1000,
onLoad: { duration: 1000 },
}}
interpolation="natural"
padding={{ right: -120 }}
scale={{
x: "time",
y: "linear",
}}
style={{
data: {
stroke: "var(--color-green)",
strokeWidth: 4,
},
parent: { border: "1px solid #ccc" },
}}
data={data.heartbeatList["1"].map((heartbeat) => {
return {
x: new Date(heartbeat.time),
y: heartbeat.ping,
};
})}
/>
</VictoryChart>
</div>
);
}
// If the graph param was not provided, return the latency average as a div
else {
return `${average}ms`;
}
}
export default ApiMetricsGraph;
================================================
FILE: src/components/barter-tooltip/index.css
================================================
.barter-tooltip-wrapper {
display: flex;
flex-wrap: wrap;
}
.barter-tooltip-details-wrapper > div {
line-height: 20px;
}
.barter-tooltip-wrapper h3 {
text-align: center;
width: 100%;
}
.barter-required-item-image img {
width: 48px;
height: 48px;
}
.barter-tooltip-wrapper .reward-image-wrapper img {
height: 48px;
width: 48px;
}
.barter-tooltip-item-wrapper {
align-items: center;
display: flex;
position: relative;
width: 100%;
padding: 5px 0px;
}
.barter-tooltip-details-wrapper a {
font-size: 16px;
}
.barter-tooltip-details-wrapper .price-wrapper {
white-space: nowrap;
}
.barter-tooltip-icon {
border: 0;
position: relative;
top: 2px;
width: 14px;
margin-right: 3px;
}
.barter-required-item-image {
margin-right: 10px;
}
================================================
FILE: src/components/barter-tooltip/index.jsx
================================================
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { Icon } from "@mdi/react";
import { mdiCached, mdiProgressWrench } from "@mdi/js";
import ItemImage from "../item-image/index.jsx";
import formatPrice from "../../modules/format-price.js";
import { isAnyDogtag, getDogTagCost } from "../../modules/dogtags.js";
import { getCheapestPrice } from "../../modules/format-cost-items.js";
import { getDurationDisplay } from "../../modules/format-duration.js";
import useHideoutData from "../../features/hideout/index.js";
import useTraderData from "../../features/traders/index.js";
import "./index.css";
function BarterTooltip({
barter,
showTitle = true,
title,
allowAllSources = false,
crafts,
barters,
useBarterIngredients,
useCraftIngredients,
}) {
const settings = useSelector((state) => state.settings[state.settings.gameMode]);
const { t } = useTranslation();
const { data: hideout } = useHideoutData();
const { data: traders } = useTraderData();
if (barters && typeof useBarterIngredients === "undefined") {
useBarterIngredients = true;
}
if (crafts && typeof useCraftIngredients === "undefined") {
useCraftIngredients = true;
}
const requirements = useMemo(() => {
if (!barter) {
return false;
}
const items = barter.requiredItems;
if (!items) {
// Should never happen
return false;
}
return items.map((req) => {
const cheapestPrice = getCheapestPrice(req.item, {
barters: useBarterIngredients ? barters : false,
crafts: useCraftIngredients ? crafts : false,
settings,
allowAllSources,
useBarterIngredients,
useCraftIngredients,
});
return {
...req,
cheapestPrice,
};
});
}, [barter, settings, allowAllSources, barters, crafts, useBarterIngredients, useCraftIngredients]);
const totalCost = useMemo(() => {
if (!requirements) {
return 0;
}
return requirements.reduce((total, req) => {
if (req.attributes.some((att) => att.type === "tool")) {
return total;
}
total += req.cheapestPrice.pricePerUnit * req.count;
return total;
}, 0);
}, [requirements]);
if (!barter) {
return t("No barters found for this item");
}
if (!barter.trader && !barter.station) {
// Should never happen
return "Missing trader for this barter";
}
if (!requirements) {
// Should never happen
return "Missing requirements for this barter";
}
let titleElement = "";
if (showTitle) {
const source = barter.trader
? traders.find((t) => t.id === barter.trader.id)
: hideout.find((s) => s.id === barter.station.id);
const sourceLevelText = barter.trader
? `${source.name} ${t("LL{{level}}", { level: barter.level })}`
: `${source.name} ${barter.level}`;
const tipTitle = barter.trader
? t("Barter at {{trader}}", { trader: sourceLevelText })
: t("Craft at {{station}}", { station: sourceLevelText });
titleElement = (
<h3>
<Icon path={barter.trader ? mdiCached : mdiProgressWrench} size={1} className="icon-with-text" />
{tipTitle}
</h3>
);
if (title) {
titleElement = <h4>{title}</h4>;
}
}
return (
<div className="barter-tooltip-wrapper">
{titleElement}
{requirements.map((requiredItem) => {
let itemName = requiredItem.item.name;
let price = requiredItem.cheapestPrice.pricePerUnit;
let sourceName =
requiredItem.cheapestPrice.vendor?.normalizedName ||
requiredItem.cheapestPrice.craft?.station.normalizedName;
if (isAnyDogtag(requiredItem.item.id)) {
const dogtagCost = getDogTagCost(requiredItem, settings);
itemName = dogtagCost.name;
price = dogtagCost.price;
sourceName = dogtagCost.sourceNormalizedName;
}
let sourceImage = (
<img
alt={t("Barter")}
className="barter-tooltip-icon"
loading="lazy"
src={`${process.env.PUBLIC_URL}/images/traders/${sourceName}-icon.jpg`}
/>
);
if (requiredItem.cheapestPrice.type === "craft") {
const station = hideout.find((s) => s.id === requiredItem.cheapestPrice.craft.station.id);
const craftInfo = t("Craft at {{stationName}} {{stationLevel}}", {
stationName: station.name,
stationLevel: requiredItem.cheapestPrice.craft.level,
});
sourceImage = (
<Link to={`/hideout-profit/?search=${requiredItem.item.name}`}>
<img
alt={craftInfo}
title={craftInfo}
className="barter-tooltip-icon"
loading="lazy"
src={station.imageLink}
/>
</Link>
);
}
return (
<div className="barter-tooltip-item-wrapper" key={`reward-tooltip-item-${requiredItem.item.id}`}>
<div className="barter-required-item-image">
<ItemImage
item={requiredItem.item}
attributes={requiredItem.attributes}
count={requiredItem.count}
imageField="iconLink"
linkToItem={true}
/>
</div>
<div className="barter-tooltip-details-wrapper">
<div>
<Link to={`/item/${requiredItem.item.normalizedName}`}>{itemName}</Link>
</div>
<div className="price-wrapper">
{sourceImage}
{requiredItem.cheapestPrice.barter && (
<Link to={`/barters/?search=${requiredItem.item.name}`}>
<img
alt={t("Barter")}
className="item-cost-barter-icon"
loading="lazy"
src={`${process.env.PUBLIC_URL}/images/icon-barter.png`}
/>
</Link>
)}
{requiredItem.count} <span>X</span> {formatPrice(price)} <span>=</span>{" "}
{formatPrice(requiredItem.count * price)}
</div>
</div>
</div>
);
})}
{barter.rewardItems[0].count > 1 && barter.trader && (
<div className="barter-tooltip-item-wrapper" key={`reward-tooltip-details`}>
{t("Provides {{count}} for {{totalCost}}", {
count: barter.rewardItems[0].count,
totalCost: formatPrice(totalCost),
})}
</div>
)}
{barter.station && (
<div className="barter-tooltip-item-wrapper" key={`reward-tooltip-details`}>
{t("Crafts {{count}} in {{duration}} for {{totalCost}}", {
count: barter.rewardItems[0].count,
duration: getDurationDisplay(barter.duration * 1000),
totalCost: formatPrice(totalCost),
})}
</div>
)}
</div>
);
}
export default BarterTooltip;
================================================
FILE: src/components/barters-table/index.css
================================================
/* CSS Placeholder */
================================================
FILE: src/components/barters-table/index.jsx
================================================
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import DataTable from "../data-table/index.jsx";
import useBartersData from "../../features/barters/index.js";
import useCraftsData from "../../features/crafts/index.js";
import useItemsData, { useHandbookData } from "../../features/items/index.js";
import { selectAllTraders } from "../../features/settings/settingsSlice.mjs";
import ValueCell from "../value-cell/index.jsx";
import CostItemsCell from "../cost-items-cell/index.jsx";
import RewardCell from "../reward-cell/index.jsx";
import FleaMarketLoadingIcon from "../FleaMarketLoadingIcon.jsx";
import { formatCostItems, getCheapestCashPrice, getCheapestBarter } from "../../modules/format-cost-items.js";
import { isAnyDogtag, isBothDogtags } from "../../modules/dogtags.js";
import fleaMarketFee from "../../modules/flea-market-fee.mjs";
import "./index.css";
function BartersTable({ selectedTrader, nameFilter, itemFilter, showAll, useBarterIngredients, useCraftIngredients }) {
const { t } = useTranslation();
const settings = useSelector((state) => state.settings[state.settings.gameMode]);
const { hasJaeger, removeDogtags, completedQuests } = useMemo(() => {
return {
hasJaeger: settings.jaeger !== 0,
removeDogtags: settings.hideDogtagBarters,
completedQuests: settings.completedQuests,
};
}, [settings]);
const traders = useSelector(selectAllTraders);
const [skippedBySettings, setSkippedBySettings] = useState(false);
const { data: barters } = useBartersData();
const { data: crafts } = useCraftsData();
const { data: items } = useItemsData();
const { data: handbook } = useHandbookData();
const columns = useMemo(
() => [
{
Header: t("Reward"),
id: "reward",
accessor: "reward",
Cell: ({ value }) => {
return <RewardCell {...value} />;
},
},
{
Header: t("Cost"),
id: "costItems",
accessor: "costItems",
sortType: (a, b, columnId, desc) => {
if (
a.values.costItems[0].id === "5d235b4d86f7742e017bc88a" &&
a.values.costItems[0].id === "5d235b4d86f7742e017bc88a"
) {
const aGPCost = a.values.costItems[0].price || 0;
const bGPCost = b.values.costItems[0].price || 0;
return aGPCost - bGPCost;
}
const aCost = a.values.cost || 0;
const bCost = b.values.cost || 0;
return aCost - bCost;
},
Cell: ({ value }) => {
return (
<CostItemsCell
costItems={value}
allowAllSources={showAll}
barters={useBarterIngredients ? barters : false}
crafts={useCraftIngredients ? crafts : false}
/>
);
},
},
{
Header: t("Cost ₽"),
id: "cost",
accessor: "cost",
Cell: (props) => {
if (props.row.original.cached) {
return (
<div className="center-content">
<FleaMarketLoadingIcon />
</div>
);
}
return <ValueCell value={props.value} valueCount={props.row.original.reward.count} />;
},
},
{
Header: t("Estimated savings"),
id: "savings",
accessor: (d) => Number(d.savings),
sortType: (a, b, columnId, desc) => {
const aSave = a.values.savings || (desc ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER);
const bSave = b.values.savings || (desc ? Number.MIN_SAFE_INTEGER : Number.MAX_SAFE_INTEGER);
return aSave - bSave;
},
Cell: (props) => {
if (props.row.original.cached) {
return (
<div className="center-content">
<FleaMarketLoadingIcon />
</div>
);
}
return (
<ValueCell value={props.value} highlightProfit valueDetails={props.row.original.savingsParts} />
);
},
},
{
Header: t("InstaProfit"),
id: "instaProfit",
accessor: "instaProfit",
sortType: (a, b, columnId, desc) => {
const aProf = a.values.instaProfit || 0;
const bProf = b.values.instaProfit || 0;
if (aProf === bProf) {
const aSave = a.values.savings || 0;
const bSave = b.values.savings || 0;
return aSave - bSave;
}
return aProf - bProf;
},
Cell: (props) => {
if (props.row.original.cached) {
return (
<div className="center-content">
<FleaMarketLoadingIcon />
</div>
);
}
return (
<ValueCell
value={props.value}
highlightProfit
valueDetails={props.row.original.instaProfitDetails}
>
<div className="duration-wrapper">
{props.row.original.instaProfitSource.vendor.normalizedName !== "unknown"
? props.row.original.instaProfitSource.vendor.name
: ""}
</div>
</ValueCell>
);
},
},
],
[t, showAll, useBarterIngredients, useCraftIngredients, barters, crafts],
);
const data = useMemo(() => {
let addedTraders = [];
setSkippedBySettings(false);
return barters
.filter((barter) => {
return !!barter.rewardItems[0];
})
.filter((barter) => {
if (!itemFilter) {
return true;
}
for (const requiredItem of barter.requiredItems) {
if (requiredItem === null) {
continue;
}
if (requiredItem.item.id === itemFilter) {
return true;
}
if (isBothDogtags(itemFilter) && isAnyDogtag(requiredItem.item.id)) {
return true;
}
if (isBothDogtags(requiredItem.item.id) && isAnyDogtag(itemFilter)) {
return true;
}
}
for (const rewardItem of barter.rewardItems) {
if (rewardItem.item.id === itemFilter) {
return true;
}
if (!rewardItem.item.containsItems) {
continue;
}
for (const contained of rewardItem.item.containsItems) {
if (!contained) {
continue;
}
if (contained.item.id === itemFilter) {
return true;
}
}
}
return false;
})
.filter((barter) => {
let traderNormalizedName = barter.trader.normalizedName;
let level = barter.level;
if (
!nameFilter &&
selectedTrader &&
selectedTrader !== "all" &&
selectedTrader !== traderNormalizedName
) {
return false;
}
if (!showAll && level > traders[traderNormalizedName]) {
setSkippedBySettings(true);
return false;
}
if (!showAll && barter.taskUnlock && settings.useTarkovTracker) {
if (!completedQuests.some((taskId) => taskId === barter.taskUnlock.id)) {
setSkippedBySettings(true);
return false;
}
}
if (removeDogtags) {
for (const requiredItem of barter.requiredItems) {
if (requiredItem === null) {
continue;
}
if (requiredItem.item.normalizedName.includes("dogtag")) {
setSkippedBySettings(true);
return false;
}
}
}
return true;
})
.filter((barter) => {
if (!nameFilter || nameFilter.length <= 0) {
return true;
}
const findString = nameFilter.toLowerCase().replace(/\s/g, "");
for (const requiredItem of barter.requiredItems) {
if (requiredItem === null) {
continue;
}
if (requiredItem.item.name.toLowerCase().replace(/\s/g, "").includes(findString)) {
return true;
}
}
for (const rewardItem of barter.rewardItems) {
if (rewardItem.item.name.toLowerCase().replace(/\s/g, "").includes(findString)) {
return true;
}
}
return false;
})
.filter((barter) => {
if (selectedTrader !== "all") {
return true;
}
if (selectedTrader === "all") {
return true;
}
if (addedTraders.includes(barter.reward.source)) {
return false;
}
addedTraders.push(barter.reward.source);
return true;
})
.map((barterRow) => {
let cost = 0;
const costItems = formatCostItems(barterRow.requiredItems, {
settings,
barters: useBarterIngredients ? barters : false,
crafts: useCraftIngredients ? crafts : false,
allowAllSources: showAll,
useBarterIngredients,
useCraftIngredients,
});
costItems.forEach((costItem) => (cost += costItem.pricePerUnit * costItem.count));
const barterRewardItem = barterRow.rewardItems[0].item;
let barterRewardContainedItem;
if (barterRewardItem.bsgCategoryId === "543be5cb4bdc2deb348b4568") {
// "ammo-container"
barterRewardContainedItem = items.find((i) => i.id === barterRewardItem.containsItems[0]?.item.id);
if (barterRewardContainedItem?.types.includes("noFlea")) {
barterRewardContainedItem = null;
}
}
const whatWeSell = barterRewardContainedItem ? barterRewardContainedItem : barterRewardItem;
const howManyWeSell = barterRewardContainedItem
? barterRewardItem.containsItems[0].count
: barterRow.rewardItems[0].count;
const bestSellTo = whatWeSell.sellFor.reduce(
(previousSellFor, currentSellFor) => {
if (
currentSellFor.vendor.normalizedName === "flea-market" &&
(handbook.fleaMarket.foundInRaidRequired || !handbook.fleaMarket.enabled)
) {
return previousSellFor;
}
if (currentSellFor.vendor.normalizedName === "jaeger" && !hasJaeger) {
return previousSellFor;
}
if (previousSellFor.priceRUB > currentSellFor.priceRUB) {
return previousSellFor;
}
return currentSellFor;
},
{
vendor: {
name: t("N/A"),
normalizedName: "unknown",
},
priceRUB: 0,
},
);
if (cost === 0 && costItems.length === 1 && costItems[0].id === "5d235b4d86f7742e017bc88a") {
// "gp-coin"
cost = bestSellTo.priceRUB * howManyWeSell;
const GPCoinPrice = cost / costItems[0].count;
costItems[0].price = GPCoinPrice;
costItems[0].priceRUB = GPCoinPrice;
costItems[0].pricePerUnit = GPCoinPrice;
}
let fleaFee = 0;
if (bestSellTo.vendor.normalizedName === "flea-market") {
fleaFee = fleaMarketFee(barterRewardItem.basePrice, bestSellTo.priceRUB, { count: howManyWeSell });
}
const tradeData = {
costItems: costItems,
cost: cost,
instaProfit: bestSellTo.priceRUB * howManyWeSell - cost - fleaFee,
instaProfitSource: bestSellTo,
instaProfitDetails: [
{
name: bestSellTo.vendor.name,
value: bestSellTo.priceRUB * howManyWeSell,
},
],
reward: {
item: barterRewardItem,
count: barterRow.rewardItems[0].count,
source: `${barterRow.trader.name} ${t("LL{{level}}", { level: barterRow.level })}`,
sellTo: bestSellTo.vendor.name,
sellToNormalized: bestSellTo.vendor.normalizedName,
sellValue: bestSellTo.priceRUB * howManyWeSell,
taskUnlock: barterRow.taskUnlock,
isFIR: false,
},
cached: barterRow.cached || barterRewardItem.cached,
};
if (fleaFee) {
tradeData.instaProfitDetails.push({
name: t("Flea Fee"),
value: fleaFee * -1,
});
}
tradeData.instaProfitDetails.push({
name: t("Barter cost"),
value: cost * -1,
});
if (barterRewardItem.priceCustom) {
tradeData.reward.sellValue = barterRewardItem.priceCustom;
tradeData.reward.sellType = "custom";
}
if (barterRewardContainedItem) {
// "ammo-container"
tradeData.reward.sellNote = t("Unpacked");
}
tradeData.savingsParts = [];
const cheapestPrice = getCheapestCashPrice(barterRewardItem, settings, showAll);
const cheapestBarter = getCheapestBarter(barterRewardItem, {
barters,
crafts: useCraftIngredients ? crafts : false,
settings,
useBarterIngredients,
useCraftIngredients,
allowAllSources: showAll,
});
if (cheapestPrice.type === "cash-sell") {
//this item cannot be purchased for cash
if (cheapestBarter) {
if (cheapestBarter.priceRUB !== cost) {
tradeData.savingsParts.push({
name: `${cheapestBarter.vendor.name} ${t("LL{{level}}", { level: cheapestBarter.vendor.minTraderLevel })} ${t("Barter")}`,
value: cheapestBarter.priceRUB,
});
}
tradeData.savings = cheapestBarter.priceRUB - cost;
}
} else if (cheapestPrice.type !== "none") {
// savings based on cheapest cash price
let sellerName = cheapestPrice.vendor.name;
if (cheapestPrice.vendor.minTraderLevel) {
sellerName += ` ${t("LL{{level}}", { level: cheapestPrice.vendor.minTraderLevel })}`;
}
tradeData.savingsParts.push({
name: sellerN
gitextract_ffd8hvvc/ ├── .devcontainer/ │ ├── Dockerfile │ ├── devcontainer.json │ └── docker-compose.yml ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ └── feature-request.yml │ ├── dependabot.yml │ ├── exclude.txt │ ├── new-pr-comment.md │ ├── pull_request_template.md │ └── workflows/ │ ├── branch-deploy.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── combine-prs.yml │ ├── deploy.yml │ ├── new-pr.yml │ └── unlock-on-merge.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .node-version ├── .nvmrc ├── .prettierignore ├── .stylelintignore ├── .vscode/ │ ├── launch.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── additional.d.ts ├── dependabot.yml ├── i18next-parser.config.mjs ├── package.json ├── prettier.config.mjs ├── public/ │ ├── browserconfig.xml │ ├── data/ │ │ └── .gitignore │ ├── index.html │ ├── robots.txt │ └── site.webmanifest ├── rsbuild.config.ts ├── rstest.config.ts ├── rstest.setup.ts ├── scripts/ │ ├── build-redirects.mjs │ ├── build-sitemap.mjs │ ├── critical.mjs │ ├── custom-loader.mjs │ ├── generate-thumbnails.mjs │ ├── generate_api-users_thumbs_macOS.sh │ ├── generate_items_thumbs.mjs │ ├── generate_items_thumbs_macOS.sh │ ├── generate_known_icons_macOS.sh │ ├── get-contributors.mjs │ ├── get-supported-languages.mjs │ ├── test-redirects.mjs │ └── update-props.mjs ├── src/ │ ├── App.css │ ├── App.jsx │ ├── __tests__/ │ │ ├── App.test.jsx │ │ ├── test-utils.js │ │ └── tsconfig.json │ ├── components/ │ │ ├── Debug.jsx │ │ ├── FilterIcon.jsx │ │ ├── FleaMarketLoadingIcon.jsx │ │ ├── Graph.jsx │ │ ├── GraphLabel.jsx │ │ ├── SEO.jsx │ │ ├── Symbol.jsx │ │ ├── Time.jsx │ │ ├── api-metrics-graph/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── barter-tooltip/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── barters-table/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── boss-list/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── canvas-grid/ │ │ │ └── index.jsx │ │ ├── center-cell/ │ │ │ └── index.jsx │ │ ├── cheeki-breeki-effect/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── contained-items-list/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── contributors/ │ │ │ └── index.jsx │ │ ├── cost-items-cell/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── countdown/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── crafts-table/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── data-table/ │ │ │ ├── Arrow.tsx │ │ │ ├── TableHead.tsx │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── em-item-tag/ │ │ │ └── index.jsx │ │ ├── filter/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── flea-price-cell/ │ │ │ └── index.jsx │ │ ├── footer/ │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── item-cost/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item-grid/ │ │ │ ├── Item.jsx │ │ │ ├── ItemIcon.jsx │ │ │ ├── ItemTooltip.jsx │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item-icon-list/ │ │ │ └── index.jsx │ │ ├── item-image/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item-name-cell/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item-search/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── items-for-hideout/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── items-summary-table/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── loading/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── loading-small/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── loyalty-level-icon/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── menu/ │ │ │ ├── CategoryMenu.jsx │ │ │ ├── MenuItem.jsx │ │ │ ├── alert-config.js │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ ├── menu-data.js │ │ │ └── useMenuOverflow.js │ │ ├── open-collective-button/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── patreon-button/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── points/ │ │ │ ├── Circle.jsx │ │ │ ├── Diamond.jsx │ │ │ ├── Plus.jsx │ │ │ ├── Square.jsx │ │ │ ├── TriangleDown.jsx │ │ │ ├── TriangleUp.jsx │ │ │ └── index.jsx │ │ ├── preset-selector/ │ │ │ └── index.jsx │ │ ├── price-graph/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── property-list/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── quest-items-cell/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── quest-table/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── remote-control-id/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── reward-cell/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── reward-image/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── scroll-to-top/ │ │ │ └── index.jsx │ │ ├── server-status/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── small-item-table/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── station-skill-trader-setting/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── supporter/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── supporters-list/ │ │ │ └── index.jsx │ │ ├── trader-image/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── trader-price-cell/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── trader-reset-time/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── ukraine-button/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ └── value-cell/ │ │ ├── index.css │ │ └── index.jsx │ ├── data/ │ │ ├── api-users.json │ │ ├── bosses.json │ │ ├── category-pages.json │ │ ├── game-modes.json │ │ ├── item-grids.json │ │ ├── maps.json │ │ ├── maps_static.json │ │ ├── patreons.json │ │ └── wipe-details.json │ ├── features/ │ │ ├── barters/ │ │ │ ├── do-fetch-barters.mjs │ │ │ └── index.js │ │ ├── crafts/ │ │ │ ├── do-fetch-crafts.mjs │ │ │ └── index.js │ │ ├── hideout/ │ │ │ ├── do-fetch-hideout.mjs │ │ │ └── index.js │ │ ├── items/ │ │ │ ├── do-fetch-items.mjs │ │ │ └── index.js │ │ ├── maps/ │ │ │ ├── do-fetch-maps.mjs │ │ │ └── index.js │ │ ├── quests/ │ │ │ ├── do-fetch-quests.mjs │ │ │ └── index.js │ │ ├── settings/ │ │ │ └── settingsSlice.mjs │ │ ├── sockets/ │ │ │ └── socketsSlice.js │ │ ├── status/ │ │ │ ├── do-fetch-status.mjs │ │ │ └── index.mjs │ │ └── traders/ │ │ ├── do-fetch-traders.mjs │ │ └── index.js │ ├── hooks/ │ │ ├── useDate.jsx │ │ ├── useKeyPress.jsx │ │ ├── useObserver.jsx │ │ ├── useRepositoryContributors.js │ │ └── useStateWithLocalStorage.jsx │ ├── i18n.js │ ├── index.jsx │ ├── modules/ │ │ ├── api-query.mjs │ │ ├── api-request.mjs │ │ ├── best-price.js │ │ ├── camelcase-to-dashes.js │ │ ├── capitalize-first.js │ │ ├── dogtags.js │ │ ├── flea-market-fee.mjs │ │ ├── format-ammo.mjs │ │ ├── format-category-name.js │ │ ├── format-cost-items.js │ │ ├── format-duration.js │ │ ├── format-price.js │ │ ├── graphql-request.mjs │ │ ├── item-can-contain.js │ │ ├── item-search.js │ │ ├── lang-helpers.js │ │ ├── leaflet-control-coordinates.js │ │ ├── leaflet-control-groupedlayer.js │ │ ├── leaflet-control-map-search.js │ │ ├── leaflet-control-map-settings.js │ │ ├── leaflet-control-raid-info.js │ │ ├── leaflet-control-remote.js │ │ ├── make-id.js │ │ ├── mui-theme.mjs │ │ ├── player-stats.mjs │ │ ├── polyfills.js │ │ ├── property-format.js │ │ ├── queue-browser-task.js │ │ ├── remote-websocket.mjs │ │ ├── task-elements.css │ │ ├── task-elements.mjs │ │ ├── window-focus-handler.mjs │ │ └── wipe-length.js │ ├── pages/ │ │ ├── about/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── achievements/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── ammo/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── api-docs/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── api-users/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── barters/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── bitcoin-farm-calculator/ │ │ │ ├── data.js │ │ │ ├── graph.jsx │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ ├── profit-info.jsx │ │ │ └── profitable-graph.jsx │ │ ├── boss/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── bosses/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── control/ │ │ │ ├── Connect.jsx │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── converter/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── crafts/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── error-page/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── hideout/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── item-tracker/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── items/ │ │ │ ├── armors/ │ │ │ │ └── index.jsx │ │ │ ├── backpacks/ │ │ │ │ └── index.jsx │ │ │ ├── barter-items/ │ │ │ │ └── index.jsx │ │ │ ├── bsg-category/ │ │ │ │ └── index.jsx │ │ │ ├── containers/ │ │ │ │ └── index.jsx │ │ │ ├── glasses/ │ │ │ │ └── index.jsx │ │ │ ├── grenades/ │ │ │ │ └── index.jsx │ │ │ ├── guns/ │ │ │ │ └── index.jsx │ │ │ ├── handbook-category/ │ │ │ │ └── index.jsx │ │ │ ├── headsets/ │ │ │ │ └── index.jsx │ │ │ ├── helmets/ │ │ │ │ └── index.jsx │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ ├── keys/ │ │ │ │ └── index.jsx │ │ │ ├── mods/ │ │ │ │ └── index.jsx │ │ │ ├── pistol-grips/ │ │ │ │ └── index.jsx │ │ │ ├── provisions/ │ │ │ │ └── index.jsx │ │ │ ├── rigs/ │ │ │ │ └── index.jsx │ │ │ └── suppressors/ │ │ │ └── index.jsx │ │ ├── loot-tiers/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── map/ │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ └── map-images.mjs │ │ ├── maps/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── moobot/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── nightbot/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── other-tools/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── player/ │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ └── player-forward.jsx │ │ ├── players/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── prestige/ │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ └── list.jsx │ │ ├── quest/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── quests/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── settings/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── start/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── stash-bot/ │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── stream-elements/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── tarkov-monitor/ │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── trader/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── traders/ │ │ │ ├── index.css │ │ │ └── index.jsx │ │ └── wipe-length/ │ │ ├── index.css │ │ └── index.jsx │ ├── serviceWorker.js │ ├── setupTests.js │ ├── store.js │ ├── styles/ │ │ ├── mapRemote.css │ │ ├── mapSearch.css │ │ ├── mapSettings.css │ │ └── singleEntity.css │ └── translations/ │ ├── de/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── en/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── es/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── fr/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── it/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── ja/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── pl/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── pt/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── remove_equal_key_value.py │ ├── ru/ │ │ ├── bosses.json │ │ ├── maps.json │ │ ├── properties.json │ │ └── translation.json │ ├── sync_key_value.py │ └── zh/ │ ├── bosses.json │ ├── maps.json │ ├── properties.json │ └── translation.json ├── stylelint.config.mjs ├── tsconfig.json ├── workers-site/ │ ├── .cargo-ok │ ├── .gitignore │ ├── index-template.js │ └── package.json └── wrangler.toml
SYMBOL INDEX (287 symbols across 168 files)
FILE: scripts/build-sitemap.mjs
function build_sitemap (line 106) | async function build_sitemap() {
function build_sitemap_items (line 166) | async function build_sitemap_items() {
function build_sitemap_index (line 201) | async function build_sitemap_index() {
FILE: scripts/custom-loader.mjs
function load (line 8) | async function load(url, context, defaultLoad) {
FILE: scripts/generate_items_thumbs.mjs
function shuffle (line 42) | function shuffle(array) {
FILE: scripts/get-contributors.mjs
function getContributors (line 28) | async function getContributors(repository) {
FILE: src/App.jsx
function Fallback (line 100) | function Fallback({ error, resetErrorBoundary }) {
function App (line 150) | function App() {
FILE: src/__tests__/test-utils.js
function renderWithProviders (line 10) | function renderWithProviders(
FILE: src/components/Debug.jsx
function Debug (line 3) | function Debug() {
FILE: src/components/FilterIcon.jsx
class FilterIcon (line 3) | class FilterIcon extends Component {
method render (line 4) | render() {
FILE: src/components/FleaMarketLoadingIcon.jsx
function FleaMarketLoadingIcon (line 6) | function FleaMarketLoadingIcon({ size = 1, tooltip }) {
FILE: src/components/Graph.jsx
constant MAX_DAMAGE (line 21) | const MAX_DAMAGE = 170;
constant MAX_PENETRATION (line 22) | const MAX_PENETRATION = 70;
FILE: src/components/GraphLabel.jsx
class GraphLabel (line 4) | class GraphLabel extends React.Component {
method render (line 7) | render() {
FILE: src/components/SEO.jsx
function SEO (line 4) | function SEO({ title, description, url, image, type = "article", card = ...
FILE: src/components/Symbol.jsx
constant SIZE (line 6) | const SIZE = 2;
class Symbol (line 8) | class Symbol extends Component {
method constructor (line 9) | constructor() {
method render (line 27) | render() {
FILE: src/components/Time.jsx
function hrs (line 20) | function hrs(num) {
function realTimeToTarkovTime (line 24) | function realTimeToTarkovTime(time, left) {
function timeUntilRelative (line 40) | function timeUntilRelative(until, left, date) {
function formattedTarkovTime (line 53) | function formattedTarkovTime(left = true) {
function formatFuture (line 59) | function formatFuture(ms) {
function MapDetails (line 79) | function MapDetails(props) {
function Time (line 106) | function Time(props) {
FILE: src/components/api-metrics-graph/index.jsx
constant API_METRICS_ENDPOINT (line 7) | const API_METRICS_ENDPOINT = "https://status.tarkov.dev/api/status-page/...
function ApiMetricsGraph (line 14) | function ApiMetricsGraph({ graph }) {
FILE: src/components/barter-tooltip/index.jsx
function BarterTooltip (line 18) | function BarterTooltip({
FILE: src/components/barters-table/index.jsx
function BartersTable (line 25) | function BartersTable({ selectedTrader, nameFilter, itemFilter, showAll,...
FILE: src/components/boss-list/index.jsx
function BossPageList (line 10) | function BossPageList() {
function BossListNav (line 41) | function BossListNav(onClick) {
function BossList (line 71) | function BossList() {
FILE: src/components/canvas-grid/index.jsx
function CanvasGrid (line 3) | function CanvasGrid(props) {
FILE: src/components/cheeki-breeki-effect/index.jsx
function CheekiBreekiEffect (line 5) | function CheekiBreekiEffect() {
FILE: src/components/contributors/index.jsx
function Contributors (line 9) | function Contributors(props) {
FILE: src/components/cost-items-cell/index.jsx
function CostItemsCell (line 12) | function CostItemsCell({
FILE: src/components/countdown/index.jsx
function WipeCountdown (line 19) | function WipeCountdown() {
FILE: src/components/crafts-table/index.jsx
function CraftTable (line 25) | function CraftTable({
function getLocalFinishes (line 574) | function getLocalFinishes(time, t) {
FILE: src/components/data-table/Arrow.tsx
function ArrowIcon (line 3) | function ArrowIcon({
FILE: src/components/data-table/TableHead.tsx
function TableHead (line 7) | function TableHead({
FILE: src/components/data-table/index.jsx
function DataTable (line 10) | function DataTable({
FILE: src/components/em-item-tag/index.jsx
function EmItemTag (line 3) | function EmItemTag({ children }) {
FILE: src/components/filter/index.jsx
function ButtonGroupFilterButton (line 16) | function ButtonGroupFilterButton({ tooltipContent, onClick, content, sel...
function ButtonGroupFilter (line 29) | function ButtonGroupFilter({ children }) {
function SliderFilter (line 33) | function SliderFilter({
function RangeFilter (line 79) | function RangeFilter({
function ToggleFilter (line 124) | function ToggleFilter({ label, onChange, checked, tooltipContent, disabl...
function SelectFilter (line 202) | function SelectFilter({
function SelectItemFilter (line 265) | function SelectItemFilter({
function InputFilter (line 344) | function InputFilter({
function Filter (line 389) | function Filter({ center, children, fullWidth }) {
FILE: src/components/footer/index.tsx
function Footer (line 14) | function Footer() {
FILE: src/components/item-cost/index.jsx
function ItemCost (line 19) | function ItemCost({
FILE: src/components/item-grid/Item.jsx
function Item (line 19) | function Item(props) {
FILE: src/components/item-grid/ItemIcon.jsx
function ItemIcon (line 1) | function ItemIcon(props) {
FILE: src/components/item-grid/ItemTooltip.jsx
function ItemTooltip (line 5) | function ItemTooltip(props) {
FILE: src/components/item-grid/index.jsx
function ItemGrid (line 26) | function ItemGrid(props) {
FILE: src/components/item-icon-list/index.jsx
function ItemIconList (line 53) | function ItemIconList(key) {
FILE: src/components/item-image/index.jsx
function ItemImage (line 23) | function ItemImage({
FILE: src/components/item-name-cell/index.jsx
function ItemNameCell (line 7) | function ItemNameCell(props) {
FILE: src/components/item-search/index.jsx
function ItemSearch (line 17) | function ItemSearch({
FILE: src/components/items-for-hideout/index.jsx
function ItemsForHideout (line 11) | function ItemsForHideout(props) {
FILE: src/components/items-summary-table/index.jsx
function ItemsSummaryTable (line 39) | function ItemsSummaryTable({ includeItems, includeTraders, includeStatio...
FILE: src/components/loading-small/index.jsx
function LoadingSmall (line 5) | function LoadingSmall() {
FILE: src/components/loading/index.jsx
function Loading (line 5) | function Loading() {
FILE: src/components/loyalty-level-icon/index.jsx
function loyaltyLevelIcon (line 3) | function loyaltyLevelIcon({ loyaltyLevel }) {
FILE: src/components/menu/MenuItem.jsx
function MenuItem (line 6) | function MenuItem(props) {
FILE: src/components/menu/menu-data.js
constant CATEGORIES (line 1) | const CATEGORIES = {
FILE: src/components/open-collective-button/index.jsx
function OpenCollectiveButton (line 5) | function OpenCollectiveButton({ large = false, linkStyle }) {
FILE: src/components/patreon-button/index.jsx
function PatreonButton (line 5) | function PatreonButton({ onlyLarge, linkStyle, wrapperStyle, text, child...
FILE: src/components/points/Circle.jsx
class Circle (line 3) | class Circle extends Component {
method render (line 4) | render() {
FILE: src/components/points/Diamond.jsx
class TriangleUp (line 3) | class TriangleUp extends Component {
method render (line 4) | render() {
FILE: src/components/points/Plus.jsx
class Plus (line 3) | class Plus extends Component {
method render (line 4) | render() {
FILE: src/components/points/Square.jsx
class Square (line 3) | class Square extends Component {
method render (line 4) | render() {
FILE: src/components/points/TriangleDown.jsx
class TriangleUp (line 3) | class TriangleUp extends Component {
method render (line 4) | render() {
FILE: src/components/points/TriangleUp.jsx
class TriangleUp (line 3) | class TriangleUp extends Component {
method render (line 4) | render() {
FILE: src/components/preset-selector/index.jsx
function PresetSelector (line 7) | function PresetSelector({ item, alt = "" }) {
FILE: src/components/price-graph/index.jsx
function PriceGraph (line 20) | function PriceGraph({ item, itemId, days }) {
FILE: src/components/property-list/index.jsx
function PropertyList (line 14) | function PropertyList({ properties, id }) {
FILE: src/components/quest-items-cell/index.jsx
function QuestItemsCell (line 13) | function QuestItemsCell({ questItems }) {
FILE: src/components/quest-table/index.jsx
function getRequiredQuestItems (line 19) | function getRequiredQuestItems(quest, itemFilter = false) {
function getRewardQuestItems (line 110) | function getRewardQuestItems(quest, itemFilter = false) {
function QuestTable (line 150) | function QuestTable({
FILE: src/components/remote-control-id/index.jsx
function ID (line 13) | function ID(props) {
FILE: src/components/reward-cell/index.jsx
function RewardCell (line 17) | function RewardCell({
FILE: src/components/reward-image/index.jsx
function RewardImage (line 3) | function RewardImage({
FILE: src/components/scroll-to-top/index.jsx
function ScrollToTop (line 4) | function ScrollToTop() {
FILE: src/components/server-status/index.jsx
function ServerStatus (line 9) | function ServerStatus() {
FILE: src/components/small-item-table/index.jsx
function getItemCountPrice (line 35) | function getItemCountPrice(item) {
function TraderSellCell (line 46) | function TraderSellCell(datum, showSlotValue = false) {
function shuffleArray (line 101) | function shuffleArray(array, randomSeeds) {
function SmallItemTable (line 163) | function SmallItemTable(props) {
FILE: src/components/supporter/index.jsx
function Supporter (line 6) | function Supporter(props) {
FILE: src/components/supporters-list/index.jsx
function SupportersList (line 7) | function SupportersList({ tierFilter, typeFilter, type }) {
FILE: src/components/trader-image/index.jsx
function TraderImage (line 6) | function TraderImage({ trader, image = "icon", reputationChange, style =...
FILE: src/components/trader-price-cell/index.jsx
function getItemCountPrice (line 12) | function getItemCountPrice(price, currency = "RUB", count = 1) {
function TraderPriceCell (line 23) | function TraderPriceCell(props) {
FILE: src/components/trader-reset-time/index.jsx
function TraderResetTime (line 34) | function TraderResetTime({ timestamp, center = false, locale = "en" }) {
FILE: src/components/ukraine-button/index.jsx
function UkraineButton (line 5) | function UkraineButton({ large = false, linkStyle }) {
FILE: src/components/value-cell/index.jsx
function ValueCell (line 8) | function ValueCell(props) {
FILE: src/features/barters/do-fetch-barters.mjs
class BartersQuery (line 3) | class BartersQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/barters/index.js
function useBartersData (line 169) | function useBartersData() {
FILE: src/features/crafts/do-fetch-crafts.mjs
class CraftsQuery (line 3) | class CraftsQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
function doFetchCrafts (line 43) | async function doFetchCrafts(options) {
FILE: src/features/crafts/index.js
function useCraftsData (line 183) | function useCraftsData() {
FILE: src/features/hideout/do-fetch-hideout.mjs
class HideoutQuery (line 3) | class HideoutQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/hideout/index.js
function useHideoutData (line 75) | function useHideoutData() {
FILE: src/features/items/do-fetch-items.mjs
class ItemsQuery (line 6) | class ItemsQuery extends APIQuery {
method constructor (line 7) | constructor() {
method query (line 11) | async query(options) {
FILE: src/features/items/index.js
function processFetchedItems (line 11) | function processFetchedItems(allData) {
function useItemsData (line 131) | function useItemsData() {
function useHandbookData (line 171) | function useHandbookData() {
FILE: src/features/maps/do-fetch-maps.mjs
class MapsQuery (line 3) | class MapsQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/maps/index.js
function useMapsData (line 124) | function useMapsData() {
function useBossesData (line 279) | function useBossesData() {
FILE: src/features/quests/do-fetch-quests.mjs
class QuestsQuery (line 3) | class QuestsQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/quests/index.js
function useAchievementsData (line 174) | function useAchievementsData() {
function useQuestsData (line 254) | function useQuestsData() {
FILE: src/features/status/do-fetch-status.mjs
class StatusQuery (line 3) | class StatusQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/status/index.mjs
function useStatusData (line 50) | function useStatusData() {
FILE: src/features/traders/do-fetch-traders.mjs
class TradersQuery (line 3) | class TradersQuery extends APIQuery {
method constructor (line 4) | constructor() {
method query (line 8) | async query(options) {
FILE: src/features/traders/index.js
function useTradersData (line 86) | function useTradersData() {
FILE: src/hooks/useDate.jsx
function useDate (line 3) | function useDate(initial, updateSpeed) {
FILE: src/hooks/useKeyPress.jsx
function useKeyPress (line 3) | function useKeyPress(targetKey) {
FILE: src/hooks/useRepositoryContributors.js
function normalizeContributors (line 5) | function normalizeContributors(rawContributors = []) {
function useRepositoryContributors (line 16) | function useRepositoryContributors(repository) {
FILE: src/modules/api-query.mjs
class APIQuery (line 12) | class APIQuery {
method constructor (line 13) | constructor(queryName, cacheMinutes = 5) {
method apiRequest (line 19) | apiRequest(path, options) {
method graphqlRequest (line 23) | graphqlRequest(queryString, variables) {
method query (line 27) | async query() {
method run (line 31) | async run(options = defaultOptions) {
FILE: src/modules/api-request.mjs
function apiRequest (line 21) | async function apiRequest(path, options = {}) {
FILE: src/modules/best-price.js
function bestPrice (line 3) | function bestPrice(itemData, Ti = false, Tr = false, startPrice) {
FILE: src/modules/camelcase-to-dashes.js
function camelCaseToDashes (line 4) | function camelCaseToDashes(input) {
FILE: src/modules/capitalize-first.js
function capitalizeTheFirstLetterOfEachWord (line 1) | function capitalizeTheFirstLetterOfEachWord(words) {
FILE: src/modules/dogtags.js
function isAnyDogtag (line 1) | function isAnyDogtag(id) {
function isBothDogtags (line 5) | function isBothDogtags(id) {
function getDogTagCost (line 9) | function getDogTagCost(requiredItem, settings = { minDogtagLevel: 1 }) {
FILE: src/modules/flea-market-fee.mjs
function fleaMarketFee (line 5) | function fleaMarketFee(basePrice, sellPrice, options = {}) {
FILE: src/modules/format-ammo.mjs
function caliberArrayWithSplit (line 31) | function caliberArrayWithSplit() {
FILE: src/modules/format-cost-items.js
function getCheapestCashPrice (line 8) | function getCheapestCashPrice(item, settings = {}, allowAllSources = fal...
function getItemBarters (line 68) | function getItemBarters(item, barters, settings, allowAllSources) {
function getBarterCost (line 108) | function getBarterCost(
function getCheapestBarter (line 151) | function getCheapestBarter(
function getItemCrafts (line 212) | function getItemCrafts(item, crafts, settings, allowAllSources) {
function getCheapestCraft (line 239) | function getCheapestCraft(
function getCheapestPrice (line 308) | function getCheapestPrice(
FILE: src/modules/format-duration.js
function getDurationDisplay (line 8) | function getDurationDisplay(time) {
function getFinishDisplay (line 30) | function getFinishDisplay(time) {
function getRelativeTimeAndUnit (line 44) | function getRelativeTimeAndUnit(d1, d2 = new Date()) {
FILE: src/modules/format-price.js
function formatPrice (line 1) | function formatPrice(price, currency = "RUB") {
FILE: src/modules/graphql-request.mjs
function graphqlRequest (line 8) | async function graphqlRequest(queryString, variables) {
function useQuery (line 36) | function useQuery(queryName, queryString, settings) {
FILE: src/modules/item-can-contain.js
function itemCanContain (line 1) | function itemCanContain(item, containedItem, containerType = false) {
FILE: src/modules/lang-helpers.js
function validateLangCode (line 7) | function validateLangCode(lng) {
function langCode (line 20) | function langCode() {
function useLangCode (line 42) | function useLangCode() {
FILE: src/modules/leaflet-control-map-search.js
function debounce (line 400) | function debounce(func, delay) {
FILE: src/modules/make-id.js
function makeID (line 1) | function makeID(length) {
FILE: src/modules/polyfills.js
function loadPolyfills (line 1) | async function loadPolyfills() {
FILE: src/modules/remote-websocket.mjs
class RemoteWebSocket (line 4) | class RemoteWebSocket extends WebSocket {
method constructor (line 5) | constructor(sessionId) {
method heartbeat (line 28) | heartbeat() {
FILE: src/modules/task-elements.mjs
function CustomizationReward (line 24) | function CustomizationReward(reward, items, settings) {
function TaskObjective (line 55) | function TaskObjective({ objective, items, bosses, quests, traders, maps...
function TaskRewards (line 696) | function TaskRewards({ rewards, t, items, settings, traders, stations, a...
FILE: src/modules/wipe-length.js
function averageWipeLength (line 35) | function averageWipeLength() {
function wipeDetails (line 51) | function wipeDetails() {
function currentWipeLength (line 55) | function currentWipeLength() {
FILE: src/pages/about/index.jsx
function About (line 16) | function About() {
FILE: src/pages/achievements/index.jsx
function Achievements (line 33) | function Achievements() {
FILE: src/pages/ammo/index.jsx
constant MAX_DAMAGE (line 30) | const MAX_DAMAGE = 170;
constant MAX_PENETRATION (line 31) | const MAX_PENETRATION = 70;
constant MAX_PRICE_CEILING (line 32) | const MAX_PRICE_CEILING = 3000;
function Ammo (line 53) | function Ammo() {
FILE: src/pages/api-docs/index.jsx
function APIDocs (line 11) | function APIDocs() {
FILE: src/pages/api-users/index.jsx
function ApiUsers (line 9) | function ApiUsers() {
FILE: src/pages/barters/index.jsx
function Barters (line 26) | function Barters() {
FILE: src/pages/boss/index.jsx
function BossPage (line 34) | function BossPage(params) {
function Boss (line 579) | function Boss() {
FILE: src/pages/bosses/index.jsx
function Bosses (line 13) | function Bosses(props) {
FILE: src/pages/control/Connect.jsx
function Connect (line 8) | function Connect() {
FILE: src/pages/control/index.jsx
function Control (line 60) | function Control(props) {
FILE: src/pages/converter/index.jsx
function Converter (line 17) | function Converter() {
FILE: src/pages/crafts/index.jsx
function Crafts (line 24) | function Crafts() {
FILE: src/pages/error-page/index.jsx
function ErrorPage (line 9) | function ErrorPage(props) {
FILE: src/pages/hideout/index.jsx
function Hideout (line 18) | function Hideout() {
FILE: src/pages/item-tracker/index.jsx
function ItemTracker (line 17) | function ItemTracker() {
FILE: src/pages/item/index.jsx
function TraderPrice (line 63) | function TraderPrice({ currency, price, priceRUB }) {
function Item (line 75) | function Item() {
FILE: src/pages/items/armors/index.jsx
function Armors (line 22) | function Armors(props) {
FILE: src/pages/items/backpacks/index.jsx
function Backpacks (line 11) | function Backpacks() {
FILE: src/pages/items/barter-items/index.jsx
function BarterItems (line 13) | function BarterItems() {
FILE: src/pages/items/bsg-category/index.jsx
function BsgCategory (line 14) | function BsgCategory() {
FILE: src/pages/items/containers/index.jsx
function Containers (line 11) | function Containers(props) {
FILE: src/pages/items/glasses/index.jsx
function Glasses (line 11) | function Glasses() {
FILE: src/pages/items/grenades/index.jsx
function Grenades (line 13) | function Grenades() {
FILE: src/pages/items/guns/index.jsx
function Guns (line 13) | function Guns() {
FILE: src/pages/items/handbook-category/index.jsx
function HandbookCategory (line 14) | function HandbookCategory() {
FILE: src/pages/items/headsets/index.jsx
function Headsets (line 13) | function Headsets() {
FILE: src/pages/items/helmets/index.jsx
function Helmets (line 22) | function Helmets() {
FILE: src/pages/items/index.jsx
function Items (line 15) | function Items(props) {
FILE: src/pages/items/keys/index.jsx
function Keys (line 13) | function Keys() {
FILE: src/pages/items/mods/index.jsx
function Mods (line 13) | function Mods() {
FILE: src/pages/items/pistol-grips/index.jsx
function PistolGrips (line 13) | function PistolGrips() {
FILE: src/pages/items/provisions/index.jsx
function Provisions (line 13) | function Provisions() {
FILE: src/pages/items/rigs/index.jsx
function Rigs (line 24) | function Rigs() {
FILE: src/pages/items/suppressors/index.jsx
function Suppressors (line 13) | function Suppressors() {
FILE: src/pages/loot-tiers/index.jsx
constant DEFAULT_MAX_ITEMS (line 27) | const DEFAULT_MAX_ITEMS = 256;
function LootTier (line 43) | function LootTier(props) {
FILE: src/pages/map/index.jsx
function getCRS (line 44) | function getCRS(mapData) {
function applyRotation (line 70) | function applyRotation(latLng, rotation) {
function pos (line 88) | function pos(position) {
function getScaledBounds (line 92) | function getScaledBounds(bounds, scaleFactor) {
function checkMarkerBounds (line 116) | function checkMarkerBounds(position, markerBounds) {
function getBounds (line 131) | function getBounds(bounds) {
function markerIsOnLayer (line 139) | function markerIsOnLayer(marker, layer) {
function markerIsOnActiveLayer (line 166) | function markerIsOnActiveLayer(marker) {
function checkMarkerForActiveLayers (line 200) | function checkMarkerForActiveLayers(event) {
function mouseHoverOutline (line 224) | function mouseHoverOutline(event) {
function toggleForceOutline (line 233) | function toggleForceOutline(event) {
function activateMarkerLayer (line 242) | function activateMarkerLayer(event) {
function outlineToPoly (line 262) | function outlineToPoly(outline) {
function addElevation (line 269) | function addElevation(item, popup) {
function Map (line 281) | function Map() {
FILE: src/pages/maps/index.jsx
function Maps (line 14) | function Maps() {
FILE: src/pages/moobot/index.jsx
function Moobot (line 7) | function Moobot() {
FILE: src/pages/nightbot/index.jsx
function Nightbot (line 7) | function Nightbot() {
FILE: src/pages/other-tools/index.jsx
function OtherTools (line 14) | function OtherTools() {
FILE: src/pages/player/index.jsx
function getHMS (line 37) | function getHMS(seconds) {
function Player (line 154) | function Player() {
FILE: src/pages/player/player-forward.jsx
function PlayerForward (line 4) | function PlayerForward() {
FILE: src/pages/players/index.jsx
function Players (line 22) | function Players() {
FILE: src/pages/prestige/index.jsx
function Prestige (line 26) | function Prestige() {
FILE: src/pages/prestige/list.jsx
function Prestiges (line 13) | function Prestiges() {
FILE: src/pages/quest/index.jsx
function Quest (line 45) | function Quest() {
FILE: src/pages/quests/index.jsx
function Quests (line 24) | function Quests() {
FILE: src/pages/settings/index.jsx
function getNumericSelect (line 54) | function getNumericSelect(min, max) {
function Settings (line 67) | function Settings() {
FILE: src/pages/start/index.jsx
function Start (line 43) | function Start() {
FILE: src/pages/stash-bot/index.js
constant INVITE_URL (line 10) | const INVITE_URL =
constant REPOSITORY_URL (line 12) | const REPOSITORY_URL = "https://github.com/the-hideout/stash";
constant ISSUE_URL (line 13) | const ISSUE_URL = "https://github.com/the-hideout/stash/issues";
function StashBotPage (line 139) | function StashBotPage() {
FILE: src/pages/stream-elements/index.jsx
function StreamElements (line 7) | function StreamElements() {
FILE: src/pages/tarkov-monitor/index.js
constant RELEASE_URL (line 9) | const RELEASE_URL = "https://github.com/the-hideout/TarkovMonitor/releas...
constant REPOSITORY_URL (line 10) | const REPOSITORY_URL = "https://github.com/the-hideout/TarkovMonitor";
constant DISCORD_URL (line 11) | const DISCORD_URL = "https://discord.gg/XPAsKGHSzH";
function TarkovMonitorPage (line 12) | function TarkovMonitorPage() {
FILE: src/pages/trader/index.jsx
function Trader (line 41) | function Trader() {
FILE: src/pages/traders/index.jsx
function Traders (line 18) | function Traders(props) {
FILE: src/pages/wipe-length/index.jsx
function getAverageWipeLength (line 119) | function getAverageWipeLength() {
function getWipeData (line 123) | function getWipeData() {
FILE: src/serviceWorker.js
function register (line 21) | function register(config) {
function registerValidSW (line 55) | function registerValidSW(swUrl, config) {
function checkValidServiceWorker (line 99) | function checkValidServiceWorker(swUrl, config) {
function unregister (line 124) | function unregister() {
FILE: workers-site/index-template.js
constant DEBUG (line 24) | const DEBUG = false;
function handleEvent (line 42) | async function handleEvent(event) {
function handlePrefix (line 111) | function handlePrefix(prefix) {
Condensed preview — 399 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,684K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 173,
"preview": "FROM node:20\nEXPOSE 3000\n\nENV APP_ROOT /tarkov-dev\nRUN mkdir -p $APP_ROOT\nWORKDIR $APP_ROOT\n\nCMD [\"/bin/bash\", \"-c\", \"np"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 1619,
"preview": "{\n \"name\": \"Tarkov-Dev Local Dev\",\n \"dockerComposeFile\": \"docker-compose.yml\",\n \"service\": \"dev\",\n \"workspac"
},
{
"path": ".devcontainer/docker-compose.yml",
"chars": 157,
"preview": "services:\n dev:\n build:\n context: ..\n dockerfile: ./.devcontainer/Dockerfile\n volumes:\n - ..:/tark"
},
{
"path": ".github/CODEOWNERS",
"chars": 77,
"preview": "# Default reviewers for all files in the repo\n* @the-hideout/reviewers\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 74,
"preview": "# These are supported funding model platforms\nopen_collective: tarkov-dev\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug.yml",
"chars": 2169,
"preview": "name: Bug Report\ndescription: File a bug/issue report\nlabels: [\"bug\"]\nbody:\n - type: markdown\n attributes:\n val"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 27,
"preview": "blank_issues_enabled: true\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature-request.yml",
"chars": 586,
"preview": "name: Feature Request\ndescription: Suggest a new feature or enhancement\nlabels: [\"enhancement\"]\nbody:\n - type: markdown"
},
{
"path": ".github/dependabot.yml",
"chars": 1108,
"preview": "# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repositor"
},
{
"path": ".github/exclude.txt",
"chars": 117,
"preview": "# custom exclude file for the GrantBirki/json-yaml-validate@vX.X.X Actions workflow\n\n.devcontainer/devcontainer.json\n"
},
{
"path": ".github/new-pr-comment.md",
"chars": 794,
"preview": "### 👋 Thanks for opening a pull request!\n\nIf you are new, please check out the trimmed down summary of our deployment pr"
},
{
"path": ".github/pull_request_template.md",
"chars": 1246,
"preview": "<!-- \n\n⚠️ IMPORTANT ⚠️\n\n- Please fill in all sections [in brackets]\n- Please read the collapsible sections if you need h"
},
{
"path": ".github/workflows/branch-deploy.yml",
"chars": 3549,
"preview": "name: branch-deploy\n\non:\n issue_comment:\n types: [ created ]\n\n# Permissions needed for reacting and adding comments "
},
{
"path": ".github/workflows/ci.yml",
"chars": 989,
"preview": "name: ci\n\non:\n push:\n branches:\n - main\n pull_request:\n branches: [ main ]\n\npermissions:\n contents: read\n "
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 877,
"preview": "name: CodeQL\n\non:\n push:\n branches: [ main ]\n schedule:\n - cron: '29 10 * * 4'\n\njobs:\n analyze:\n name: Analy"
},
{
"path": ".github/workflows/combine-prs.yml",
"chars": 457,
"preview": "name: Combine PRs\n\non:\n schedule:\n - cron: \"0 1 * * 3\" # Wednesday at 01:00\n workflow_dispatch:\n\njobs:\n combine-pr"
},
{
"path": ".github/workflows/deploy.yml",
"chars": 3067,
"preview": "name: deploy\n\non:\n push:\n branches:\n - main\n\npermissions:\n contents: read\n\njobs:\n deploy:\n if: github.even"
},
{
"path": ".github/workflows/new-pr.yml",
"chars": 492,
"preview": "name: New Pull Request\n\non:\n pull_request:\n branches:\n - main\n\npermissions:\n pull-requests: write\n\njobs:\n com"
},
{
"path": ".github/workflows/unlock-on-merge.yml",
"chars": 478,
"preview": "name: Unlock On Merge\n\non:\n pull_request:\n types: [closed]\n\npermissions:\n contents: write\n\njobs:\n unlock-on-merge:"
},
{
"path": ".gitignore",
"chars": 6857,
"preview": "# Created by https://www.toptal.com/developers/gitignore/api/node,macos,react,windows,jetbrains,visualstudiocode\n# Edit "
},
{
"path": ".husky/pre-commit",
"chars": 16,
"preview": "npx lint-staged\n"
},
{
"path": ".node-version",
"chars": 7,
"preview": "24.11.1"
},
{
"path": ".nvmrc",
"chars": 7,
"preview": "24.11.1"
},
{
"path": ".prettierignore",
"chars": 66,
"preview": "# Ignore artifacts:\nbuild\ndist\n\n# Ignore all HTML files:\n**/*.html"
},
{
"path": ".stylelintignore",
"chars": 66,
"preview": "# Ignore artifacts:\nbuild\ndist\n\n# Ignore all HTML files:\n**/*.html"
},
{
"path": ".vscode/launch.json",
"chars": 283,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"type\": \"chrome\",\n \"request\": \"launch"
},
{
"path": ".vscode/settings.json",
"chars": 48,
"preview": "{\n \"cSpell.words\": [\n \"Tarkov\"\n ]\n}"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5230,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "CONTRIBUTING.md",
"chars": 2801,
"preview": "# How to Contribute to tarkov-dev 💻\r\n\r\n> This contributing guide is specfic to the [tarkov-dev](https://github.com/the-h"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2019 Oskar Risberg\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 6334,
"preview": "# tarkov.dev 💻\n\n[](https://gi"
},
{
"path": "SECURITY.md",
"chars": 298,
"preview": "# Security Policy 🔒\n\n## Supported Versions\n\nThe `main` branch of this repo is considered active and supported for all se"
},
{
"path": "additional.d.ts",
"chars": 400,
"preview": "declare const __COMMIT_HASH__: string;\ndeclare const __BRANCH_NAME__: string;\n\ndeclare module \"*.svg\" {\n export const"
},
{
"path": "dependabot.yml",
"chars": 583,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"npm\"\n directory: \"/\"\n schedule:\n interval: \"monthly\"\n ignore:\n"
},
{
"path": "i18next-parser.config.mjs",
"chars": 4713,
"preview": "// i18next-parser.config.mjs\n\nconst config = {\n contextSeparator: \"_\",\n // Key separator used in your translation "
},
{
"path": "package.json",
"chars": 3921,
"preview": "{\n \"name\": \"tarkov.dev\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"imports\": {\n \"#src/*\": \"./s"
},
{
"path": "prettier.config.mjs",
"chars": 254,
"preview": "/**\n * @see https://prettier.io/docs/configuration\n * @type {import(\"prettier\").Config}\n */\nconst config = {\n printWi"
},
{
"path": "public/browserconfig.xml",
"chars": 360,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n <msapplication>\n <tile>\n <square70x70logo s"
},
{
"path": "public/data/.gitignore",
"chars": 14,
"preview": "*\n!.gitignore\n"
},
{
"path": "public/index.html",
"chars": 7429,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"widt"
},
{
"path": "public/robots.txt",
"chars": 68,
"preview": "User-agent: *\nAllow: /\nSitemap: https://tarkov.dev/sitemap_index.xml"
},
{
"path": "public/site.webmanifest",
"chars": 485,
"preview": "{\n \"name\": \"Tarkov.dev\",\n \"short_name\": \"Tarkov.dev\",\n \"icons\": [\n {\n \"src\": \"/android-chrome"
},
{
"path": "rsbuild.config.ts",
"chars": 736,
"preview": "import { defineConfig } from \"@rsbuild/core\";\nimport { pluginReact } from \"@rsbuild/plugin-react\";\nimport { pluginSvgr }"
},
{
"path": "rstest.config.ts",
"chars": 236,
"preview": "import { defineConfig } from \"@rstest/core\";\nimport rsbuildConfig from \"./rsbuild.config\";\n\nexport default defineConfig("
},
{
"path": "rstest.setup.ts",
"chars": 352,
"preview": "import \"dotenv/config\";\nimport \"@testing-library/jest-dom\";\n\nimport { resetIntersectionMocking, setupIntersectionMocking"
},
{
"path": "scripts/build-redirects.mjs",
"chars": 794,
"preview": "import fs from \"fs\";\nimport path from \"path\";\nimport url from \"url\";\n\n(async () => {\n let redirects;\n\n try {\n "
},
{
"path": "scripts/build-sitemap.mjs",
"chars": 6972,
"preview": "import { writeFileSync } from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport { createGzip }"
},
{
"path": "scripts/critical.mjs",
"chars": 391,
"preview": "import * as critical from \"critical\";\n\ncritical.generate(\n {\n base: \"build/\",\n src: \"./index.html\",\n "
},
{
"path": "scripts/custom-loader.mjs",
"chars": 1146,
"preview": "import { URL } from \"url\";\nimport { readFile } from \"fs/promises\";\n\n/**\n * This function forces .mjs files to be loades "
},
{
"path": "scripts/generate-thumbnails.mjs",
"chars": 2006,
"preview": "import fs from \"fs/promises\";\nimport sharp from \"sharp\";\n\n(async () => {\n console.time(\"Generating thumbnails\");\n\n "
},
{
"path": "scripts/generate_api-users_thumbs_macOS.sh",
"chars": 685,
"preview": "#!/bin/bash\n\n# Max height from css \".api-users-page-wrapper img\"\nHEIGHT=150\n\ncd ../public/images/api-users/\n\n# Remove ol"
},
{
"path": "scripts/generate_items_thumbs.mjs",
"chars": 5365,
"preview": "import fs from \"fs\";\nimport path from \"path\";\nimport sharp from \"sharp\";\nimport { exit } from \"process\";\n\n//import apiRe"
},
{
"path": "scripts/generate_items_thumbs_macOS.sh",
"chars": 1164,
"preview": "#!/bin/bash\n\n# Max height and width\nHEIGHT=144\nWIDTH=256\n\ncd ../public/images/items/\n\n# Remove old thumbs\nrm *_thumb.jpg"
},
{
"path": "scripts/generate_known_icons_macOS.sh",
"chars": 1241,
"preview": "#!/bin/bash\n\n# Max height and width\nHEIGHT=64\nWIDTH=64\nCWD=\"$(pwd)\"\n\ncd ../public/images/traders/\n\n# Remove old icons\nrm"
},
{
"path": "scripts/get-contributors.mjs",
"chars": 5122,
"preview": "import fs from \"fs\";\nimport path from \"path\";\nimport url from \"url\";\n\nconst repositories = [\n \"the-hideout/tarkov-dev"
},
{
"path": "scripts/get-supported-languages.mjs",
"chars": 468,
"preview": "import fs from \"fs\";\nimport apiRequest from \"../src/modules/api-request.mjs\";\n\ntry {\n const endpoints = await apiRequ"
},
{
"path": "scripts/test-redirects.mjs",
"chars": 1371,
"preview": "import fs from \"fs\";\nimport path from \"path\";\nimport url from \"url\";\n\nimport redirects from \"../workers-site/redirects.j"
},
{
"path": "scripts/update-props.mjs",
"chars": 487,
"preview": "import fs from \"fs\";\nimport path from \"path\";\nimport url from \"url\";\n\nconst files = [\n //'item-props',\n \"item-grid"
},
{
"path": "src/App.css",
"chars": 5886,
"preview": "@import url(\"./styles/singleEntity.css\");\n@import url(\"./styles/mapSearch.css\");\n\n/* Variables start */\n:root {\n --co"
},
{
"path": "src/App.jsx",
"chars": 47204,
"preview": "/* eslint-disable no-restricted-globals */\nimport React, { useEffect, useCallback, useRef, Suspense } from \"react\";\nimpo"
},
{
"path": "src/__tests__/App.test.jsx",
"chars": 751,
"preview": "import { expect, it } from \"@rstest/core\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimp"
},
{
"path": "src/__tests__/test-utils.js",
"chars": 754,
"preview": "// https://redux.js.org/usage/writing-tests#components\n\nimport React from \"react\";\nimport { render } from \"@testing-libr"
},
{
"path": "src/__tests__/tsconfig.json",
"chars": 166,
"preview": "{\n \"compilerOptions\": {\n \"types\": [\"@testing-library/jest-dom\", \"@rstest/core/globals\"]\n },\n \"extends\": \"."
},
{
"path": "src/components/Debug.jsx",
"chars": 328,
"preview": "import { useState } from \"react\";\n\nfunction Debug() {\n const [itemId, setItemId] = useState(\"5eff09cd30a7dc22fd1ddfed"
},
{
"path": "src/components/FilterIcon.jsx",
"chars": 572,
"preview": "import { Component } from \"react\";\n\nclass FilterIcon extends Component {\n render() {\n return (\n <sv"
},
{
"path": "src/components/FleaMarketLoadingIcon.jsx",
"chars": 555,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { Icon } from \"@mdi/react\";\nimport { mdiTimerSand } from \"@mdi/js"
},
{
"path": "src/components/Graph.jsx",
"chars": 8334,
"preview": "import { useCallback, useMemo } from \"react\";\nimport {\n VictoryChart,\n VictoryScatter,\n VictoryTheme,\n Victo"
},
{
"path": "src/components/GraphLabel.jsx",
"chars": 815,
"preview": "import React from \"react\";\nimport { VictoryLabel, VictoryTooltip } from \"victory\";\n\nclass GraphLabel extends React.Compo"
},
{
"path": "src/components/SEO.jsx",
"chars": 1216,
"preview": "import React from \"react\";\nimport { Helmet } from \"react-helmet\";\n\nexport default function SEO({ title, description, url"
},
{
"path": "src/components/Symbol.jsx",
"chars": 1054,
"preview": "import { Component } from \"react\";\nimport { Navigate } from \"react-router-dom\";\n\nimport * as shapes from \"./points/index"
},
{
"path": "src/components/Time.jsx",
"chars": 3458,
"preview": "import dayjs from \"dayjs\";\nimport dayjsUtc from \"dayjs/plugin/utc\";\nimport { useTranslation } from \"react-i18next\";\n\nimp"
},
{
"path": "src/components/api-metrics-graph/index.css",
"chars": 70,
"preview": ".api-metrics-wrapper {\n height: 300px;\n margin-bottom: 100px;\n}\n"
},
{
"path": "src/components/api-metrics-graph/index.jsx",
"chars": 3546,
"preview": "import { useQuery } from \"@tanstack/react-query\";\nimport { VictoryChart, VictoryLine, VictoryTheme, VictoryVoronoiContai"
},
{
"path": "src/components/barter-tooltip/index.css",
"chars": 823,
"preview": ".barter-tooltip-wrapper {\n display: flex;\n flex-wrap: wrap;\n}\n\n.barter-tooltip-details-wrapper > div {\n line-he"
},
{
"path": "src/components/barter-tooltip/index.jsx",
"chars": 8599,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { useMemo } from \"react\";\nimport { useSelector } from \"react-redu"
},
{
"path": "src/components/barters-table/index.css",
"chars": 22,
"preview": "/* CSS Placeholder */\n"
},
{
"path": "src/components/barters-table/index.jsx",
"chars": 19829,
"preview": "import { useMemo, useState } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { useTranslation } from \"re"
},
{
"path": "src/components/boss-list/index.css",
"chars": 74,
"preview": ".boss-icon {\n height: 24px;\n width: 24px;\n margin-right: 10px;\n}\n"
},
{
"path": "src/components/boss-list/index.jsx",
"chars": 3907,
"preview": "import { Link } from \"react-router-dom\";\n\nimport MenuItem from \"../menu/MenuItem.jsx\";\nimport LoadingSmall from \"../load"
},
{
"path": "src/components/canvas-grid/index.jsx",
"chars": 2653,
"preview": "import { useRef, useEffect } from \"react\";\n\nfunction CanvasGrid(props) {\n const canvas = useRef(null);\n let boxes "
},
{
"path": "src/components/center-cell/index.jsx",
"chars": 283,
"preview": "const CenterCell = ({ children, className, nowrap = false, value }) => {\n return (\n <div className={`center-co"
},
{
"path": "src/components/cheeki-breeki-effect/index.css",
"chars": 109,
"preview": ".cheeki-breeki {\n display: flex;\n}\n\n.cheeki-breeki-button {\n padding: .2rem;\n border-radius: 4px;\n}\n"
},
{
"path": "src/components/cheeki-breeki-effect/index.jsx",
"chars": 1875,
"preview": "import { motion } from \"framer-motion\";\n\nimport \"./index.css\";\n\nfunction CheekiBreekiEffect() {\n return (\n <di"
},
{
"path": "src/components/contained-items-list/index.css",
"chars": 164,
"preview": ".contained-item-title-wrapper,\n.contained-item-link-wrapper {\n display: inline-block;\n margin-right: 5px;\n}\n\n.cont"
},
{
"path": "src/components/contained-items-list/index.jsx",
"chars": 4884,
"preview": "import { useMemo } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\""
},
{
"path": "src/components/contributors/index.jsx",
"chars": 2660,
"preview": "import { AvatarGroup, Avatar, createTheme, ThemeProvider } from \"@mui/material\";\n\nimport contributorJson from \"../../dat"
},
{
"path": "src/components/cost-items-cell/index.css",
"chars": 607,
"preview": ".cost-item-wrapper {\n align-items: center;\n display: flex;\n position: relative;\n width: 100%;\n}\n\n.cost-image"
},
{
"path": "src/components/cost-items-cell/index.jsx",
"chars": 4007,
"preview": "import { Link } from \"react-router-dom\";\nimport { useDispatch } from \"react-redux\";\n\nimport ItemCost from \"../item-cost/"
},
{
"path": "src/components/countdown/index.css",
"chars": 22,
"preview": "/* CSS Placeholder */\n"
},
{
"path": "src/components/countdown/index.jsx",
"chars": 589,
"preview": "import Countdown from \"react-countdown\";\n\nimport \"./index.css\";\n\nconst renderer = ({ days, hours, minutes, seconds, comp"
},
{
"path": "src/components/crafts-table/index.css",
"chars": 138,
"preview": ".duration-wrapper, .finish-wrapper {\n color: var(--color-gray-light);\n font-size: 14px;\n}\n\n.finish-wrapper {\n c"
},
{
"path": "src/components/crafts-table/index.jsx",
"chars": 22164,
"preview": "import { useMemo, useState, useCallback } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { useTranslati"
},
{
"path": "src/components/data-table/Arrow.tsx",
"chars": 785,
"preview": "import clsx from \"clsx\";\n\nfunction ArrowIcon({\n className,\n direction = \"down\",\n}: {\n className?: string;\n d"
},
{
"path": "src/components/data-table/TableHead.tsx",
"chars": 1593,
"preview": "import clsx from \"clsx\";\nimport { TableHeaderProps } from \"react-table\";\n\nimport \"./index.css\";\nimport ArrowIcon from \"#"
},
{
"path": "src/components/data-table/index.css",
"chars": 2841,
"preview": ".data-table,\n.data-table-filters-wrapper {\n border: 0;\n border-collapse: collapse;\n max-width: 1200px;\n marg"
},
{
"path": "src/components/data-table/index.jsx",
"chars": 7704,
"preview": "import { useEffect } from \"react\";\nimport { useTable, useSortBy, useExpanded, usePagination } from \"react-table/index.js"
},
{
"path": "src/components/em-item-tag/index.jsx",
"chars": 330,
"preview": "import { Link } from \"react-router-dom\";\n\nfunction EmItemTag({ children }) {\n // console.log(props);\n const itemNa"
},
{
"path": "src/components/filter/index.css",
"chars": 3169,
"preview": ".filter-wrapper {\n left: 0;\n line-height: 24px;\n margin: 0 auto;\n max-width: 1200px;\n overflow: hidden;\n "
},
{
"path": "src/components/filter/index.jsx",
"chars": 12771,
"preview": "import { useState, useRef, useEffect } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport Select from "
},
{
"path": "src/components/flea-price-cell/index.jsx",
"chars": 1545,
"preview": "import { Icon } from \"@mdi/react\";\nimport { mdiCloseOctagon, mdiHelpRhombus, mdiTimerSand } from \"@mdi/js\";\nimport { use"
},
{
"path": "src/components/footer/index.css",
"chars": 693,
"preview": ".footer-wrapper {\n background-color: var(--color-black);\n display: flex;\n flex-flow: wrap;\n gap: 30px;\n j"
},
{
"path": "src/components/footer/index.tsx",
"chars": 7267,
"preview": "import { Trans, useTranslation } from \"react-i18next\";\nimport { Link, useLocation } from \"react-router-dom\";\n\nimport { R"
},
{
"path": "src/components/item-cost/index.css",
"chars": 588,
"preview": ".hidden {\n display: none;\n}\n\n.item-cost-custom-price {\n background-color: var(--color-gunmetal-dark);\n border: "
},
{
"path": "src/components/item-cost/index.jsx",
"chars": 7074,
"preview": "import { useMemo, useState, useEffect } from \"react\";\nimport { Tooltip } from \"@mui/material\";\nimport { useDispatch } fr"
},
{
"path": "src/components/item-grid/Item.jsx",
"chars": 1374,
"preview": "import { Link } from \"react-router-dom\";\n\nimport ItemTooltip from \"./ItemTooltip.jsx\";\nimport ItemIcon from \"./ItemIcon."
},
{
"path": "src/components/item-grid/ItemIcon.jsx",
"chars": 555,
"preview": "function ItemIcon(props) {\n let sellTo = props.sellTo;\n let sellToNormalized = props.sellToNormalized;\n let cou"
},
{
"path": "src/components/item-grid/ItemTooltip.jsx",
"chars": 725,
"preview": "import { useTranslation } from \"react-i18next\";\n\nimport formatPrice from \"../../modules/format-price.js\";\n\nfunction Item"
},
{
"path": "src/components/item-grid/index.css",
"chars": 4012,
"preview": ".item-group-wrapper {\n border: 1px solid var(--color-black-light);\n background-color: var(--color-gunmetal-dark);\n"
},
{
"path": "src/components/item-grid/index.jsx",
"chars": 3133,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\n\nimport ItemImage from \"../item"
},
{
"path": "src/components/item-icon-list/index.jsx",
"chars": 1004,
"preview": "import {\n mdiAccountGroup,\n mdiAmmunition,\n mdiHammerWrench,\n mdiFinance,\n mdiCached,\n mdiProgressWren"
},
{
"path": "src/components/item-image/index.css",
"chars": 899,
"preview": ".item-image-mask {\n background: linear-gradient(to left, var(--color-gold-two) 20%, var(--color-gold-one) 40%, var(--"
},
{
"path": "src/components/item-image/index.jsx",
"chars": 21501,
"preview": "import React, { useState, useEffect, useRef, useMemo, useCallback } from \"react\";\nimport { renderToStaticMarkup } from \""
},
{
"path": "src/components/item-name-cell/index.css",
"chars": 696,
"preview": ".small-item-table-description-wrapper {\n align-items: center;\n display: flex;\n}\n\n.small-item-table-image-wrapper {"
},
{
"path": "src/components/item-name-cell/index.jsx",
"chars": 1683,
"preview": "import { Link } from \"react-router-dom\";\n\nimport ContainedItemsList from \"../contained-items-list/index.jsx\";\n\nimport \"."
},
{
"path": "src/components/item-search/index.css",
"chars": 1299,
"preview": ".item-search {\n position: relative;\n margin-bottom: 10px;\n}\n\n.item-search input[type='text'] {\n font-size: 20px"
},
{
"path": "src/components/item-search/index.jsx",
"chars": 12716,
"preview": "import { useMemo, useState, useCallback, useEffect, useRef } from \"react\";\nimport { Link, useNavigate, useLocation } fro"
},
{
"path": "src/components/items-for-hideout/index.css",
"chars": 1220,
"preview": ".hideout-item-list thead {\n background: rgb( from var(--color-black) r g b / 0.4);\n position: sticky;\n top: 0;\n"
},
{
"path": "src/components/items-for-hideout/index.jsx",
"chars": 5632,
"preview": "import { useMemo } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { useTranslation } from \"react-i18nex"
},
{
"path": "src/components/items-summary-table/index.css",
"chars": 119,
"preview": ".trader-unlock-wrapper {\n color: var(--color-gray);\n}\n\n.trader-station-level-unmet {\n color: var(--color-red);\n}\n"
},
{
"path": "src/components/items-summary-table/index.jsx",
"chars": 20480,
"preview": "import { useMemo } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { Link } from \"react-router-dom\";\nimp"
},
{
"path": "src/components/loading/index.css",
"chars": 141,
"preview": ".loader-wrapper {\n display: flex;\n align-items: center;\n justify-content: space-around;\n height: 100vh;\n "
},
{
"path": "src/components/loading/index.jsx",
"chars": 553,
"preview": "import { ScaleLoader } from \"react-spinners\";\n\nimport \"./index.css\";\n\nfunction Loading() {\n return (\n <div cla"
},
{
"path": "src/components/loading-small/index.css",
"chars": 501,
"preview": ".loading-wipe {\n background: linear-gradient(to left, var(--color-gold-one) 20%, var(--color-gold-two) 40%, var(--col"
},
{
"path": "src/components/loading-small/index.jsx",
"chars": 235,
"preview": "import { useTranslation } from \"react-i18next\";\n\nimport \"./index.css\";\n\nfunction LoadingSmall() {\n const { t } = useT"
},
{
"path": "src/components/loyalty-level-icon/index.css",
"chars": 388,
"preview": ".loyalty-level-parent {\n filter: drop-shadow(2px 3px 1px rgb( from var(--color-black) r g b / 0.5));\n left: 2px;\n "
},
{
"path": "src/components/loyalty-level-icon/index.jsx",
"chars": 417,
"preview": "import \"./index.css\";\n\nfunction loyaltyLevelIcon({ loyaltyLevel }) {\n let loyaltyLevelString = loyaltyLevel;\n\n if "
},
{
"path": "src/components/menu/CategoryMenu.jsx",
"chars": 3080,
"preview": "import { useState, useRef } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { motion, AnimatePresence } fr"
},
{
"path": "src/components/menu/MenuItem.jsx",
"chars": 2110,
"preview": "import { mdiEarthBox } from \"@mdi/js\";\nimport { Icon } from \"@mdi/react\";\nimport { useState, useEffect } from \"react\";\ni"
},
{
"path": "src/components/menu/alert-config.js",
"chars": 2006,
"preview": "const alertConfig = {\n // set this bool if the site alert should be enabled or not\n alertEnabled: false,\n\n // i"
},
{
"path": "src/components/menu/index.css",
"chars": 9410,
"preview": ":root {\n --nav-height: 64px;\n --glass-bg: rgb(18 18 18 / 0.85);\n --glass-border: rgb(210 175 120 / 0.15);\n -"
},
{
"path": "src/components/menu/index.jsx",
"chars": 14387,
"preview": "import { useMemo, useState, useRef } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport useSta"
},
{
"path": "src/components/menu/menu-data.js",
"chars": 3601,
"preview": "export const CATEGORIES = {\n MAPS: \"Maps\",\n DATABASE: \"Database\",\n CALCULATORS: \"Calculators\",\n PROGRESSION:"
},
{
"path": "src/components/menu/useMenuOverflow.js",
"chars": 2528,
"preview": "import { useState, useEffect } from \"react\";\n\n/**\n * A hook to determine how many menu items can fit in a container.\n * "
},
{
"path": "src/components/open-collective-button/index.css",
"chars": 474,
"preview": ".oc-button {\n border-radius: 5px !important;\n color: white !important;\n font-size: 16px !important;\n font-fa"
},
{
"path": "src/components/open-collective-button/index.jsx",
"chars": 605,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { Button } from \"@mui/material\";\nimport \"./index.css\";\n\nfunction "
},
{
"path": "src/components/patreon-button/index.css",
"chars": 604,
"preview": ".become-supporter-wrapper {\n text-align: center;\n}\n\n.become-supporter-wrapper.only-large {\n display: none;\n}\n\n.bec"
},
{
"path": "src/components/patreon-button/index.jsx",
"chars": 607,
"preview": "import { useTranslation } from \"react-i18next\";\n\nimport \"./index.css\";\n\nfunction PatreonButton({ onlyLarge, linkStyle, w"
},
{
"path": "src/components/points/Circle.jsx",
"chars": 498,
"preview": "import { Component } from \"react\";\n\nclass Circle extends Component {\n render() {\n return (\n <svg {."
},
{
"path": "src/components/points/Diamond.jsx",
"chars": 441,
"preview": "import { Component } from \"react\";\n\nclass TriangleUp extends Component {\n render() {\n return (\n <sv"
},
{
"path": "src/components/points/Plus.jsx",
"chars": 682,
"preview": "import { Component } from \"react\";\n\nclass Plus extends Component {\n render() {\n return (\n <svg {..."
},
{
"path": "src/components/points/Square.jsx",
"chars": 314,
"preview": "import { Component } from \"react\";\n\nclass Square extends Component {\n render() {\n return (\n <svg {."
},
{
"path": "src/components/points/TriangleDown.jsx",
"chars": 410,
"preview": "import { Component } from \"react\";\n\nclass TriangleUp extends Component {\n render() {\n return (\n <sv"
},
{
"path": "src/components/points/TriangleUp.jsx",
"chars": 412,
"preview": "import { Component } from \"react\";\n\nclass TriangleUp extends Component {\n render() {\n return (\n <sv"
},
{
"path": "src/components/points/index.jsx",
"chars": 318,
"preview": "export { default as Square } from \"./Square.jsx\";\nexport { default as Circle } from \"./Circle.jsx\";\nexport { default as "
},
{
"path": "src/components/preset-selector/index.jsx",
"chars": 2204,
"preview": "import React, { useMemo, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\n\nimport useItemsData f"
},
{
"path": "src/components/price-graph/index.css",
"chars": 46,
"preview": ".price-history-wrapper {\n height: 300px;\n}\n"
},
{
"path": "src/components/price-graph/index.jsx",
"chars": 10024,
"preview": "import { useMemo, useState, useEffect, useRef } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport {\n Vi"
},
{
"path": "src/components/property-list/index.css",
"chars": 932,
"preview": ".property-list {\n display: flex;\n flex-flow: wrap;\n gap: 5px;\n margin-top: 20px;\n margin-bottom: 20px;\n}\n"
},
{
"path": "src/components/property-list/index.jsx",
"chars": 3141,
"preview": "import { useMemo } from \"react\";\nimport propertyFormatter from \"../../modules/property-format.js\";\nimport { useTranslati"
},
{
"path": "src/components/quest-items-cell/index.css",
"chars": 400,
"preview": ".quest-item-wrapper {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.quest-image-wrapper {"
},
{
"path": "src/components/quest-items-cell/index.jsx",
"chars": 1661,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\n\nimport ItemImage from \"../item"
},
{
"path": "src/components/quest-table/index.css",
"chars": 987,
"preview": ".quest-name-wrapper {\n display: flex;\n align-items: center;\n}\n\n.quest-giver-image {\n margin-right: 10px;\n ma"
},
{
"path": "src/components/quest-table/index.jsx",
"chars": 23405,
"preview": "import { useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\""
},
{
"path": "src/components/remote-control-id/index.css",
"chars": 1997,
"preview": ".id-wrapper {\n background-color: rgb(from var(--color-black) r g b / 0.5);\n bottom: 0;\n color: #fff;\n displa"
},
{
"path": "src/components/remote-control-id/index.jsx",
"chars": 3348,
"preview": "import { useState, useCallback, useMemo } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { useTranslati"
},
{
"path": "src/components/reward-cell/index.css",
"chars": 770,
"preview": ".hidden {\n display: none;\n}\n\n.reward-wrapper {\n align-items: center;\n display: flex;\n}\n\n.reward-info-wrapper {\n"
},
{
"path": "src/components/reward-cell/index.jsx",
"chars": 5569,
"preview": "import { useMemo, useState, useEffect } from \"react\";\nimport { useDispatch } from \"react-redux\";\nimport { Link } from \"r"
},
{
"path": "src/components/reward-image/index.css",
"chars": 958,
"preview": ".reward-image-wrapper {\n position: relative;\n margin-right: 10px;\n font-size: 0;\n}\n\n.reward-image-extra-wrapper"
},
{
"path": "src/components/reward-image/index.jsx",
"chars": 1051,
"preview": "import \"./index.css\";\n\nfunction RewardImage({\n count,\n iconLink,\n height = \"64\",\n width = \"64\",\n isTool ="
},
{
"path": "src/components/scroll-to-top/index.jsx",
"chars": 258,
"preview": "import { useEffect } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\nexport default function ScrollToTop("
},
{
"path": "src/components/server-status/index.css",
"chars": 479,
"preview": ".server-status-wrapper {\n text-align: center;\n}\n\n.server-status-wrapper a {\n color: var(--color-gold-one);\n}\n\n.sta"
},
{
"path": "src/components/server-status/index.jsx",
"chars": 1825,
"preview": "import { useTranslation } from \"react-i18next\";\n// import ApiMetricsGraph from '../../components/api-metrics-graph/index"
},
{
"path": "src/components/small-item-table/index.css",
"chars": 373,
"preview": ".small-data-table td.data-cell {\n padding: 10px;\n}\n\n.small-data-table.no-borders td.data-cell:first-child {\n borde"
},
{
"path": "src/components/small-item-table/index.jsx",
"chars": 81871,
"preview": "import { useMemo, useCallback } from \"react\";\nimport { useSelector } from \"react-redux\";\nimport { Link } from \"react-rou"
},
{
"path": "src/components/station-skill-trader-setting/index.css",
"chars": 524,
"preview": ".station-skill-trader-setting-wrapper {\n text-align: center;\n display: flex;\n}\n\n.station-skill-trader-setting-wrap"
},
{
"path": "src/components/station-skill-trader-setting/index.jsx",
"chars": 3861,
"preview": "import React from \"react\";\nimport Select from \"react-select\";\nimport { Tooltip } from \"@mui/material\";\n\nimport \"./index."
},
{
"path": "src/components/supporter/index.css",
"chars": 327,
"preview": ".supporter-wrapper {\n margin-bottom: 5px;\n}\n\n.supporter-wrapper a,\n.supporter-wrapper span {\n display: flex;\n j"
},
{
"path": "src/components/supporter/index.jsx",
"chars": 1022,
"preview": "import { ReactComponent as PatreonIcon } from \"../../images/Patreon.svg\";\nimport { ReactComponent as GithubIcon } from \""
},
{
"path": "src/components/supporters-list/index.jsx",
"chars": 1363,
"preview": "import { useTranslation } from \"react-i18next\";\n\nimport Supporter from \"../supporter/index.jsx\";\n\nimport supporters from"
},
{
"path": "src/components/trader-image/index.css",
"chars": 365,
"preview": ".trader-image-reputation {\n background-color: rgb( from var(--color-black) r g b / 0.8);\n border-top-left-radius: "
},
{
"path": "src/components/trader-image/index.jsx",
"chars": 1975,
"preview": "import { useMemo } from \"react\";\nimport { Link } from \"react-router-dom\";\n\nimport \"./index.css\";\n\nexport default functio"
},
{
"path": "src/components/trader-price-cell/index.css",
"chars": 63,
"preview": ".trader-unlock-wrapper {\n color: var(--color-gray-light);\n}\n"
},
{
"path": "src/components/trader-price-cell/index.jsx",
"chars": 2870,
"preview": "import { Link } from \"react-router-dom\";\nimport { Tooltip } from \"@mui/material\";\nimport { Icon } from \"@mdi/react\";\nimp"
},
{
"path": "src/components/trader-reset-time/index.css",
"chars": 129,
"preview": ".countdown-wrapper.center {\n text-align: center;\n}\n\n.countdown-wrapper.center .countdown-text-wrapper {\n display: "
},
{
"path": "src/components/trader-reset-time/index.jsx",
"chars": 1286,
"preview": "import Countdown from \"react-countdown\";\nimport { Translation } from \"react-i18next\";\n\nimport { getRelativeTimeAndUnit }"
},
{
"path": "src/components/ukraine-button/index.css",
"chars": 506,
"preview": ".ua-button {\n border-radius: 5px !important;\n color: white !important;\n font-size: 16px !important;\n font-fa"
},
{
"path": "src/components/ukraine-button/index.jsx",
"chars": 601,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { Button } from \"@mui/material\";\nimport \"./index.css\";\n\nfunction "
},
{
"path": "src/components/value-cell/index.css",
"chars": 95,
"preview": ".craft-profit {\n color: var(--color-green);\n}\n\n.craft-loss {\n color: var(--color-red);\n}\n"
},
{
"path": "src/components/value-cell/index.jsx",
"chars": 2503,
"preview": "import { Tooltip } from \"@mui/material\";\nimport { useTranslation } from \"react-i18next\";\n\nimport formatPrice from \"../.."
},
{
"path": "src/data/api-users.json",
"chars": 2468,
"preview": "[\n {\n \"title\": \"Stash\",\n \"link\": \"https://github.com/the-hideout/stash\",\n \"text\": \"The Hideout's"
},
{
"path": "src/data/bosses.json",
"chars": 2452,
"preview": "[\n {\n \"normalizedName\": \"rogue\",\n \"spawnChanceOverride\": [\n {\n \"chance\": 1,\n "
},
{
"path": "src/data/category-pages.json",
"chars": 2014,
"preview": "[\n {\n \"key\": \"headsets\",\n \"displayText\": \"Headsets\",\n \"icon\": \"mdiHeadset\",\n \"type\": \"hea"
},
{
"path": "src/data/game-modes.json",
"chars": 28,
"preview": "[\n \"regular\",\n \"pve\"\n]"
},
{
"path": "src/data/item-grids.json",
"chars": 82558,
"preview": "{\n \"5c0e805e86f774683f3dd637\": [\n {\n \"row\": 0,\n \"col\": 0,\n \"width\": 5,\n "
},
{
"path": "src/data/maps.json",
"chars": 101889,
"preview": "[\n {\n \"normalizedName\": \"streets-of-tarkov\",\n \"primaryPath\": \"/map/streets-of-tarkov\",\n \"maps\": "
},
{
"path": "src/data/maps_static.json",
"chars": 921,
"preview": "{\n \"terminal\": {\n \"spawn_sniper_scav\": [\n {\n \"name\": \"Top left\",\n \"po"
},
{
"path": "src/data/patreons.json",
"chars": 2522,
"preview": "[\n {\n \"name\": \"Macy Creech\",\n \"tier\": \"api-users\"\n },\n {\n \"name\": \"The Hideout\",\n \""
},
{
"path": "src/data/wipe-details.json",
"chars": 1365,
"preview": "[\n {\n \"name\": \"0.4.0\",\n \"start\": \"2017-10-27T00:00:00.000Z\"\n },\n {\n \"name\": \"0.5.0\",\n \"start\": \"2017-12-2"
},
{
"path": "src/features/barters/do-fetch-barters.mjs",
"chars": 1438,
"preview": "import APIQuery from \"../../modules/api-query.mjs\";\n\nclass BartersQuery extends APIQuery {\n constructor() {\n s"
},
{
"path": "src/features/barters/index.js",
"chars": 7141,
"preview": "import { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice, createA"
},
{
"path": "src/features/crafts/do-fetch-crafts.mjs",
"chars": 1453,
"preview": "import APIQuery from \"../../modules/api-query.mjs\";\n\nclass CraftsQuery extends APIQuery {\n constructor() {\n su"
},
{
"path": "src/features/crafts/index.js",
"chars": 7669,
"preview": "import { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice, createA"
},
{
"path": "src/features/hideout/do-fetch-hideout.mjs",
"chars": 1647,
"preview": "import APIQuery from \"../../modules/api-query.mjs\";\n\nclass HideoutQuery extends APIQuery {\n constructor() {\n s"
},
{
"path": "src/features/hideout/index.js",
"chars": 3596,
"preview": "import { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice, createA"
},
{
"path": "src/features/items/do-fetch-items.mjs",
"chars": 13028,
"preview": "import jp from \"jsonpath\";\n\nimport APIQuery from \"../../modules/api-query.mjs\";\nimport { localStorageWriteJson } from \"."
},
{
"path": "src/features/items/index.js",
"chars": 6282,
"preview": "import { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice, createA"
},
{
"path": "src/features/maps/do-fetch-maps.mjs",
"chars": 5104,
"preview": "import APIQuery from \"../../modules/api-query.mjs\";\n\nclass MapsQuery extends APIQuery {\n constructor() {\n supe"
},
{
"path": "src/features/maps/index.js",
"chars": 11181,
"preview": "import { useEffect, useMemo } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice"
},
{
"path": "src/features/quests/do-fetch-quests.mjs",
"chars": 7989,
"preview": "import APIQuery from \"../../modules/api-query.mjs\";\n\nclass QuestsQuery extends APIQuery {\n constructor() {\n su"
},
{
"path": "src/features/quests/index.js",
"chars": 10277,
"preview": "import { useEffect } from \"react\";\nimport { useSelector, useDispatch } from \"react-redux\";\nimport { createSlice, createA"
}
]
// ... and 199 more files (download for full content)
About this extraction
This page contains the full source code of the the-hideout/tarkov-dev GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 399 files (2.3 MB), approximately 628.7k tokens, and a symbol index with 287 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.