mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-10 03:14:40 +00:00
Compare commits
3 Commits
tracking-f
...
map-events
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1f1098df6 | ||
|
|
ed5d824c0a | ||
|
|
d8e4631981 |
13
.check.exs
13
.check.exs
@@ -13,8 +13,8 @@
|
||||
|
||||
## list of tools (see `mix check` docs for a list of default curated tools)
|
||||
tools: [
|
||||
## Allow compilation warnings for now (error budget: unlimited warnings)
|
||||
{:compiler, "mix compile"},
|
||||
## curated tools may be disabled (e.g. the check for compilation warnings)
|
||||
{:compiler, false},
|
||||
|
||||
## ...or have command & args adjusted (e.g. enable skip comments for sobelow)
|
||||
# {:sobelow, "mix sobelow --exit --skip"},
|
||||
@@ -22,15 +22,10 @@
|
||||
## ...or reordered (e.g. to see output from dialyzer before others)
|
||||
# {:dialyzer, order: -1},
|
||||
|
||||
## Credo with relaxed error budget: max 200 issues
|
||||
{:credo, "mix credo --strict --max-issues 200"},
|
||||
## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
|
||||
|
||||
## Dialyzer but don't halt on exit (allow warnings)
|
||||
{:dialyzer, "mix dialyzer"},
|
||||
|
||||
## Tests without warnings-as-errors for now
|
||||
{:ex_unit, "mix test"},
|
||||
{:doctor, false},
|
||||
{:ex_unit, false},
|
||||
{:npm_test, false},
|
||||
{:sobelow, false}
|
||||
|
||||
|
||||
18
.credo.exs
18
.credo.exs
@@ -82,6 +82,8 @@
|
||||
# You can customize the priority of any check
|
||||
# Priority values are: `low, normal, high, higher`
|
||||
#
|
||||
{Credo.Check.Design.AliasUsage,
|
||||
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||
# You can also customize the exit_status of each check.
|
||||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
@@ -97,9 +99,10 @@
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, false},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
@@ -118,12 +121,14 @@
|
||||
#
|
||||
{Credo.Check.Refactor.Apply, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, []},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.MapJoin, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
@@ -191,19 +196,10 @@
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||
{Credo.Check.Warning.MixEnv, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||
|
||||
# {Credo.Check.Refactor.MapInto, []},
|
||||
|
||||
#
|
||||
# Temporarily disable checks that generate too many issues
|
||||
# to get under the 200 issue budget
|
||||
#
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Design.AliasUsage, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []}
|
||||
|
||||
#
|
||||
# Custom checks can be created using `mix credo.gen.check`.
|
||||
#
|
||||
|
||||
127
.credo.test.exs
127
.credo.test.exs
@@ -1,127 +0,0 @@
|
||||
# Credo configuration specific to test files
|
||||
# This enforces stricter quality standards for test code
|
||||
|
||||
%{
|
||||
configs: [
|
||||
%{
|
||||
name: "test",
|
||||
files: %{
|
||||
included: ["test/"],
|
||||
excluded: ["test/support/"]
|
||||
},
|
||||
requires: [],
|
||||
strict: true,
|
||||
color: true,
|
||||
checks: [
|
||||
# Consistency checks
|
||||
{Credo.Check.Consistency.ExceptionNames, []},
|
||||
{Credo.Check.Consistency.LineEndings, []},
|
||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||
|
||||
# Design checks - stricter for tests
|
||||
{Credo.Check.Design.AliasUsage, priority: :high},
|
||||
# Lower threshold for tests
|
||||
{Credo.Check.Design.DuplicatedCode, mass_threshold: 25},
|
||||
{Credo.Check.Design.TagTODO, []},
|
||||
{Credo.Check.Design.TagFIXME, []},
|
||||
|
||||
# Readability checks - very important for tests
|
||||
{Credo.Check.Readability.AliasOrder, []},
|
||||
{Credo.Check.Readability.FunctionNames, []},
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
# Slightly longer for test descriptions
|
||||
{Credo.Check.Readability.MaxLineLength, max_length: 120},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
# Not required for test modules
|
||||
{Credo.Check.Readability.ModuleDoc, false},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||
{Credo.Check.Readability.Semicolons, []},
|
||||
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||
{Credo.Check.Readability.StringSigils, []},
|
||||
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||
{Credo.Check.Readability.VariableNames, []},
|
||||
{Credo.Check.Readability.WithSingleClause, []},
|
||||
|
||||
# Test-specific readability checks
|
||||
# Discourage single pipes in tests
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
# Specs not needed in tests
|
||||
{Credo.Check.Readability.Specs, false},
|
||||
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||
|
||||
# Refactoring opportunities - important for test maintainability
|
||||
# Higher limit for complex test setups
|
||||
{Credo.Check.Refactor.ABCSize, max_size: 50},
|
||||
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, max_complexity: 10},
|
||||
# Lower for test helpers
|
||||
{Credo.Check.Refactor.FunctionArity, max_arity: 4},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MapInto, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
# Keep tests flat
|
||||
{Credo.Check.Refactor.Nesting, max_nesting: 3},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
{Credo.Check.Refactor.RejectReject, []},
|
||||
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||
|
||||
# Warnings - all should be fixed
|
||||
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||
{Credo.Check.Warning.IExPry, []},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||
{Credo.Check.Warning.UnusedListOperation, []},
|
||||
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||
{Credo.Check.Warning.UnsafeExec, []},
|
||||
|
||||
# Test-specific checks
|
||||
# Important for test isolation
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
|
||||
# Custom checks for test patterns
|
||||
{
|
||||
Credo.Check.Refactor.PipeChainStart,
|
||||
# Factory functions
|
||||
excluded_functions: ["build", "create", "insert"],
|
||||
excluded_argument_types: [:atom, :number]
|
||||
}
|
||||
],
|
||||
|
||||
# Disable these checks for test files
|
||||
disabled: [
|
||||
# Tests don't need module docs
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
# Tests don't need specs
|
||||
{Credo.Check.Readability.Specs, []},
|
||||
# Common in test setup
|
||||
{Credo.Check.Refactor.VariableRebinding, []}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +1,12 @@
|
||||
FROM elixir:1.17-otp-27
|
||||
|
||||
RUN apt install -yq curl gnupg
|
||||
# Install OS packages and Node.js (via nodesource),
|
||||
# plus inotify-tools and yarn
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
sudo \
|
||||
curl \
|
||||
make \
|
||||
git \
|
||||
bash \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
jq \
|
||||
vim \
|
||||
net-tools \
|
||||
procps \
|
||||
# Optionally add any other tools you need, e.g. vim, wget...
|
||||
&& curl -sL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs inotify-tools \
|
||||
&& npm install -g yarn \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM elixir:1.16-otp-25
|
||||
|
||||
RUN apt update -yq
|
||||
RUN apt install -yq curl gnupg mc inotify-tools
|
||||
RUN apt --fix-broken install
|
||||
RUN apt remove -y nodejs nodejs-doc
|
||||
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
|
||||
RUN apt install -y nodejs
|
||||
RUN npm install --global yarn
|
||||
|
||||
RUN mix local.hex --force
|
||||
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
{
|
||||
"name": "wanderer-dev",
|
||||
"dockerComposeFile": ["./docker-compose.yml"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"jakebecker.elixir-ls",
|
||||
"JakeBecker.elixir-ls",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"search.exclude": {
|
||||
"**/doc": true
|
||||
},
|
||||
"elixirLS.dialyzerEnabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": ["jakebecker.elixir-ls"],
|
||||
"service": "wanderer",
|
||||
"workspaceFolder": "/app",
|
||||
"shutdownAction": "stopCompose",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"networkArgs": ["--add-host=host.docker.internal:host-gateway"]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [4444]
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"search.exclude": {
|
||||
"**/doc": true
|
||||
},
|
||||
"elixirLS.dialyzerEnabled": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "0.1"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:13-alpine
|
||||
image: postgres:14.3
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
@@ -10,11 +10,11 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- db-new:/var/lib/postgresql/data
|
||||
- db:/var/lib/postgresql/data
|
||||
|
||||
wanderer:
|
||||
environment:
|
||||
PORT: 4444
|
||||
PORT: 8000
|
||||
DB_HOST: db
|
||||
WEB_APP_URL: "http://localhost:4444"
|
||||
ERL_AFLAGS: "-kernel shell_history enabled"
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 4444:4444
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- ..:/app:delegated
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
@@ -33,4 +33,4 @@ services:
|
||||
|
||||
volumes:
|
||||
elixir-artifacts: {}
|
||||
db-new: {}
|
||||
db: {}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "→ fetching & compiling deps"
|
||||
mix deps.get
|
||||
mix compile
|
||||
|
||||
# only run Ecto if the project actually has those tasks
|
||||
if mix help | grep -q "ecto.create"; then
|
||||
echo "→ waiting for database to be ready..."
|
||||
|
||||
# Wait for database to be ready
|
||||
DB_HOST=${DB_HOST:-db}
|
||||
timeout=60
|
||||
while ! nc -z $DB_HOST 5432 2>/dev/null; do
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "❌ Database connection timeout"
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for database... ($timeout seconds remaining)"
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
|
||||
# Give the database a bit more time to fully initialize
|
||||
echo "→ giving database 2 more seconds to fully initialize..."
|
||||
sleep 2
|
||||
|
||||
echo "→ database is ready, running ecto.create && ecto.migrate"
|
||||
mix ecto.create --quiet
|
||||
mix ecto.migrate
|
||||
fi
|
||||
|
||||
cd assets
|
||||
echo "→ installing JS & CSS dependencies"
|
||||
yarn install --frozen-lockfile
|
||||
echo "→ building assets"
|
||||
|
||||
echo "✅ setup complete"
|
||||
@@ -6,11 +6,3 @@ export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
|
||||
export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
|
||||
export GIT_SHA="1111"
|
||||
export WANDERER_INVITES="false"
|
||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||
export WANDERER_CHARACTER_API_DISABLED="false"
|
||||
export WANDERER_KILLS_SERVICE_ENABLED="true"
|
||||
export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
|
||||
export WANDERER_SSE_ENABLED="true"
|
||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||
109
.github/workflows/advanced-test.yml
vendored
109
.github/workflows/advanced-test.yml
vendored
@@ -1,109 +0,0 @@
|
||||
name: Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-test:
|
||||
name: 🚀 Deploy to test env (fly.io)
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.base_ref == 'develop' || (github.ref == 'refs/heads/develop' && github.event_name == 'push') }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
|
||||
- name: 👀 Read app name
|
||||
uses: SebRollen/toml-action@v1.0.0
|
||||
id: app_name
|
||||
with:
|
||||
file: "fly.toml"
|
||||
field: "app"
|
||||
|
||||
- name: 🚀 Deploy Test
|
||||
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ (github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
matrix:
|
||||
otp: ["27"]
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Elixir
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{matrix.otp}}
|
||||
elixir-version: ${{matrix.elixir}}
|
||||
# nix build would also work here because `todos` is the default package
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
${{ runner.os }}-build-
|
||||
# Step: Download project dependencies. If unchanged, uses
|
||||
# the cached version.
|
||||
- name: 🌐 Install dependencies
|
||||
run: mix deps.get --only "prod"
|
||||
|
||||
# Step: Compile the project treating any warnings as errors.
|
||||
# Customize this step if a different behavior is desired.
|
||||
- name: 🛠 Compiles without warnings
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
run: mix compile
|
||||
141
.github/workflows/build.yml
vendored
141
.github/workflows/build.yml
vendored
@@ -1,11 +1,12 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
- "releases/*"
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -19,10 +20,31 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-test:
|
||||
name: 🚀 Deploy to test env (fly.io)
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && github.event_name == 'push') }}
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
- uses: superfly/flyctl-actions/setup-flyctl@master
|
||||
|
||||
- name: 👀 Read app name
|
||||
uses: SebRollen/toml-action@v1.0.0
|
||||
id: app_name
|
||||
with:
|
||||
file: "fly.toml"
|
||||
field: "app"
|
||||
|
||||
- name: 🚀 Deploy Test
|
||||
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
|
||||
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -37,7 +59,7 @@ jobs:
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -53,27 +75,25 @@ jobs:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: "${{ secrets.COMMIT_KEY }}"
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
${{ runner.os }}-mix-${{ env.cache-name }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
_build
|
||||
**/_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
@@ -91,24 +111,15 @@ jobs:
|
||||
|
||||
- name: Generate Changelog & Update Tag Version
|
||||
id: generate-changelog
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
git config --global user.name 'CI'
|
||||
git config --global user.email 'ci@users.noreply.github.com'
|
||||
mix git_ops.release --force-patch --yes
|
||||
git commit --allow-empty -m 'chore: [skip ci]'
|
||||
git push --follow-tags
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set commit hash for develop
|
||||
id: set-commit-develop
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
@@ -124,7 +135,6 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -137,6 +147,17 @@ jobs:
|
||||
ref: ${{ needs.build.outputs.commit_hash }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -162,77 +183,45 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ env.REGISTRY_IMAGE }}:latest,${{ env.REGISTRY_IMAGE }}:${{ steps.get-latest-tag.outputs.tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.build.outputs.digest }}
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
stringToTruncate: |
|
||||
📣 Wanderer new release available 🎉
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
create-release:
|
||||
name: 🏷 Create Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: docker
|
||||
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
||||
needs: build
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
187
.github/workflows/docker-arm.yml
vendored
187
.github/workflows/docker-arm.yml
vendored
@@ -1,187 +0,0 @@
|
||||
name: Build Docker ARM Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition-arm
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer **ARM** release available 🎉
|
||||
|
||||
**Version**: :${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: 🏷 Notify about release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
187
.github/workflows/docker.yml
vendored
187
.github/workflows/docker.yml
vendored
@@ -1,187 +0,0 @@
|
||||
name: Build Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REGISTRY_IMAGE: wandererltd/community-edition
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
release-notes: ${{ steps.get-content.outputs.string }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
with:
|
||||
fallback: 1.0.0
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
MIX_ENV=prod
|
||||
BUILD_METADATA=${{ steps.meta.outputs.json }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- uses: markpatterson27/markdown-to-output@v1
|
||||
id: extract-changelog
|
||||
with:
|
||||
filepath: CHANGELOG.md
|
||||
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer new release available 🎉
|
||||
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
truncationSymbol: "…"
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.WANDERER_DOCKER_USER }}
|
||||
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: 🏷 Notify about release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [docker, merge]
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ needs.docker.outputs.release-notes }}
|
||||
300
.github/workflows/flaky-test-detection.yml
vendored
300
.github/workflows/flaky-test-detection.yml
vendored
@@ -1,300 +0,0 @@
|
||||
name: Flaky Test Detection
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run nightly at 2 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_file:
|
||||
description: 'Specific test file to check (optional)'
|
||||
required: false
|
||||
type: string
|
||||
iterations:
|
||||
description: 'Number of test iterations'
|
||||
required: false
|
||||
default: '10'
|
||||
type: string
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
ELIXIR_VERSION: "1.17"
|
||||
OTP_VERSION: "27"
|
||||
|
||||
jobs:
|
||||
detect-flaky-tests:
|
||||
name: 🔍 Detect Flaky Tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wanderer_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🏗️ Setup Elixir & Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||
otp-version: ${{ env.OTP_VERSION }}
|
||||
|
||||
- name: 📦 Restore dependencies cache
|
||||
uses: actions/cache@v4
|
||||
id: deps-cache
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
if: steps.deps-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
|
||||
- name: 🏗️ Compile project
|
||||
run: mix compile --warnings-as-errors
|
||||
|
||||
- name: 🏗️ Setup test database
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
|
||||
|
||||
- name: 🔍 Run flaky test detection
|
||||
id: flaky-detection
|
||||
run: |
|
||||
# Determine test target
|
||||
TEST_FILE="${{ github.event.inputs.test_file }}"
|
||||
ITERATIONS="${{ github.event.inputs.iterations || '10' }}"
|
||||
|
||||
if [ -n "$TEST_FILE" ]; then
|
||||
echo "Checking specific file: $TEST_FILE"
|
||||
mix test.stability --runs $ITERATIONS --file "$TEST_FILE" --detect --report flaky_report.json
|
||||
else
|
||||
echo "Checking all tests"
|
||||
mix test.stability --runs $ITERATIONS --detect --report flaky_report.json
|
||||
fi
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wanderer_test
|
||||
continue-on-error: true
|
||||
|
||||
- name: 📊 Upload flaky test report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: flaky-test-report
|
||||
path: flaky_report.json
|
||||
retention-days: 30
|
||||
|
||||
- name: 💬 Comment on flaky tests
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Read the report
|
||||
let report;
|
||||
try {
|
||||
const reportContent = fs.readFileSync('flaky_report.json', 'utf8');
|
||||
report = JSON.parse(reportContent);
|
||||
} catch (error) {
|
||||
console.log('No flaky test report found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!report.flaky_tests || report.flaky_tests.length === 0) {
|
||||
console.log('No flaky tests detected!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create issue body
|
||||
const issueBody = `## 🔍 Flaky Tests Detected
|
||||
|
||||
The automated flaky test detection found ${report.flaky_tests.length} potentially flaky test(s).
|
||||
|
||||
### Summary
|
||||
- **Total test runs**: ${report.summary.total_runs}
|
||||
- **Success rate**: ${(report.summary.success_rate * 100).toFixed(1)}%
|
||||
- **Average duration**: ${(report.summary.avg_duration_ms / 1000).toFixed(2)}s
|
||||
|
||||
### Flaky Tests
|
||||
|
||||
| Test | Failure Rate | Details |
|
||||
|------|--------------|---------|
|
||||
${report.flaky_tests.map(test =>
|
||||
`| ${test.test} | ${(test.failure_rate * 100).toFixed(1)}% | Failed ${test.failures}/${report.summary.total_runs} runs |`
|
||||
).join('\n')}
|
||||
|
||||
### Recommended Actions
|
||||
|
||||
1. Review the identified tests for race conditions
|
||||
2. Check for timing dependencies or async issues
|
||||
3. Ensure proper test isolation and cleanup
|
||||
4. Consider adding explicit waits or synchronization
|
||||
5. Use \`async: false\` if tests share resources
|
||||
|
||||
---
|
||||
*This issue was automatically created by the flaky test detection workflow.*
|
||||
*Run time: ${new Date().toISOString()}*
|
||||
`;
|
||||
|
||||
try {
|
||||
// Check if there's already an open issue
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'flaky-test',
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (issues.data.length > 0) {
|
||||
// Update existing issue
|
||||
const issue = issues.data[0];
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: issueBody
|
||||
});
|
||||
console.log(`Updated existing issue #${issue.number}`);
|
||||
} catch (commentError) {
|
||||
console.error('Failed to create comment:', commentError.message);
|
||||
throw commentError;
|
||||
}
|
||||
} else {
|
||||
// Create new issue
|
||||
try {
|
||||
const newIssue = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: '🔍 Flaky Tests Detected',
|
||||
body: issueBody,
|
||||
labels: ['flaky-test', 'test-quality', 'automated']
|
||||
});
|
||||
console.log(`Created new issue #${newIssue.data.number}`);
|
||||
} catch (createError) {
|
||||
console.error('Failed to create issue:', createError.message);
|
||||
throw createError;
|
||||
}
|
||||
}
|
||||
} catch (listError) {
|
||||
console.error('Failed to list issues:', listError.message);
|
||||
console.error('API error details:', listError.response?.data || 'No response data');
|
||||
throw listError;
|
||||
}
|
||||
|
||||
- name: 📈 Update metrics
|
||||
if: always()
|
||||
run: |
|
||||
# Parse and store metrics for tracking
|
||||
if [ -f flaky_report.json ]; then
|
||||
FLAKY_COUNT=$(jq '.flaky_tests | length' flaky_report.json)
|
||||
SUCCESS_RATE=$(jq '.summary.success_rate' flaky_report.json)
|
||||
|
||||
echo "FLAKY_TEST_COUNT=$FLAKY_COUNT" >> $GITHUB_ENV
|
||||
echo "TEST_SUCCESS_RATE=$SUCCESS_RATE" >> $GITHUB_ENV
|
||||
|
||||
# Log metrics (could be sent to monitoring service)
|
||||
echo "::notice title=Flaky Test Metrics::Found $FLAKY_COUNT flaky tests with ${SUCCESS_RATE}% success rate"
|
||||
fi
|
||||
|
||||
analyze-test-history:
|
||||
name: 📊 Analyze Test History
|
||||
runs-on: ubuntu-22.04
|
||||
needs: detect-flaky-tests
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📥 Download previous reports
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: flaky-test-detection.yml
|
||||
workflow_conclusion: completed
|
||||
name: flaky-test-report
|
||||
path: historical-reports
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: 📊 Generate trend analysis
|
||||
run: |
|
||||
# Analyze historical trends
|
||||
python3 <<'EOF'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import glob
|
||||
|
||||
reports = []
|
||||
for report_file in glob.glob('historical-reports/*/flaky_report.json'):
|
||||
try:
|
||||
with open(report_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
reports.append(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not reports:
|
||||
print("No historical data found")
|
||||
exit(0)
|
||||
|
||||
# Sort by timestamp
|
||||
reports.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
||||
|
||||
# Analyze trends
|
||||
print("## Test Stability Trend Analysis")
|
||||
print(f"\nAnalyzed {len(reports)} historical reports")
|
||||
print("\n### Flaky Test Counts Over Time")
|
||||
|
||||
for report in reports[:10]: # Last 10 reports
|
||||
timestamp = report.get('timestamp', 'Unknown')
|
||||
flaky_count = len(report.get('flaky_tests', []))
|
||||
success_rate = report.get('summary', {}).get('success_rate', 0) * 100
|
||||
print(f"- {timestamp[:10]}: {flaky_count} flaky tests ({success_rate:.1f}% success rate)")
|
||||
|
||||
# Identify persistently flaky tests
|
||||
all_flaky = {}
|
||||
for report in reports:
|
||||
for test in report.get('flaky_tests', []):
|
||||
test_name = test.get('test', '')
|
||||
if test_name not in all_flaky:
|
||||
all_flaky[test_name] = 0
|
||||
all_flaky[test_name] += 1
|
||||
|
||||
if all_flaky:
|
||||
print("\n### Persistently Flaky Tests")
|
||||
sorted_flaky = sorted(all_flaky.items(), key=lambda x: x[1], reverse=True)
|
||||
for test_name, count in sorted_flaky[:5]:
|
||||
percentage = (count / len(reports)) * 100
|
||||
print(f"- {test_name}: Flaky in {count}/{len(reports)} runs ({percentage:.1f}%)")
|
||||
EOF
|
||||
|
||||
- name: 💾 Save analysis
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-stability-analysis
|
||||
path: |
|
||||
flaky_report.json
|
||||
historical-reports/
|
||||
retention-days: 90
|
||||
2
.github/workflows/release_actions.yml
vendored
2
.github/workflows/release_actions.yml
vendored
@@ -18,4 +18,4 @@ jobs:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
port: ${{ secrets.PORT }}
|
||||
script: |
|
||||
/home/wanderer/app/deploy.sh ${{ github.event.release.tag_name }}
|
||||
/app/release/linux/deploy.sh ${{ github.event.release.tag_name }}
|
||||
|
||||
333
.github/workflows/test.yml
vendored
333
.github/workflows/test.yml
vendored
@@ -1,333 +0,0 @@
|
||||
name: 🧪 Test Suite
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
push:
|
||||
branches: [main, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MIX_ENV: test
|
||||
ELIXIR_VERSION: '1.16'
|
||||
OTP_VERSION: '26'
|
||||
NODE_VERSION: '18'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wanderer_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Elixir/OTP
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||
otp-version: ${{ env.OTP_VERSION }}
|
||||
|
||||
- name: Cache Elixir dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
|
||||
- name: Install Elixir dependencies
|
||||
run: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
|
||||
- name: Check code formatting
|
||||
id: format
|
||||
run: |
|
||||
if mix format --check-formatted; then
|
||||
echo "status=✅ Passed" >> $GITHUB_OUTPUT
|
||||
echo "count=0" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||
echo "count=1" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Compile code and capture warnings
|
||||
id: compile
|
||||
run: |
|
||||
# Capture compilation output
|
||||
output=$(mix compile 2>&1 || true)
|
||||
echo "$output" > compile_output.txt
|
||||
|
||||
# Count warnings
|
||||
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
|
||||
# Check if compilation succeeded
|
||||
if mix compile > /dev/null 2>&1; then
|
||||
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
|
||||
echo "output<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup database
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
|
||||
- name: Run tests with coverage
|
||||
id: tests
|
||||
run: |
|
||||
# Run tests with coverage
|
||||
output=$(mix test --cover 2>&1 || true)
|
||||
echo "$output" > test_output.txt
|
||||
|
||||
# Parse test results
|
||||
if echo "$output" | grep -q "0 failures"; then
|
||||
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
|
||||
test_status="success"
|
||||
else
|
||||
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
|
||||
test_status="failed"
|
||||
fi
|
||||
|
||||
# Extract test counts
|
||||
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures")
|
||||
total_tests=$(echo "$test_line" | grep -o '[0-9]\+ tests\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
|
||||
echo "total=$total_tests" >> $GITHUB_OUTPUT
|
||||
echo "failures=$failures" >> $GITHUB_OUTPUT
|
||||
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
|
||||
|
||||
# Calculate success rate
|
||||
if [ "$total_tests" -gt 0 ]; then
|
||||
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
|
||||
else
|
||||
success_rate="0"
|
||||
fi
|
||||
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
|
||||
|
||||
exit_code=$?
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate coverage report
|
||||
id: coverage
|
||||
run: |
|
||||
# Generate coverage report with GitHub format
|
||||
output=$(mix coveralls.github 2>&1 || true)
|
||||
echo "$output" > coverage_output.txt
|
||||
|
||||
# Extract coverage percentage
|
||||
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
|
||||
if [ -z "$coverage" ]; then
|
||||
coverage="0"
|
||||
fi
|
||||
|
||||
echo "percentage=$coverage" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if (( $(echo "$coverage >= 80" | bc -l) )); then
|
||||
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$coverage >= 60" | bc -l) )); then
|
||||
echo "status=⚠️ Good" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Credo analysis
|
||||
id: credo
|
||||
run: |
|
||||
# Run Credo and capture output
|
||||
output=$(mix credo --strict --format=json 2>&1 || true)
|
||||
echo "$output" > credo_output.txt
|
||||
|
||||
# Try to parse JSON output
|
||||
if echo "$output" | jq . > /dev/null 2>&1; then
|
||||
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
|
||||
high_issues=$(echo "$output" | jq '.issues | map(select(.priority == "high")) | length' 2>/dev/null || echo "0")
|
||||
normal_issues=$(echo "$output" | jq '.issues | map(select(.priority == "normal")) | length' 2>/dev/null || echo "0")
|
||||
low_issues=$(echo "$output" | jq '.issues | map(select(.priority == "low")) | length' 2>/dev/null || echo "0")
|
||||
else
|
||||
# Fallback: try to count issues from regular output
|
||||
regular_output=$(mix credo --strict 2>&1 || true)
|
||||
issues=$(echo "$regular_output" | grep -c "┃" || echo "0")
|
||||
high_issues="0"
|
||||
normal_issues="0"
|
||||
low_issues="0"
|
||||
fi
|
||||
|
||||
echo "total_issues=$issues" >> $GITHUB_OUTPUT
|
||||
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
|
||||
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
|
||||
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "$issues" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
elif [ "$issues" -lt 10 ]; then
|
||||
echo "status=⚠️ Minor Issues" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Dialyzer analysis
|
||||
id: dialyzer
|
||||
run: |
|
||||
# Ensure PLT is built
|
||||
mix dialyzer --plt
|
||||
|
||||
# Run Dialyzer and capture output
|
||||
output=$(mix dialyzer --format=github 2>&1 || true)
|
||||
echo "$output" > dialyzer_output.txt
|
||||
|
||||
# Count warnings and errors
|
||||
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
errors=$(echo "$output" | grep -c "error:" || echo "0")
|
||||
|
||||
echo "warnings=$warnings" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine status
|
||||
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
elif [ "$errors" -eq 0 ]; then
|
||||
echo "status=⚠️ Warnings Only" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create test results summary
|
||||
id: summary
|
||||
run: |
|
||||
# Calculate overall score
|
||||
format_score=${{ steps.format.outputs.count == '0' && '100' || '0' }}
|
||||
compile_score=${{ steps.compile.outputs.warnings == '0' && '100' || '80' }}
|
||||
test_score=${{ steps.tests.outputs.success_rate }}
|
||||
coverage_score=${{ steps.coverage.outputs.percentage }}
|
||||
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
|
||||
dialyzer_score=$(echo "scale=0; (100 - ${{ steps.dialyzer.outputs.warnings }} * 2 - ${{ steps.dialyzer.outputs.errors }} * 10)" | bc | sed 's/^-.*$/0/')
|
||||
|
||||
overall_score=$(echo "scale=1; ($format_score + $compile_score + $test_score + $coverage_score + $credo_score + $dialyzer_score) / 6" | bc)
|
||||
|
||||
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
|
||||
|
||||
# Determine overall status
|
||||
if (( $(echo "$overall_score >= 90" | bc -l) )); then
|
||||
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$overall_score >= 80" | bc -l) )); then
|
||||
echo "overall_status=✅ Good" >> $GITHUB_OUTPUT
|
||||
elif (( $(echo "$overall_score >= 70" | bc -l) )); then
|
||||
echo "overall_status=⚠️ Needs Improvement" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Find existing PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
id: find_comment
|
||||
uses: peter-evans/find-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '## 🧪 Test Results Summary'
|
||||
|
||||
- name: Create or update PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.find_comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## 🧪 Test Results Summary
|
||||
|
||||
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
|
||||
|
||||
### 📊 Metrics Dashboard
|
||||
|
||||
| Category | Status | Count | Details |
|
||||
|----------|---------|-------|---------|
|
||||
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
|
||||
| 🔨 **Compilation** | ${{ steps.compile.outputs.status }} | ${{ steps.compile.outputs.warnings }} warnings | `mix compile` |
|
||||
| 🧪 **Tests** | ${{ steps.tests.outputs.status }} | ${{ steps.tests.outputs.failures }}/${{ steps.tests.outputs.total }} failed | Success rate: ${{ steps.tests.outputs.success_rate }}% |
|
||||
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` |
|
||||
| 🎯 **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} |
|
||||
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
|
||||
|
||||
### 🎯 Quality Gates
|
||||
|
||||
Based on the project's quality thresholds:
|
||||
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
|
||||
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
|
||||
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
|
||||
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
|
||||
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
|
||||
|
||||
<details>
|
||||
<summary>📈 Progress Toward Goals</summary>
|
||||
|
||||
Target goals for the project:
|
||||
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
|
||||
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
|
||||
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
|
||||
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
|
||||
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>🔧 Quick Actions</summary>
|
||||
|
||||
To improve code quality:
|
||||
```bash
|
||||
# Fix formatting issues
|
||||
mix format
|
||||
|
||||
# View detailed Credo analysis
|
||||
mix credo --strict
|
||||
|
||||
# Check Dialyzer warnings
|
||||
mix dialyzer
|
||||
|
||||
# Generate detailed coverage report
|
||||
mix coveralls.html
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,8 +4,7 @@
|
||||
*.iml
|
||||
|
||||
*.key
|
||||
.repomixignore
|
||||
repomix*
|
||||
|
||||
/.idea/
|
||||
/node_modules/
|
||||
/assets/node_modules/
|
||||
@@ -18,9 +17,6 @@ repomix*
|
||||
/priv/static/*.js
|
||||
/priv/static/*.css
|
||||
|
||||
# Dialyzer PLT files
|
||||
/priv/plts/
|
||||
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
@@ -30,7 +26,6 @@ repomix*
|
||||
|
||||
.env
|
||||
*.local.env
|
||||
test/manual/.auto*
|
||||
|
||||
.direnv/
|
||||
.cache/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
erlang 26.2.5.5
|
||||
elixir 1.17.3-otp-26
|
||||
erlang 25.3
|
||||
elixir 1.16-otp-25
|
||||
nodejs 18.0.0
|
||||
|
||||
2595
CHANGELOG.md
2595
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
11
Dockerfile
11
Dockerfile
@@ -9,9 +9,6 @@ WORKDIR /app
|
||||
# set build ENV
|
||||
ENV MIX_ENV="prod"
|
||||
|
||||
# Set ERL_FLAGS for ARM compatibility
|
||||
ENV ERL_FLAGS="+JPperf true"
|
||||
|
||||
# install mix dependencies
|
||||
COPY mix.exs mix.lock ./
|
||||
RUN rm -Rf _build deps && mix deps.get --only $MIX_ENV
|
||||
@@ -21,17 +18,21 @@ RUN mkdir config
|
||||
# to ensure any relevant config change will trigger the dependencies
|
||||
# to be re-compiled.
|
||||
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||
|
||||
COPY priv priv
|
||||
|
||||
COPY lib lib
|
||||
|
||||
COPY assets assets
|
||||
|
||||
RUN mix assets.deploy
|
||||
RUN mix compile
|
||||
|
||||
RUN mix assets.deploy
|
||||
|
||||
# Changes to config/runtime.exs don't require recompiling the code
|
||||
COPY config/runtime.exs config/
|
||||
COPY rel rel
|
||||
|
||||
COPY rel rel
|
||||
RUN mix release
|
||||
|
||||
# start a new build stage so that the final image will only contain
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
|
||||
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
SHELL := /bin/bash
|
||||
@@ -35,11 +35,6 @@ test t:
|
||||
coverage cover co:
|
||||
mix test --cover
|
||||
|
||||
unit-tests ut:
|
||||
@echo "Running unit tests..."
|
||||
@find test/unit -name "*.exs" -exec elixir {} \;
|
||||
@echo "All unit tests completed."
|
||||
|
||||
versions v:
|
||||
@echo "Tool Versions"
|
||||
@cat .tool-versions
|
||||
|
||||
19
README.md
19
README.md
@@ -20,11 +20,11 @@ Interested to learn more? [Check more on our website](https://wanderer.ltd/news)
|
||||
|
||||
Wanderer is open source project and we have a free as in beer and self-hosted solution called [Wanderer Community Edition (CE)](https://wanderer.ltd/news/community-edition). Here are the differences between Wanderer and Wanderer CE:
|
||||
|
||||
| | Wanderer Cloud | Wanderer Community Edition |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Infrastructure management** | Easy and convenient. It takes 2 minutes to register your character and create a map. We manage everything so you don’t have to worry about anything and can focus on gameplay. | You do it all yourself. You need to get a server and you need to manage your infrastructure. You are responsible for installation, maintenance, upgrades, server capacity, uptime, backup, security, stability, consistency, loading time and so on. |
|
||||
| **Release schedule** | Continuously developed and improved with new features and updates multiple times per week. | Latest features and improvements won't be immediately available. |
|
||||
| **Server location** | All visitor data is exclusively processed on EU-owned cloud infrastructure. We keep your site data on a secure, encrypted and green energy powered server in Germany. This ensures that your site data is protected by the strict European Union data privacy laws and ensures compliance with GDPR. Your website data never leaves the EU. | You have full control and can host your instance on any server in any country that you wish. Host it on a server in your basement or host it with any cloud provider wherever you want, even those that are not GDPR compliant. |
|
||||
| | Wanderer Cloud | Wanderer Community Edition |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| **Infrastructure management** | Easy and convenient. It takes 2 minutes to register your character and create a map. We manage everything so you don’t have to worry about anything and can focus on gameplay. | You do it all yourself. You need to get a server and you need to manage your infrastructure. You are responsible for installation, maintenance, upgrades, server capacity, uptime, backup, security, stability, consistency, loading time and so on.|
|
||||
| **Release schedule** | Continuously developed and improved with new features and updates multiple times per week. | Latest features and improvements won't be immediately available.|
|
||||
| **Server location** | All visitor data is exclusively processed on EU-owned cloud infrastructure. We keep your site data on a secure, encrypted and green energy powered server in Germany. This ensures that your site data is protected by the strict European Union data privacy laws and ensures compliance with GDPR. Your website data never leaves the EU. | You have full control and can host your instance on any server in any country that you wish. Host it on a server in your basement or host it with any cloud provider wherever you want, even those that are not GDPR compliant.|
|
||||
|
||||
Interested in self-hosting Wanderer CE on your server? Take a look at our [Wanderer CE installation instructions](https://github.com/wanderer-industries/community-edition/).
|
||||
|
||||
@@ -54,14 +54,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
|
||||
#### Using .devcontainer
|
||||
|
||||
- Run devcontainer
|
||||
- Install additional dependencies inside Dev container
|
||||
- `root@0d0a785313b6:/app# apt update`
|
||||
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
|
||||
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
|
||||
- `root@0d0a785313b6:/app# npm install -g yarn`
|
||||
- `root@0d0a785313b6:/app# mix setup`
|
||||
|
||||
- See how to run server in #Run section
|
||||
- See how to start server in #setup section
|
||||
|
||||
#### Using nix flakes
|
||||
|
||||
|
||||
@@ -17,6 +17,5 @@ module.exports = {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
"linebreak-style": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "auto"
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// import '@fontsource-variable/inter'
|
||||
// import '@fontsource-variable/jetbrains-mono'
|
||||
// import './lib/tailwind/index.css';
|
||||
import './css/app.css';
|
||||
|
||||
import './lib/phoenix';
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
@import 'primereact/resources/themes/arya-blue/theme.css' layer(primereact);
|
||||
/*@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css' layer(primereact);*/
|
||||
|
||||
@import '../js/hooks/Mapper/components/map/styles/index.scss';
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
@@ -25,10 +23,6 @@ body {
|
||||
width: 400px; /* As IE6 ignores !important it will set width as 400px; */
|
||||
}
|
||||
|
||||
body > div:first-of-type {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
||||
.lending-normal {
|
||||
font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||
font-weight: 500;
|
||||
@@ -112,19 +106,19 @@ body > div:first-of-type {
|
||||
}
|
||||
|
||||
.wd-characters-icons {
|
||||
/*display: flex;*/
|
||||
/*transition:*/
|
||||
/* border-color 250ms,*/
|
||||
/* opacity 250ms;*/
|
||||
/*width: 35px;*/
|
||||
/*height: 35px;*/
|
||||
/*border-radius: 50%;*/
|
||||
/*border-width: 2px;*/
|
||||
/*border-style: solid;*/
|
||||
/*border-color: #5a5a5a;*/
|
||||
/*background-color: rgba(0, 0, 0, 0);*/
|
||||
/*cursor: pointer;*/
|
||||
/*opacity: 0.6;*/
|
||||
display: flex;
|
||||
transition:
|
||||
border-color 250ms,
|
||||
opacity 250ms;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: #5a5a5a;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.wd-bg-default {
|
||||
@@ -472,530 +466,3 @@ body > div:first-of-type {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Map refresh */
|
||||
.socket {
|
||||
scale: 0.5;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
left: 50%;
|
||||
/* margin-left: -75px; */
|
||||
top: 50%;
|
||||
/* margin-top: -50px; */
|
||||
}
|
||||
|
||||
.hex-brick {
|
||||
background: #000;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
animation-name: fade;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-animation-name: fade;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.hex-brick--active {
|
||||
animation-name: fade-active;
|
||||
-webkit-animation-name: fade-active;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
transform: rotate(60deg);
|
||||
-webkit-transform: rotate(60deg);
|
||||
}
|
||||
|
||||
.h3 {
|
||||
transform: rotate(-60deg);
|
||||
-webkit-transform: rotate(-60deg);
|
||||
}
|
||||
|
||||
.gel {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
transition: all 0.3s;
|
||||
-webkit-transition: all 0.3s;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.center-gel {
|
||||
margin-left: -15px;
|
||||
margin-top: -15px;
|
||||
|
||||
animation-name: pulse-version;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-animation-name: pulse-version;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-left: -47px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
margin-left: -31px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
margin-left: 1px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin-left: 17px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
.c5 {
|
||||
margin-left: -31px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
margin-left: 1px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: -63px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
margin-left: 33px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
margin-left: -15px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
margin-left: -63px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
margin-left: 33px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
margin-left: -15px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin-left: -47px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
margin-left: 17px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
margin-left: -47px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
margin-left: 17px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
margin-left: -79px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
margin-left: 49px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
margin-left: -63px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
margin-left: 33px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
margin-left: 1px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
margin-left: -31px;
|
||||
margin-top: -99px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
margin-left: -63px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
margin-left: 33px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
margin-left: 1px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c26 {
|
||||
margin-left: -31px;
|
||||
margin-top: 69px;
|
||||
}
|
||||
|
||||
.c27 {
|
||||
margin-left: -79px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c28 {
|
||||
margin-left: -95px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c29 {
|
||||
margin-left: -95px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c30 {
|
||||
margin-left: 49px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c31 {
|
||||
margin-left: -79px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c32 {
|
||||
margin-left: -111px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.c33 {
|
||||
margin-left: 65px;
|
||||
margin-top: -43px;
|
||||
}
|
||||
|
||||
.c34 {
|
||||
margin-left: 65px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.c35 {
|
||||
margin-left: -79px;
|
||||
margin-top: 41px;
|
||||
}
|
||||
|
||||
.c36 {
|
||||
margin-left: 49px;
|
||||
margin-top: -71px;
|
||||
}
|
||||
|
||||
.c37 {
|
||||
margin-left: 81px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.r1 {
|
||||
animation-name: pulse-version;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.2s;
|
||||
-webkit-animation-name: pulse-version;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.r2 {
|
||||
animation-name: pulse-version;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.4s;
|
||||
-webkit-animation-name: pulse-version;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.r3 {
|
||||
animation-name: pulse-version;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.6s;
|
||||
-webkit-animation-name: pulse-version;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.r1 > .hex-brick {
|
||||
animation-name: fade;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.2s;
|
||||
-webkit-animation-name: fade;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.r1 > .hex-brick--active {
|
||||
animation-name: fade-active;
|
||||
-webkit-animation-name: fade-active;
|
||||
}
|
||||
|
||||
.r2 > .hex-brick {
|
||||
animation-name: fade;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.4s;
|
||||
-webkit-animation-name: fade;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.r2 > .hex-brick--active {
|
||||
animation-name: fade-active;
|
||||
-webkit-animation-name: fade-active;
|
||||
}
|
||||
|
||||
.r3 > .hex-brick {
|
||||
animation-name: fade;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: 0.6s;
|
||||
-webkit-animation-name: fade;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-webkit-animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
.r3 > .hex-brick--active {
|
||||
animation-name: fade-active;
|
||||
-webkit-animation-name: fade-active;
|
||||
}
|
||||
|
||||
@keyframes pulse-version {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(0.01);
|
||||
transform: scale(0.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
background: #09d0e2;
|
||||
}
|
||||
|
||||
50% {
|
||||
background: #8ae6ee;
|
||||
}
|
||||
|
||||
100% {
|
||||
background: #09d0e2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-active {
|
||||
0% {
|
||||
background: #ff52d9;
|
||||
}
|
||||
|
||||
50% {
|
||||
background: #ff52d9;
|
||||
}
|
||||
|
||||
100% {
|
||||
background: #ff52d9;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(0.01);
|
||||
transform: scale(0.01);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fade {
|
||||
0% {
|
||||
background: #abf8ff;
|
||||
}
|
||||
|
||||
50% {
|
||||
background: #389ca6;
|
||||
}
|
||||
|
||||
100% {
|
||||
background: #abf8ff;
|
||||
}
|
||||
}
|
||||
/* Map refresh END */
|
||||
|
||||
.inputContainer {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
}
|
||||
.inputContainer > span:nth-child(1),
|
||||
.inputContainer > label:nth-child(1) {
|
||||
color: var(--gray-200);
|
||||
font-size: 13px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.inputContainer > :nth-child(2) {
|
||||
border-bottom: 2px dotted #3f3f3f;
|
||||
height: 1px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.smallInputSwitch {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.smallInputSwitch .p-inputswitch {
|
||||
height: 1rem;
|
||||
width: 2rem;
|
||||
}
|
||||
.smallInputSwitch .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider::before {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
.smallInputSwitch .p-inputswitch.p-highlight .p-inputswitch-slider:before {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
.smallInputSwitch .p-inputswitch .p-inputswitch-slider::before {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
margin-top: -0.4rem;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.checkboxRoot.sizeXS {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.checkboxRoot.sizeXS .p-checkbox-box,
|
||||
.checkboxRoot.sizeXS .p-checkbox-input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.checkboxRoot.sizeM {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.checkboxRoot.sizeM .p-checkbox-box,
|
||||
.checkboxRoot.sizeM .p-checkbox-input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.verticalTabsContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-panels {
|
||||
padding: 6px 1rem !important;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav-container {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav {
|
||||
flex-direction: column;
|
||||
width: 150px;
|
||||
min-height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li {
|
||||
width: 100%;
|
||||
border-right: 4px solid var(--surface-hover);
|
||||
background-color: var(--surface-card);
|
||||
transition:
|
||||
background-color 200ms,
|
||||
border-right-color 200ms;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--surface-100);
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li .p-tabview-nav-link {
|
||||
transition: color 200ms;
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
background-color: initial;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
border-radius: initial;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected {
|
||||
background-color: var(--surface-50);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected .p-tabview-nav-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-nav li.p-tabview-selected:hover {
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
.verticalTabsContainer .p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['<rootDir>'],
|
||||
moduleDirectories: ['node_modules', 'js'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/js/$1',
|
||||
'\.scss$': 'identity-obj-proxy', // Mock SCSS files
|
||||
},
|
||||
transform: {
|
||||
'^.+\.(ts|tsx)$': 'ts-jest',
|
||||
'^.+\.(js|jsx)$': 'babel-jest', // Add babel-jest for JS/JSX files if needed
|
||||
},
|
||||
};
|
||||
@@ -1,26 +1,27 @@
|
||||
import { PrimeReactProvider } from 'primereact/api';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { PrimeReactProvider } from 'primereact/api';
|
||||
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
import { useMapperHandlers } from './useMapperHandlers';
|
||||
|
||||
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
|
||||
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
|
||||
import './common-styles/main.scss';
|
||||
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
|
||||
|
||||
const ErrorFallback = () => {
|
||||
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
|
||||
};
|
||||
|
||||
export default function MapRoot({ hooks }) {
|
||||
const mapRef = useRef<MapHandlers>(null);
|
||||
const providerRef = useRef<MapHandlers>(null);
|
||||
const hooksRef = useRef<any>(hooks);
|
||||
|
||||
const mapperHandlerRefs = useRef([providerRef]);
|
||||
const mapperHandlerRefs = useRef([mapRef, providerRef]);
|
||||
|
||||
const { handleCommand, handleMapEvent } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
|
||||
const { handleCommand, handleMapEvent, handleMapEvents } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
|
||||
|
||||
const logError = useCallback((error: Error, info: ErrorInfo) => {
|
||||
if (!hooksRef.current) {
|
||||
@@ -35,11 +36,12 @@ export default function MapRoot({ hooks }) {
|
||||
}
|
||||
|
||||
hooksRef.current.handleEvent('map_event', handleMapEvent);
|
||||
hooksRef.current.handleEvent('map_events', handleMapEvents);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand} mapRef={mapRef}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
|
||||
<ReactFlowProvider>
|
||||
<MapRootContent />
|
||||
|
||||
@@ -67,22 +67,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable-thead {
|
||||
th, th.p-sortable-column {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
.p-sortable-column {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.p-selectable-row td {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.p-sortable-column > .p-column-header-content > span:last-child {
|
||||
transform: scale(0.7);
|
||||
|
||||
@@ -99,11 +93,6 @@
|
||||
.p-dropdown-item {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
|
||||
.p-dropdown-item-label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.p-dropdown-item-group {
|
||||
@@ -119,168 +108,3 @@
|
||||
.p-dropdown-empty-message {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Fixed sizes of Input switch */
|
||||
.p-inputswitch {
|
||||
width: 2.0rem;
|
||||
height: 1.15rem;
|
||||
|
||||
.p-inputswitch-slider:before {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
left: 0.14rem;
|
||||
margin-top: -0.385rem;
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider:before {
|
||||
transform: translateX(0.8rem);
|
||||
}
|
||||
|
||||
&:not(.p-disabled):has(.p-inputswitch-input:hover) .p-inputswitch-slider {
|
||||
background: rgb(255 255 255 / 21%);
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider {
|
||||
background: #966d3d;
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable-wrapper {
|
||||
height: 100%;
|
||||
& {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.5) transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr.p-highlight {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.suppress-menu-behaviour {
|
||||
pointer-events: none;
|
||||
|
||||
.p-menuitem-content {
|
||||
pointer-events: initial;
|
||||
background-color: initial !important;
|
||||
}
|
||||
.p-menuitem-content:hover {
|
||||
background-color: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.p-autocomplete .p-autocomplete-multiple-container:not(.p-disabled).p-focus {
|
||||
box-shadow: 0 0 0 1px #335c7e;
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
.p-inputtext:enabled:focus {
|
||||
box-shadow: 0 0 0 1px #335c7e;
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
.p-inputtext:enabled:hover {
|
||||
border-color: #335c7e;
|
||||
}
|
||||
|
||||
|
||||
// --------------- TOAST
|
||||
.p-toast .p-toast-message {
|
||||
background-color: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
border-left: 4px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.p-toast .p-toast-message .p-toast-summary {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.p-toast .p-toast-message .p-toast-detail {
|
||||
color: #c0c0c0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.p-toast .p-toast-icon-close {
|
||||
color: #ffaa00;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.p-toast .p-toast-icon-close:hover {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.p-toast-message-success {
|
||||
border-left-color: #f1c40f;
|
||||
}
|
||||
.p-toast-message-error {
|
||||
border-left-color: #e74c3c;
|
||||
}
|
||||
.p-toast-message-info {
|
||||
border-left-color: #3498db;
|
||||
}
|
||||
.p-toast-message-warn {
|
||||
border-left-color: #e67e22;
|
||||
}
|
||||
|
||||
.p-toast-message-success .p-toast-message-icon {
|
||||
color: #f1c40f;
|
||||
}
|
||||
.p-toast-message-error .p-toast-message-icon {
|
||||
color: #e74c3c;
|
||||
}
|
||||
.p-toast-message-info .p-toast-message-icon {
|
||||
color: #3498db;
|
||||
}
|
||||
.p-toast-message-warn .p-toast-message-icon {
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
.p-toast-message-success .p-toast-message-content {
|
||||
border-left-color: #f1c40f;
|
||||
}
|
||||
|
||||
.p-toast-message-error .p-toast-message-content {
|
||||
border-left-color: #e74c3c;
|
||||
}
|
||||
|
||||
.p-toast-message-info .p-toast-message-content {
|
||||
border-left-color: #3498db;
|
||||
}
|
||||
|
||||
.p-toast-message-warn .p-toast-message-content {
|
||||
border-left-color: #e67e22;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// import './tailwind.css';
|
||||
@use 'primereact/resources/primereact.min.css';
|
||||
@use 'primeicons/primeicons.css';
|
||||
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
|
||||
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
|
||||
//@import "prime-fixes";
|
||||
@import 'primereact/resources/primereact.min.css';
|
||||
//@import 'primeflex/primeflex.css';
|
||||
@import 'primeicons/primeicons.css';
|
||||
//@import 'primereact/resources/primereact.css';
|
||||
|
||||
|
||||
@use "fixes";
|
||||
@use "prime-fixes";
|
||||
@use "custom-scrollbar";
|
||||
@use "tooltip";
|
||||
@use "context-menu";
|
||||
@import "fixes";
|
||||
@import "prime-fixes";
|
||||
@import "custom-scrollbar";
|
||||
@import "tooltip";
|
||||
@import "context-menu";
|
||||
|
||||
|
||||
.fixedImportant {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* Основной класс диалога */
|
||||
body .p-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
//visibility: hidden;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 10px 0 rgba(0,0,0,0.2);
|
||||
@@ -26,10 +29,12 @@ body .p-dialog {
|
||||
}
|
||||
}
|
||||
|
||||
/* Стиль видимого диалога */
|
||||
.p-dialog-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
.p-dialog-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -48,27 +53,31 @@ body .p-dialog {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
/* Заголовок диалога */
|
||||
.p-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: #f4f4f4;
|
||||
height: 40px;
|
||||
//border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* Содержимое диалога */
|
||||
.p-dialog-content {
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Подвал диалога */
|
||||
.p-dialog-footer {
|
||||
padding: .75rem 1rem;
|
||||
border-top: none !important;
|
||||
//background: #f4f4f4;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #ddd;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
/* Кнопка закрытия диалога */
|
||||
.p-dialog-header-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -84,12 +93,3 @@ body .p-dialog {
|
||||
.p-dialog-header-close .pi {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.p-dialog {
|
||||
.p-dialog-title {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.p-dialog-header-icons {
|
||||
align-self: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
.p-confirm-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@apply p-[12px];
|
||||
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-confirm-popup-content, .p-confirm-popup-footer {
|
||||
@apply p-0 m-0;
|
||||
}
|
||||
|
||||
.p-confirm-popup-content {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.p-confirm-popup-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.p-confirm-popup-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.p-confirm-popup-message {
|
||||
@apply m-0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.p-confirm-popup-reject.p-button-sm,
|
||||
.p-confirm-popup-accept.p-button-sm {
|
||||
@apply px-1.5 py-1 m-0;
|
||||
|
||||
& > span {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
.vertical-tabs-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
|
||||
.p-tabview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav {
|
||||
flex-direction: column;
|
||||
width: 150px;
|
||||
min-height: 100%;
|
||||
border: none;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
border-right: 4px solid var(--surface-hover);
|
||||
background-color: var(--surface-card);
|
||||
|
||||
transition: background-color 200ms, border-right-color 200ms;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--surface-100);
|
||||
}
|
||||
|
||||
.p-tabview-nav-link {
|
||||
transition: color 200ms;
|
||||
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
//background-color: var(--surface-card);
|
||||
background-color: initial;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
|
||||
border-radius: initial;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.p-tabview-selected {
|
||||
background-color: var(--surface-50);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
|
||||
.p-tabview-nav-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.color-warn {
|
||||
@apply bg-yellow-600/5 border-r-yellow-600/20;
|
||||
|
||||
&:hover {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600/40;
|
||||
}
|
||||
|
||||
|
||||
&.p-tabview-selected {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600;
|
||||
|
||||
.p-tabview-nav-link {
|
||||
@apply text-yellow-600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-yellow-600/10 border-r-yellow-600;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@use "fix-dialog";
|
||||
@use "fix-popup";
|
||||
@use "fix-tabs";
|
||||
@import "fix-dialog";
|
||||
//@import "fix-input";
|
||||
|
||||
//@import "theme";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
.Docked {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 1px;
|
||||
|
||||
background-image: url(/images/citadelLarge.png);
|
||||
left: 2px;
|
||||
top: 22px;
|
||||
transform: rotateZ(0deg);
|
||||
}
|
||||
@@ -1,39 +1,20 @@
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback } from 'react';
|
||||
import classes from './Characters.module.scss';
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
import clsx from 'clsx';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
export const Characters = ({ data }: CharactersProps) => {
|
||||
const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
const { mapRef } = useMapRootState();
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
|
||||
if (!character) {
|
||||
return;
|
||||
}
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.startTracking,
|
||||
data: { character_eve_id: character.eve_id },
|
||||
});
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: character.location?.solar_system_id?.toString(),
|
||||
});
|
||||
}, []);
|
||||
const handleSelect = useCallback(
|
||||
(character: CharacterTypeRaw) => {
|
||||
mapRef.current?.command(Commands.centerSystem, character?.location?.solar_system_id?.toString());
|
||||
},
|
||||
[mapRef],
|
||||
);
|
||||
|
||||
const items = data.map(character => (
|
||||
<li
|
||||
@@ -41,71 +22,21 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
|
||||
>
|
||||
{character.tracking_paused && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
|
||||
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
|
||||
'pi',
|
||||
PrimeIcons.PAUSE,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(character.location) && <div className={classes.Docked} />}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !character.online,
|
||||
['opacity-100']: character.online,
|
||||
},
|
||||
)}
|
||||
<div className="tooltip tooltip-bottom" title={character.name}>
|
||||
<a
|
||||
className={clsx('wd-characters-icons wd-bg-default', { ['character-online']: character.online })}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
|
||||
></div>
|
||||
></a>
|
||||
</div>
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<ul className="flex gap-1 characters" id="characters" ref={parent}>
|
||||
<ul className="flex characters" id="characters" ref={parent}>
|
||||
{items}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export default Characters;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PingType, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useContextMenuSystemItems } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
|
||||
export interface ContextMenuSystemProps {
|
||||
hubs: string[];
|
||||
userHubs: string[];
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
systemId: string | undefined;
|
||||
systems: SolarSystemRawType[];
|
||||
@@ -14,12 +13,10 @@ export interface ContextMenuSystemProps {
|
||||
onLockToggle(): void;
|
||||
onOpenSettings(): void;
|
||||
onHubToggle(): void;
|
||||
onUserHubToggle(): void;
|
||||
onSystemTag(val?: string): void;
|
||||
onSystemStatus(val: number): void;
|
||||
onSystemLabels(val: string): void;
|
||||
onCustomLabelDialog(): void;
|
||||
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
|
||||
onWaypointSet: WaypointSetContextHandler;
|
||||
}
|
||||
|
||||
@@ -28,7 +25,7 @@ export const ContextMenuSystem: React.FC<ContextMenuSystemProps> = ({ contextMen
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu className="min-w-[200px]" model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './useTagMenu';
|
||||
export * from './useStatusMenu';
|
||||
export * from './useLabelsMenu';
|
||||
export * from './useUserRoute';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from './useTagMenu.tsx';
|
||||
export * from './useTagMenu.ts';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
|
||||
|
||||
const AVAILABLE_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'X', 'Y', 'Z'];
|
||||
const AVAILABLE_NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
export const useTagMenu = (
|
||||
systems: SolarSystemRawType[],
|
||||
systemId: string | undefined,
|
||||
onSystemTag: (val?: string) => void,
|
||||
): (() => MenuItem) => {
|
||||
const ref = useRef({ onSystemTag, systems, systemId });
|
||||
ref.current = { onSystemTag, systems, systemId };
|
||||
|
||||
return useCallback(() => {
|
||||
const { onSystemTag, systemId, systems } = ref.current;
|
||||
const system = systemId ? getSystemById(systems, systemId) : undefined;
|
||||
|
||||
const isSelectedLetters = AVAILABLE_LETTERS.includes(system?.tag ?? '');
|
||||
const isSelectedNumbers = AVAILABLE_NUMBERS.includes(system?.tag ?? '');
|
||||
|
||||
const menuItem: MenuItem = {
|
||||
label: 'Tag',
|
||||
icon: PrimeIcons.HASHTAG,
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters || isSelectedNumbers }),
|
||||
items: [
|
||||
...(system?.tag !== '' && system?.tag !== null
|
||||
? [
|
||||
{
|
||||
label: 'Clear',
|
||||
icon: PrimeIcons.BAN,
|
||||
command: () => onSystemTag(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Letter',
|
||||
icon: PrimeIcons.TAGS,
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters }),
|
||||
items: AVAILABLE_LETTERS.map(x => ({
|
||||
label: x,
|
||||
icon: PrimeIcons.TAG,
|
||||
command: () => onSystemTag(x),
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: 'Digit',
|
||||
icon: PrimeIcons.TAGS,
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedNumbers }),
|
||||
items: AVAILABLE_NUMBERS.map(x => ({
|
||||
label: x,
|
||||
icon: PrimeIcons.TAG,
|
||||
command: () => onSystemTag(x),
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return menuItem;
|
||||
}, []);
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import clsx from 'clsx';
|
||||
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Button } from 'primereact/button';
|
||||
|
||||
const AVAILABLE_TAGS = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
];
|
||||
|
||||
export const useTagMenu = (
|
||||
systems: SolarSystemRawType[],
|
||||
systemId: string | undefined,
|
||||
onSystemTag: (val?: string) => void,
|
||||
): (() => MenuItem) => {
|
||||
const ref = useRef({ onSystemTag, systems, systemId });
|
||||
ref.current = { onSystemTag, systems, systemId };
|
||||
|
||||
return useCallback(() => {
|
||||
const { onSystemTag, systemId, systems } = ref.current;
|
||||
const system = systemId ? getSystemById(systems, systemId) : undefined;
|
||||
|
||||
const isSelectedTag = AVAILABLE_TAGS.includes(system?.tag ?? '');
|
||||
|
||||
const menuItem: MenuItem = {
|
||||
label: 'Tag',
|
||||
icon: PrimeIcons.HASHTAG,
|
||||
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedTag }),
|
||||
items: [
|
||||
{
|
||||
label: 'Digit',
|
||||
icon: PrimeIcons.TAGS,
|
||||
className: '!h-[128px] suppress-menu-behaviour',
|
||||
template: () => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
|
||||
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
|
||||
{AVAILABLE_TAGS.map(x => (
|
||||
<Button
|
||||
outlined={system?.tag !== x}
|
||||
severity="warning"
|
||||
key={x}
|
||||
value={x}
|
||||
size="small"
|
||||
className="p-[3px] justify-center"
|
||||
onClick={() => system?.tag !== x && onSystemTag(x)}
|
||||
>
|
||||
{x}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
disabled={!isSelectedTag}
|
||||
icon="pi pi-ban"
|
||||
size="small"
|
||||
className="!p-0 !w-[initial] justify-center"
|
||||
outlined
|
||||
severity="help"
|
||||
onClick={() => onSystemTag()}
|
||||
></Button>
|
||||
</div>
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return menuItem;
|
||||
}, []);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { MapUserAddIcon, MapUserDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
|
||||
interface UseUserRouteProps {
|
||||
systemId: string | undefined;
|
||||
userHubs: string[];
|
||||
onUserHubToggle(): void;
|
||||
}
|
||||
|
||||
export const useUserRoute = ({ userHubs, systemId, onUserHubToggle }: UseUserRouteProps) => {
|
||||
const {
|
||||
data: { isSubscriptionActive },
|
||||
windowsSettings,
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef({ userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings });
|
||||
ref.current = { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings };
|
||||
|
||||
return useCallback(() => {
|
||||
const { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings } = ref.current;
|
||||
|
||||
const isVisibleUserRoutes = windowsSettings.visible.some(x => x === WidgetsIds.userRoutes);
|
||||
|
||||
if (!isSubscriptionActive || !isVisibleUserRoutes || !systemId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: !userHubs.includes(systemId) ? 'Add User Route' : 'Remove User Route',
|
||||
icon: !userHubs.includes(systemId) ? (
|
||||
<MapUserAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapUserDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onUserHubToggle,
|
||||
},
|
||||
];
|
||||
}, [windowsSettings]);
|
||||
};
|
||||
@@ -5,29 +5,22 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
|
||||
interface UseContextMenuSystemHandlersProps {
|
||||
hubs: string[];
|
||||
userHubs: string[];
|
||||
systems: SolarSystemRawType[];
|
||||
outCommand: OutCommandHandler;
|
||||
}
|
||||
|
||||
export const useContextMenuSystemHandlers = ({
|
||||
systems,
|
||||
hubs,
|
||||
userHubs,
|
||||
outCommand,
|
||||
}: UseContextMenuSystemHandlersProps) => {
|
||||
export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
|
||||
const [system, setSystem] = useState<string>();
|
||||
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
|
||||
const ref = useRef({ hubs, userHubs, system, systems, outCommand, deleteSystems });
|
||||
ref.current = { hubs, userHubs, system, systems, outCommand, deleteSystems };
|
||||
const ref = useRef({ hubs, system, systems, outCommand, deleteSystems });
|
||||
ref.current = { hubs, system, systems, outCommand, deleteSystems };
|
||||
|
||||
const open = useCallback((ev: any, systemId: string) => {
|
||||
setSystem(systemId);
|
||||
@@ -79,37 +72,6 @@ export const useContextMenuSystemHandlers = ({
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
const onUserHubToggle = useCallback(() => {
|
||||
const { userHubs, system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
return;
|
||||
}
|
||||
|
||||
outCommand({
|
||||
type: !userHubs.includes(system) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
|
||||
data: {
|
||||
system_id: system,
|
||||
},
|
||||
});
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
// const onTogglePingRally = useCallback(() => {
|
||||
// const { userHubs, system, outCommand } = ref.current;
|
||||
// if (!system) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// outCommand({
|
||||
// type: OutCommand.openPing,
|
||||
// data: {
|
||||
// solar_system_id: system,
|
||||
// type: PingType.Rally,
|
||||
// },
|
||||
// });
|
||||
// setSystem(undefined);
|
||||
// }, []);
|
||||
|
||||
const onSystemTag = useCallback((tag?: string) => {
|
||||
const { system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
@@ -126,22 +88,6 @@ export const useContextMenuSystemHandlers = ({
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
const onSystemTemporaryName = useCallback((temporaryName?: string) => {
|
||||
const { system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
return;
|
||||
}
|
||||
|
||||
outCommand({
|
||||
type: OutCommand.updateSystemTemporaryName,
|
||||
data: {
|
||||
system_id: system,
|
||||
value: temporaryName ?? '',
|
||||
},
|
||||
});
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
const onSystemStatus = useCallback((status: number) => {
|
||||
const { system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
@@ -214,10 +160,7 @@ export const useContextMenuSystemHandlers = ({
|
||||
onDeleteSystem,
|
||||
onLockToggle,
|
||||
onHubToggle,
|
||||
onUserHubToggle,
|
||||
// onTogglePingRally,
|
||||
onSystemTag,
|
||||
onSystemTemporaryName,
|
||||
onSystemStatus,
|
||||
onSystemLabels,
|
||||
onOpenSettings,
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
useLabelsMenu,
|
||||
useStatusMenu,
|
||||
useTagMenu,
|
||||
useUserRoute,
|
||||
} from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
|
||||
import { useLabelsMenu, useStatusMenu, useTagMenu } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import classes from './ContextMenuSystem.module.scss';
|
||||
@@ -11,23 +6,11 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { ContextMenuSystemProps } from '@/hooks/Mapper/components/contexts';
|
||||
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { PingType } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import clsx from 'clsx';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const useContextMenuSystemItems = ({
|
||||
onDeleteSystem,
|
||||
onLockToggle,
|
||||
onHubToggle,
|
||||
onUserHubToggle,
|
||||
onTogglePing,
|
||||
onSystemTag,
|
||||
onSystemStatus,
|
||||
onSystemLabels,
|
||||
@@ -36,40 +19,15 @@ export const useContextMenuSystemItems = ({
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
hubs,
|
||||
userHubs,
|
||||
systems,
|
||||
}: Omit<ContextMenuSystemProps, 'contextMenuRef'>) => {
|
||||
const getTags = useTagMenu(systems, systemId, onSystemTag);
|
||||
const getStatus = useStatusMenu(systems, systemId, onSystemStatus);
|
||||
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
|
||||
const canManageSystem = useMapCheckPermissions([UserPermission.UPDATE_SYSTEM]);
|
||||
const canDeleteSystem = useMapCheckPermissions([UserPermission.DELETE_SYSTEM]);
|
||||
const getUserRoutes = useUserRoute({ userHubs, systemId, onUserHubToggle });
|
||||
|
||||
const {
|
||||
data: { pings, isSubscriptionActive },
|
||||
} = useMapRootState();
|
||||
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
|
||||
const isShowPingBtn = useMemo(() => {
|
||||
if (!isSubscriptionActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pings.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pings[0].solar_system_id === systemId;
|
||||
}, [isSubscriptionActive, pings, systemId]);
|
||||
|
||||
return useMemo((): MenuItem[] => {
|
||||
return useMemo(() => {
|
||||
const system = systemId ? getSystemById(systems, systemId) : undefined;
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
|
||||
const hasPing = ping?.solar_system_id === systemId;
|
||||
|
||||
if (!system || !systemId) {
|
||||
return [];
|
||||
@@ -82,9 +40,7 @@ export const useContextMenuSystemItems = ({
|
||||
return (
|
||||
<FastSystemActions
|
||||
systemId={systemId}
|
||||
systemName={systemStaticInfo.solar_system_name}
|
||||
regionName={systemStaticInfo.region_name}
|
||||
isWH={isWormholeSpace(systemStaticInfo.system_class)}
|
||||
systemName={system.system_static_info.solar_system_name}
|
||||
showEdit
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
@@ -95,45 +51,13 @@ export const useContextMenuSystemItems = ({
|
||||
getTags(),
|
||||
getStatus(),
|
||||
...getLabels(),
|
||||
...getWaypointMenu(systemId, systemStaticInfo.system_class),
|
||||
...getWaypointMenu(systemId, system.system_static_info.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
command: onHubToggle,
|
||||
},
|
||||
...getUserRoutes(),
|
||||
|
||||
{ separator: true },
|
||||
{
|
||||
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing),
|
||||
disabled: !isShowPingBtn,
|
||||
template: () => {
|
||||
const iconClasses = clsx({
|
||||
'pi text-cyan-400 hero-signal': !hasPing,
|
||||
'pi text-red-400 hero-signal-slash': hasPing,
|
||||
});
|
||||
|
||||
if (isShowPingBtn) {
|
||||
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Locked. Ping can be set only for one system."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon={iconClasses}>
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
...(system.locked && canLockSystem
|
||||
...(system.locked
|
||||
? [
|
||||
{
|
||||
label: 'Unlock',
|
||||
@@ -141,60 +65,31 @@ export const useContextMenuSystemItems = ({
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!system.locked && canManageSystem
|
||||
? [
|
||||
: [
|
||||
{
|
||||
label: 'Lock',
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: onLockToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(canDeleteSystem && !system.locked
|
||||
? [
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteSystem,
|
||||
disabled: hasPing,
|
||||
template: () => {
|
||||
if (!hasPing) {
|
||||
return <WdMenuItem icon="text-red-400 pi pi-trash">Delete</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Locked. System can not be deleted until ping set."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon="text-red-400 pi pi-trash">
|
||||
Delete
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
];
|
||||
}, [
|
||||
systemId,
|
||||
systems,
|
||||
systemId,
|
||||
getTags,
|
||||
getStatus,
|
||||
getLabels,
|
||||
getWaypointMenu,
|
||||
getUserRoutes,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
canLockSystem,
|
||||
onLockToggle,
|
||||
canDeleteSystem,
|
||||
onDeleteSystem,
|
||||
onOpenSettings,
|
||||
onTogglePing,
|
||||
ping,
|
||||
isShowPingBtn,
|
||||
onLockToggle,
|
||||
onDeleteSystem,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/ty
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
|
||||
export interface ContextMenuSystemInfoProps {
|
||||
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
|
||||
@@ -50,6 +48,7 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
if (!systemId || !system) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
className: classes.FastActions,
|
||||
@@ -58,8 +57,6 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
<FastSystemActions
|
||||
systemId={systemId}
|
||||
systemName={system.solar_system_name}
|
||||
regionName={system.region_name}
|
||||
isWH={isWormholeSpace(system.system_class)}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
);
|
||||
@@ -70,12 +67,8 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
command: onHubToggle,
|
||||
},
|
||||
...(!systemOnMap
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { RefObject, useCallback, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Commands, MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import * as React from 'react';
|
||||
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
|
||||
export const useContextMenuSystemInfoHandlers = () => {
|
||||
const { outCommand } = useMapRootState();
|
||||
const { hubs = [], toggleHubCommand } = useRouteProvider();
|
||||
interface UseContextMenuSystemHandlersProps {
|
||||
hubs: string[];
|
||||
outCommand: OutCommandHandler;
|
||||
mapRef: RefObject<MapHandlers>;
|
||||
}
|
||||
|
||||
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: UseContextMenuSystemHandlersProps) => {
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
|
||||
const [system, setSystem] = useState<string>();
|
||||
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
|
||||
|
||||
const ref = useRef({ hubs, system, outCommand, toggleHubCommand });
|
||||
ref.current = { hubs, system, outCommand, toggleHubCommand };
|
||||
const ref = useRef({ hubs, system, outCommand, mapRef });
|
||||
ref.current = { hubs, system, outCommand, mapRef };
|
||||
|
||||
const open = useCallback(
|
||||
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
|
||||
@@ -33,17 +33,22 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
);
|
||||
|
||||
const onHubToggle = useCallback(() => {
|
||||
const { system } = ref.current;
|
||||
const { hubs, system, outCommand } = ref.current;
|
||||
if (!system) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current.toggleHubCommand(system);
|
||||
outCommand({
|
||||
type: !hubs.includes(system) ? OutCommand.addHub : OutCommand.deleteHub,
|
||||
data: {
|
||||
system_id: system,
|
||||
},
|
||||
});
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
const onAddSystem = useCallback(() => {
|
||||
const { system: solarSystemId, outCommand } = ref.current;
|
||||
const { system: solarSystemId, outCommand, mapRef } = ref.current;
|
||||
if (!solarSystemId) {
|
||||
return;
|
||||
}
|
||||
@@ -54,14 +59,8 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
system_id: solarSystemId,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO add it to some queue
|
||||
setTimeout(() => {
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: solarSystemId,
|
||||
});
|
||||
|
||||
mapRef.current?.command(Commands.centerSystem, solarSystemId);
|
||||
setSystem(undefined);
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { Node } from 'reactflow';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const useContextMenuSystemMultipleHandlers = () => {
|
||||
const {
|
||||
data: { pings },
|
||||
} = useMapRootState();
|
||||
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
|
||||
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
|
||||
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
|
||||
|
||||
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
|
||||
setSystems(systems_);
|
||||
ev.preventDefault();
|
||||
@@ -31,17 +24,13 @@ export const useContextMenuSystemMultipleHandlers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const sysToDel = systems
|
||||
.filter(x => !x.data.locked)
|
||||
.filter(x => x.id !== ping?.solar_system_id)
|
||||
.map(x => x.id);
|
||||
|
||||
const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
|
||||
if (sysToDel.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteSystems(sysToDel);
|
||||
}, [deleteSystems, systems, ping]);
|
||||
}, [deleteSystems, systems]);
|
||||
|
||||
return {
|
||||
handleSystemMultipleContext,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons.ts';
|
||||
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
import clsx from 'clsx';
|
||||
@@ -9,22 +9,13 @@ import { PrimeIcons } from 'primereact/api';
|
||||
export interface FastSystemActionsProps {
|
||||
systemId: string;
|
||||
systemName: string;
|
||||
regionName: string;
|
||||
isWH: boolean;
|
||||
showEdit?: boolean;
|
||||
onOpenSettings(): void;
|
||||
}
|
||||
|
||||
export const FastSystemActions = ({
|
||||
systemId,
|
||||
systemName,
|
||||
regionName,
|
||||
isWH,
|
||||
onOpenSettings,
|
||||
showEdit,
|
||||
}: FastSystemActionsProps) => {
|
||||
const ref = useRef({ systemId, systemName, regionName, isWH });
|
||||
ref.current = { systemId, systemName, regionName, isWH };
|
||||
export const FastSystemActions = ({ systemId, systemName, onOpenSettings, showEdit }: FastSystemActionsProps) => {
|
||||
const ref = useRef({ systemId, systemName });
|
||||
ref.current = { systemId, systemName };
|
||||
|
||||
const handleOpenZKB = useCallback(
|
||||
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
|
||||
@@ -36,17 +27,10 @@ export const FastSystemActions = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenDotlan = useCallback(() => {
|
||||
if (ref.current.isWH) {
|
||||
window.open(`https://evemaps.dotlan.net/system/${ref.current.systemName}`, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
return window.open(
|
||||
`https://evemaps.dotlan.net/map/${ref.current.regionName.replace(/ /gim, '_')}/${ref.current.systemName}#jumps`,
|
||||
'_blank',
|
||||
);
|
||||
}, []);
|
||||
const handleOpenDotlan = useCallback(
|
||||
() => window.open(`https://evemaps.dotlan.net/system/${ref.current.systemName}`, '_blank'),
|
||||
[],
|
||||
);
|
||||
|
||||
const copySystemNameToClipboard = useCallback(async () => {
|
||||
try {
|
||||
@@ -59,21 +43,9 @@ export const FastSystemActions = ({
|
||||
return (
|
||||
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}>
|
||||
<div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
|
||||
source={ZKB_ICON}
|
||||
onClick={handleOpenZKB}
|
||||
/>
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open Anoikis' }}
|
||||
source={ANOIK_ICON}
|
||||
onClick={handleOpenAnoikis}
|
||||
/>
|
||||
<WdImgButton
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Open Dotlan' }}
|
||||
source={DOTLAN_ICON}
|
||||
onClick={handleOpenDotlan}
|
||||
/>
|
||||
<WdImgButton source={ZKB_ICON} onClick={handleOpenZKB} />
|
||||
<WdImgButton source={ANOIK_ICON} onClick={handleOpenAnoikis} />
|
||||
<WdImgButton source={DOTLAN_ICON} onClick={handleOpenDotlan} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center pl-1">
|
||||
@@ -81,14 +53,14 @@ export const FastSystemActions = ({
|
||||
textSize={WdImageSize.off}
|
||||
className={PrimeIcons.COPY}
|
||||
onClick={copySystemNameToClipboard}
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }}
|
||||
tooltip={{ content: 'Copy system name' }}
|
||||
/>
|
||||
{showEdit && (
|
||||
<WdImgButton
|
||||
textSize={WdImageSize.off}
|
||||
className="pi pi-pen-to-square text-base"
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
|
||||
tooltip={{ content: 'Edit system name and description' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCallback } from 'react';
|
||||
import { isPossibleSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { SOLAR_SYSTEM_CLASS_IDS } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
const imperialSpace = [SOLAR_SYSTEM_CLASS_IDS.hs, SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
const criminalSpace = [SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
@@ -47,7 +47,7 @@ export const useJumpPlannerMenu = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const origin = getSystemStaticInfo(systemIdFrom);
|
||||
const origin = getSystemById(systems, systemIdFrom)?.system_static_info;
|
||||
|
||||
if (!origin) {
|
||||
return [];
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.active {
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './parseMapUserSettings.ts';
|
||||
@@ -1,67 +0,0 @@
|
||||
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export const REQUIRED_KEYS = [
|
||||
'widgets',
|
||||
'interface',
|
||||
'onTheMap',
|
||||
'routes',
|
||||
'localWidget',
|
||||
'signaturesWidget',
|
||||
'killsWidget',
|
||||
] as const;
|
||||
|
||||
type RequiredKeys = (typeof REQUIRED_KEYS)[number];
|
||||
|
||||
/** Custom error for any parsing / validation issue */
|
||||
export class MapUserSettingsParseError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(`MapUserSettings parse error: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
|
||||
|
||||
/** Minimal check that an object matches SettingsWithVersion<*> */
|
||||
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
|
||||
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
|
||||
|
||||
/** Ensure every required key is present */
|
||||
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
|
||||
typeof v === 'object' && v !== null && REQUIRED_KEYS.every(k => k in v);
|
||||
|
||||
/* ------------------------------ Main parser ------------------------------- */
|
||||
|
||||
/**
|
||||
* Parses and validates a JSON string as `MapUserSettings`.
|
||||
*
|
||||
* @throws `MapUserSettingsParseError` – если строка не JSON или нарушена структура
|
||||
*/
|
||||
export const parseMapUserSettings = (json: unknown): MapUserSettings => {
|
||||
if (typeof json !== 'string') throw new MapUserSettingsParseError('Input must be a JSON string');
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch (e) {
|
||||
throw new MapUserSettingsParseError(`Invalid JSON: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (!hasAllRequiredKeys(data)) {
|
||||
const missing = REQUIRED_KEYS.filter(k => !(k in (data as any)));
|
||||
throw new MapUserSettingsParseError(`Missing top-level field(s): ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
for (const key of REQUIRED_KEYS) {
|
||||
if (!isSettingsWithVersion((data as any)[key])) {
|
||||
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
|
||||
}
|
||||
}
|
||||
|
||||
// Everything passes, so cast is safe
|
||||
return data as MapUserSettings;
|
||||
};
|
||||
|
||||
/* ------------------------------ Usage example ----------------------------- */
|
||||
|
||||
// const raw = fetchFromServer(); // string
|
||||
// const settings = parseMapUserSettings(raw);
|
||||
@@ -1,4 +1,2 @@
|
||||
export * from './useSystemInfo';
|
||||
export * from './useGetOwnOnlineCharacters';
|
||||
export * from './useElementWidth';
|
||||
export * from './useDetectSettingsChanged';
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useDetectSettingsChanged = () => {
|
||||
const {
|
||||
storedSettings: {
|
||||
interfaceSettings,
|
||||
settingsRoutes,
|
||||
settingsLocal,
|
||||
settingsSignatures,
|
||||
settingsOnTheMap,
|
||||
settingsKills,
|
||||
},
|
||||
} = useMapRootState();
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect(
|
||||
() => setCounter(x => x + 1),
|
||||
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
|
||||
);
|
||||
|
||||
return counter;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useState, useLayoutEffect, RefObject } from 'react';
|
||||
|
||||
/**
|
||||
* useElementWidth
|
||||
*
|
||||
* A custom hook that accepts a ref to an HTML element and returns its current width.
|
||||
* It uses a ResizeObserver and window resize listener to update the width when necessary.
|
||||
*
|
||||
* @param ref - A RefObject pointing to an HTML element.
|
||||
* @returns The current width of the element.
|
||||
*/
|
||||
export function useElementWidth<T extends HTMLElement>(ref: RefObject<T>): number {
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateWidth = () => {
|
||||
if (ref.current) {
|
||||
const newWidth = ref.current.getBoundingClientRect().width;
|
||||
if (newWidth > 0) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateWidth(); // Initial measurement
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
const id = setTimeout(updateWidth, 100);
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", updateWidth);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return width;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
|
||||
interface UseSystemInfoProps {
|
||||
systemId: string;
|
||||
@@ -12,12 +12,14 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
|
||||
data: { systems, connections },
|
||||
} = useMapRootState();
|
||||
|
||||
const { systems: systemStatics } = useLoadSystemStatic({ systems: [systemId] });
|
||||
|
||||
return useMemo(() => {
|
||||
const staticInfo = getSystemStaticInfo(parseInt(systemId));
|
||||
const staticInfo = systemStatics.get(parseInt(systemId));
|
||||
const dynamicInfo = getSystemById(systems, systemId);
|
||||
|
||||
if (!staticInfo || !dynamicInfo) {
|
||||
return { dynamicInfo, staticInfo, leadsTo: [] };
|
||||
throw new Error(`Error on getting system ${systemId}`);
|
||||
}
|
||||
|
||||
const leadsTo = connections
|
||||
@@ -27,5 +29,5 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
|
||||
.filter(x => x !== systemId);
|
||||
|
||||
return { dynamicInfo, staticInfo, leadsTo };
|
||||
}, [systemId, systems, connections]);
|
||||
}, [systemStatics, systemId, systems, connections]);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
.MapRoot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--rf-bg-color, #0C0A09);
|
||||
|
||||
&.BackgroundAlternateColor {
|
||||
background-color: var(--rf-soft-bg-color, #171717);
|
||||
--rf-node-bg-color: var(--rf-node-soft-bg-color, #202020);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useRef } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeDragHandler,
|
||||
OnConnect,
|
||||
OnMoveEnd,
|
||||
OnSelectionChangeFunc,
|
||||
SelectionDragHandler,
|
||||
SelectionMode,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
NodeChange,
|
||||
useReactFlow,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import './styles/neon-theme.scss';
|
||||
import './styles/eve-common.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapHandlers, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
SolarSystemEdge,
|
||||
SolarSystemNode,
|
||||
useContextMenuConnectionHandlers,
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
@@ -75,30 +79,26 @@ const initialEdges = [
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
custom: SolarSystemNode,
|
||||
} as never;
|
||||
|
||||
const edgeTypes = {
|
||||
floating: SolarSystemEdge,
|
||||
};
|
||||
|
||||
export const MAP_ROOT_ID = 'MAP_ROOT_ID';
|
||||
|
||||
interface MapCompProps {
|
||||
refn: ForwardedRef<MapHandlers>;
|
||||
onCommand: OutCommandHandler;
|
||||
onSelectionChange: OnMapSelectionChange;
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
minimapClasses?: string;
|
||||
isShowMinimap?: boolean;
|
||||
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
|
||||
showKSpaceBG?: boolean;
|
||||
isThickConnections?: boolean;
|
||||
isShowBackgroundPattern?: boolean;
|
||||
isSoftBackground?: boolean;
|
||||
theme?: string;
|
||||
pings: PingData[];
|
||||
minimapPlacement?: PanelPosition;
|
||||
localShowShipName?: boolean;
|
||||
}
|
||||
|
||||
const MapComp = ({
|
||||
@@ -111,32 +111,24 @@ const MapComp = ({
|
||||
onSelectionContextMenu,
|
||||
isShowMinimap,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
isShowBackgroundPattern,
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
pings,
|
||||
minimapPlacement = 'bottom-right',
|
||||
localShowShipName = false,
|
||||
}: MapCompProps) => {
|
||||
const { getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
const { getNode } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<SolarSystemRawType>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>[]>(initialEdges);
|
||||
|
||||
useMapHandlers(refn, onSelectionChange);
|
||||
useUpdateNodes(nodes);
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers();
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||
const {
|
||||
data: { systems },
|
||||
} = useMapRootState();
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
};
|
||||
}, [nodeComponent]);
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
|
||||
const systemsRef = useRef({ systems });
|
||||
systemsRef.current = { systems };
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
params => {
|
||||
@@ -193,37 +185,40 @@ const MapComp = ({
|
||||
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
// prevents single node deselection on background / same node click
|
||||
// allows deseletion of all nodes if multiple are currently selected
|
||||
if (changes.length === 1 && changes[0].type == 'select' && changes[0].selected === false) {
|
||||
changes[0].selected = getNodes().filter(node => node.selected).length === 1;
|
||||
}
|
||||
|
||||
const systemsIdsToRemove: string[] = [];
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
if (change.type === 'remove') {
|
||||
const node = getNode(change.id);
|
||||
const { systems = [] } = systemsRef.current;
|
||||
if (node?.data?.id && !systems.map(s => s.id).includes(node?.data?.id)) {
|
||||
return [...acc, change];
|
||||
} else if (!node?.data?.locked) {
|
||||
systemsIdsToRemove.push(node?.data?.id);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
return [...acc, change];
|
||||
}, [] as NodeChange[]);
|
||||
|
||||
if (systemsIdsToRemove.length) {
|
||||
deleteSystems(systemsIdsToRemove);
|
||||
}
|
||||
|
||||
onNodesChange(nextChanges);
|
||||
},
|
||||
[getNodes, onNodesChange],
|
||||
[deleteSystems, getNode, onNodesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
update(x => ({
|
||||
...x,
|
||||
showKSpaceBG: showKSpaceBG,
|
||||
isThickConnections: isThickConnections,
|
||||
pings,
|
||||
localShowShipName,
|
||||
}));
|
||||
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
|
||||
}, [showKSpaceBG, update]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-window-id={MAP_ROOT_ID}
|
||||
className={clsx(classes.MapRoot, { [classes.BackgroundAlternateColor]: isSoftBackground })}
|
||||
>
|
||||
<div className={classes.MapRoot}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
@@ -235,7 +230,7 @@ const MapComp = ({
|
||||
defaultViewport={getViewPortFromStore()}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
snapToGrid
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
@@ -243,11 +238,6 @@ const MapComp = ({
|
||||
onConnectStart={() => update({ isConnecting: true })}
|
||||
onConnectEnd={() => update({ isConnecting: false })}
|
||||
onNodeMouseEnter={(_, node) => update({ hoverNodeId: node.id })}
|
||||
onPaneClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
// onKeyUp=
|
||||
onNodeMouseLeave={() => update({ hoverNodeId: null })}
|
||||
onEdgeClick={(_, t) => {
|
||||
onConnectionInfoClick?.(t.data);
|
||||
@@ -267,22 +257,14 @@ const MapComp = ({
|
||||
minZoom={0.2}
|
||||
maxZoom={1.5}
|
||||
elevateNodesOnSelect
|
||||
deleteKeyCode={['']}
|
||||
{...(isPanAndDrag
|
||||
? {
|
||||
selectionOnDrag: true,
|
||||
panOnDrag: [2],
|
||||
}
|
||||
: {})}
|
||||
deleteKeyCode={['Delete']}
|
||||
// TODO need create clear example with problem with that flag
|
||||
// if system is not visible edge not drawing (and any render in Custom node is not happening)
|
||||
// onlyRenderVisibleElements
|
||||
selectionMode={SelectionMode.Partial}
|
||||
>
|
||||
{isShowMinimap && (
|
||||
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
|
||||
)}
|
||||
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
|
||||
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>
|
||||
Test // DON NOT REMOVE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { MapUnionTypes, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { MapUnionTypes } from '@/hooks/Mapper/types';
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
|
||||
export type MapData = MapUnionTypes & {
|
||||
@@ -8,9 +8,6 @@ export type MapData = MapUnionTypes & {
|
||||
hoverNodeId: string | null;
|
||||
visibleNodes: Set<string>;
|
||||
showKSpaceBG: boolean;
|
||||
isThickConnections: boolean;
|
||||
linkedSigEveId: string;
|
||||
localShowShipName: boolean;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
@@ -20,7 +17,6 @@ interface MapProviderProps {
|
||||
|
||||
const INITIAL_DATA: MapData = {
|
||||
wormholesData: {},
|
||||
wormholes: [],
|
||||
effects: {},
|
||||
characters: [],
|
||||
userCharacters: [],
|
||||
@@ -31,19 +27,8 @@ const INITIAL_DATA: MapData = {
|
||||
isConnecting: false,
|
||||
connections: [],
|
||||
hoverNodeId: null,
|
||||
linkedSigEveId: '',
|
||||
visibleNodes: new Set(),
|
||||
showKSpaceBG: false,
|
||||
isThickConnections: false,
|
||||
userPermissions: {},
|
||||
systemSignatures: {} as Record<string, SystemSignature[]>,
|
||||
options: {} as Record<string, string | boolean>,
|
||||
isSubscriptionActive: false,
|
||||
mainCharacterEveId: null,
|
||||
followingCharacterEveId: null,
|
||||
userHubs: [],
|
||||
pings: [],
|
||||
localShowShipName: false,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
@@ -59,7 +44,7 @@ const MapContext = createContext<MapContextProps>({
|
||||
outCommand: async () => void 0,
|
||||
});
|
||||
|
||||
export const MapProvider = ({ children, onCommand }: MapProviderProps) => {
|
||||
export const MapProvider: React.FC<MapProviderProps> = ({ children, onCommand }) => {
|
||||
const { update, ref } = useContextStore<MapData>({ ...INITIAL_DATA });
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.ConnectionTimeEOL {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
|
||||
background-image: linear-gradient(207deg, transparent, #7452c3e3);
|
||||
}
|
||||
|
||||
.ConnectionFrigate {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
|
||||
background-image: linear-gradient(207deg, transparent, #325d88);
|
||||
}
|
||||
|
||||
.ConnectionSave {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-save));
|
||||
background-image: linear-gradient(207deg, transparent, rgba(155, 102, 45, 0.85));
|
||||
}
|
||||
|
||||
.SelectedItem {
|
||||
background-color: var(--selected-item-bg);
|
||||
background-color: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,11 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Edge } from '@reactflow/core/dist/esm/types/edges';
|
||||
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { Edge } from 'reactflow';
|
||||
import { MASS_STATE_NAMES, MASS_STATE_NAMES_ORDER } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
export interface ContextMenuConnectionProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
@@ -42,72 +35,46 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
}
|
||||
|
||||
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
|
||||
const isWormhole = edge.data?.type !== ConnectionType.gate;
|
||||
|
||||
return [
|
||||
...(isWormhole
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.normal : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
@@ -118,7 +85,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
|
||||
}, [edge, onChangeTimeState, onDeleteConnection, onChangeMassState, onChangeShipSizeStatus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Edge, EdgeMouseHandler } from 'reactflow';
|
||||
import { EdgeMouseHandler } from 'reactflow';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { useMapState } from '../../MapProvider.tsx';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Edge } from '@reactflow/core/dist/esm/types/edges';
|
||||
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
|
||||
export const useContextMenuConnectionHandlers = () => {
|
||||
@@ -46,23 +47,6 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
setEdge(undefined);
|
||||
};
|
||||
|
||||
const onChangeType = useCallback((type: ConnectionType) => {
|
||||
const { edge, outCommand } = ref.current;
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
outCommand({
|
||||
type: OutCommand.updateConnectionType,
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: type,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onChangeMassState = useCallback((status: MassState) => {
|
||||
const { edge, outCommand } = ref.current;
|
||||
|
||||
@@ -96,16 +80,14 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (status === ShipSizeStatus.small) {
|
||||
outCommand({
|
||||
type: OutCommand.updateConnectionMassStatus,
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: MassState.normal,
|
||||
},
|
||||
});
|
||||
}
|
||||
outCommand({
|
||||
type: OutCommand.updateConnectionMassStatus,
|
||||
data: {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: MassState.normal,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onToggleMassSave = useCallback((locked: boolean) => {
|
||||
@@ -136,7 +118,6 @@ export const useContextMenuConnectionHandlers = () => {
|
||||
contextMenuRef,
|
||||
onDeleteConnection,
|
||||
onChangeTimeState,
|
||||
onChangeType,
|
||||
onChangeMassState,
|
||||
onChangeShipSizeStatus,
|
||||
onToggleMassSave,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { useMapState } from '../../MapProvider.tsx';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
type UseContextMenuRootHandlers = {
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
};
|
||||
|
||||
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
|
||||
export const useContextMenuRootHandlers = () => {
|
||||
const rf = useReactFlow();
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
const { outCommand } = useMapState();
|
||||
const [position, setPosition] = useState<XYPosition | null>(null);
|
||||
|
||||
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -20,17 +18,14 @@ export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHa
|
||||
contextMenuRef.current?.show(e);
|
||||
};
|
||||
|
||||
const ref = useRef({ onAddSystem, position });
|
||||
ref.current = { onAddSystem, position };
|
||||
|
||||
const onAddSystemCallback = useCallback(() => {
|
||||
ref.current.onAddSystem?.({ coordinates: position });
|
||||
}, [position]);
|
||||
const onAddSystem = () => {
|
||||
outCommand({ type: OutCommand.manualAddSystem, data: { coordinates: position } });
|
||||
};
|
||||
|
||||
return {
|
||||
handleRootContext,
|
||||
|
||||
contextMenuRef,
|
||||
onAddSystem: onAddSystemCallback,
|
||||
onAddSystem,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
.KillsBookmark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 4px 3px;
|
||||
|
||||
}
|
||||
|
||||
.KillsBookmarkWithIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px #000;
|
||||
padding-right: 2px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
line-height: 12px;
|
||||
font-weight: 700;
|
||||
text-size-adjust: 100%;
|
||||
|
||||
.pi {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 9px;
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.TooltipContainer {
|
||||
background-color: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 3px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 2px;
|
||||
pointer-events: auto;
|
||||
max-width: 500px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useKillsCounter } from '../../hooks/useKillsCounter.ts';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import {
|
||||
KILLS_ROW_HEIGHT,
|
||||
SystemKillsList,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/SystemKillsList';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 40;
|
||||
|
||||
type KillsBookmarkTooltipProps = {
|
||||
killsCount: number;
|
||||
killsActivityType: string | null;
|
||||
systemId: string;
|
||||
className?: string;
|
||||
size?: TooltipSize;
|
||||
} & WithChildren &
|
||||
WithClassName;
|
||||
|
||||
export const KillsCounter = ({
|
||||
killsCount,
|
||||
systemId,
|
||||
className,
|
||||
children,
|
||||
size = TooltipSize.xs,
|
||||
}: KillsBookmarkTooltipProps) => {
|
||||
const { isLoading, kills: detailedKills } = useKillsCounter({
|
||||
realSystemId: systemId,
|
||||
});
|
||||
|
||||
const limitedKills = useMemo(() => {
|
||||
if (!detailedKills || detailedKills.length === 0) return [];
|
||||
return detailedKills.slice(0, killsCount);
|
||||
}, [detailedKills, killsCount]);
|
||||
|
||||
if (!killsCount || limitedKills.length === 0 || !systemId || isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate height based on number of kills, but ensure a minimum height
|
||||
const killsNeededHeight = limitedKills.length * KILLS_ROW_HEIGHT;
|
||||
// Add a small buffer (10px) to prevent scrollbar from appearing unnecessarily
|
||||
const tooltipHeight = Math.max(MIN_TOOLTIP_HEIGHT, Math.min(killsNeededHeight + 10, 500));
|
||||
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
content={
|
||||
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
|
||||
<div className="flex-1 h-full">
|
||||
<SystemKillsList kills={limitedKills} onlyOneSystem timeRange={1} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
tooltipClassName="!px-0"
|
||||
size={size}
|
||||
interactive={true}
|
||||
>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './KillsCounter.tsx';
|
||||
@@ -1,54 +0,0 @@
|
||||
.TooltipActive {
|
||||
pointer-events: auto !important;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hoverTarget {
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.localCounter {
|
||||
mix-blend-mode: screen;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: var(--rf-node-local-counter);
|
||||
|
||||
&.hasUserCharacters {
|
||||
color: var(--rf-has-user-characters);
|
||||
}
|
||||
|
||||
& > i {
|
||||
font-size: 9px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Pathfinder {
|
||||
.localCounter {
|
||||
@-moz-document url-prefix() {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
|
||||
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import classes from './LocalCounter.module.scss';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useLocalCharactersItemTemplate } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
|
||||
|
||||
interface LocalCounterProps {
|
||||
localCounterCharacters: Array<CharItemProps>;
|
||||
hasUserCharacters: boolean;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
|
||||
const {
|
||||
data: { localShowShipName },
|
||||
} = useMapState();
|
||||
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
|
||||
const theme = useTheme();
|
||||
|
||||
const pilotTooltipContent = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
minWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} autoSize={true} />
|
||||
</div>
|
||||
);
|
||||
}, [localCounterCharacters, itemTemplate]);
|
||||
|
||||
if (localCounterCharacters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.TooltipActive, {
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
|
||||
<div className={clsx(classes.hoverTarget)}>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
{showIcon && <i className="pi pi-users" />}
|
||||
<span>{localCounterCharacters.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './LocalCounter';
|
||||
@@ -1,55 +1,27 @@
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@import "@/hooks/Mapper/components/map/styles/neon-variables";
|
||||
|
||||
.EdgePathBack {
|
||||
fill: none;
|
||||
stroke: #80a5c5;
|
||||
stroke-width: 3px;
|
||||
|
||||
&.TimeCrit {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.Hovered {
|
||||
stroke: #b5c8d9;
|
||||
|
||||
&.TimeCrit {
|
||||
stroke: #ef7dce;
|
||||
}
|
||||
}
|
||||
|
||||
&.Tick {
|
||||
stroke-width: 5px;
|
||||
|
||||
&.TimeCrit {
|
||||
stroke-width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Gate {
|
||||
stroke: #9aff40;
|
||||
.react-flow__edge.selected {
|
||||
.EdgePathBack {
|
||||
stroke: $pastel-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathFront {
|
||||
fill: none;
|
||||
|
||||
stroke: #2c3844;
|
||||
stroke-width: 2px;
|
||||
|
||||
&.MassVerge:not(&.Frigate) {
|
||||
stroke: #af0000;
|
||||
stroke: #af2900;
|
||||
}
|
||||
|
||||
&.MassHalf:not(&.Frigate) {
|
||||
stroke: #ffd700;
|
||||
stroke: #a85f00;
|
||||
}
|
||||
|
||||
&.Frigate {
|
||||
stroke: #d4f0ff;
|
||||
}
|
||||
|
||||
&.Gate {
|
||||
stroke: #1c1e15;
|
||||
stroke: #4e62c9;
|
||||
}
|
||||
|
||||
&.Hovered {
|
||||
@@ -65,15 +37,28 @@
|
||||
}
|
||||
|
||||
&.Frigate {
|
||||
stroke: #d4f0ff;
|
||||
stroke: #41acd7;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathBack {
|
||||
fill: none;
|
||||
|
||||
stroke: #80a5c5;
|
||||
stroke-width: 3px;
|
||||
|
||||
&.TimeCrit {
|
||||
stroke: #f11ab2;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
&.Tick {
|
||||
stroke-width: 3px;
|
||||
&.Hovered {
|
||||
stroke: #b5c8d9;
|
||||
|
||||
&.Hovered {
|
||||
stroke-width: 3px;
|
||||
&.TimeCrit {
|
||||
stroke: #ef7dce;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,23 +69,7 @@
|
||||
stroke-width: 8px;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
border: 1px solid var(--pastel-blue);
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
z-index: 1001;
|
||||
|
||||
&.Tick {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
&.Right {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.LinkLabel {
|
||||
.LinkLabel{
|
||||
font-size: 9px;
|
||||
line-height: 10px;
|
||||
padding: 2px 4px;
|
||||
@@ -116,3 +85,13 @@
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid #5a7d9a;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,70 +4,36 @@ import classes from './SolarSystemEdge.module.scss';
|
||||
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
|
||||
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
|
||||
import clsx from 'clsx';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
const MAP_TRANSLATES: Record<string, string> = {
|
||||
[Position.Top]: 'translate(-48%, 0%)',
|
||||
[Position.Top]: 'translate(-50%, 0%)',
|
||||
[Position.Bottom]: 'translate(-50%, -100%)',
|
||||
[Position.Left]: 'translate(0%, -50%)',
|
||||
[Position.Right]: 'translate(-100%, -50%)',
|
||||
};
|
||||
|
||||
const MAP_OFFSETS_TICK: Record<string, { x: number; y: number }> = {
|
||||
[Position.Top]: { x: 0, y: 3 },
|
||||
[Position.Bottom]: { x: 0, y: -3 },
|
||||
[Position.Left]: { x: 3, y: 0 },
|
||||
[Position.Right]: { x: -3, y: 0 },
|
||||
};
|
||||
|
||||
const MAP_OFFSETS: Record<string, { x: number; y: number }> = {
|
||||
[Position.Top]: { x: 0, y: 0 },
|
||||
[Position.Bottom]: { x: 0, y: 0 },
|
||||
[Position.Left]: { x: 0, y: 0 },
|
||||
[Position.Right]: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
export const SHIP_SIZES_COLORS = {
|
||||
[ShipSizeStatus.small]: 'bg-indigo-400',
|
||||
[ShipSizeStatus.medium]: 'bg-cyan-500',
|
||||
[ShipSizeStatus.large]: '',
|
||||
[ShipSizeStatus.freight]: 'bg-lime-400',
|
||||
[ShipSizeStatus.capital]: 'bg-red-400',
|
||||
};
|
||||
|
||||
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
|
||||
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
|
||||
const isWormhole = data?.type !== ConnectionType.gate;
|
||||
|
||||
const {
|
||||
data: { isThickConnections },
|
||||
} = useMapState();
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
|
||||
|
||||
const method = isWormhole ? getBezierPath : getBezierPath;
|
||||
|
||||
const [edgePath, labelX, labelY] = method({
|
||||
sourceX: sx - offset.x,
|
||||
sourceY: sy - offset.y,
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx + offset.x,
|
||||
targetY: ty + offset.y,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
|
||||
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
|
||||
}, [sourceNode, targetNode]);
|
||||
|
||||
if (!sourceNode || !targetNode || !data) {
|
||||
return null;
|
||||
@@ -78,10 +44,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
<path
|
||||
id={`back_${id}`}
|
||||
className={clsx(classes.EdgePathBack, {
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
|
||||
[classes.TimeCrit]: data.time_status === TimeStatus.eol,
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.Gate]: !isWormhole,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -90,12 +54,10 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
<path
|
||||
id={`front_${id}`}
|
||||
className={clsx(classes.EdgePathFront, {
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
|
||||
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
|
||||
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.MassVerge]: data.mass_status === MassState.verge,
|
||||
[classes.MassHalf]: data.mass_status === MassState.half,
|
||||
[classes.Frigate]: data.ship_size_type === ShipSizeStatus.small,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -113,29 +75,21 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.Handle,
|
||||
{ [classes.Tick]: isThickConnections, [classes.Right]: Position.Right === sourcePos },
|
||||
'react-flow__handle absolute nodrag pointer-events-none',
|
||||
)}
|
||||
className={clsx(classes.Handle, 'react-flow__handle absolute nodrag pointer-events-none')}
|
||||
style={{ transform: `${MAP_TRANSLATES[sourcePos]} translate(${sx}px,${sy}px)` }}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.Handle,
|
||||
{ [classes.Tick]: isThickConnections },
|
||||
'react-flow__handle absolute nodrag pointer-events-none',
|
||||
)}
|
||||
className={clsx(classes.Handle, 'react-flow__handle absolute nodrag pointer-events-none')}
|
||||
style={{ transform: `${MAP_TRANSLATES[targetPos]} translate(${tx}px,${ty}px)` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute flex items-center gap-1 pointer-events-none"
|
||||
className="absolute flex items-center gap-1"
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
}}
|
||||
>
|
||||
{isWormhole && data.locked && (
|
||||
{data.locked && (
|
||||
<WdTooltipWrapper
|
||||
content="Save mass"
|
||||
className={clsx(
|
||||
@@ -146,19 +100,6 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
<span className={clsx(PrimeIcons.LOCK, classes.icon)} />
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
|
||||
<WdTooltipWrapper
|
||||
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}
|
||||
className={clsx(
|
||||
classes.LinkLabel,
|
||||
'pointer-events-auto rounded opacity-100 cursor-auto text-neutral-900 font-bold',
|
||||
SHIP_SIZES_COLORS[data.ship_size_type],
|
||||
)}
|
||||
>
|
||||
{SHIP_SIZES_NAMES_SHORT[data.ship_size_type]}
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: #d291bc;
|
||||
$pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020; // Темный фон для подсказок
|
||||
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
|
||||
background-color: $tooltip-bg;
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::before {
|
||||
background-image: url('/images/mataria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
background-image: url('/images/caldaria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::before {
|
||||
opacity: 0.45;
|
||||
background-image: url('/images/amarr-180.png');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
background-image: url('/images/gallente-180.png');
|
||||
background-position-x: 1px;
|
||||
background-position-y: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
background-color: $tooltip-bg;
|
||||
color: $text-color;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $pastel-pink;
|
||||
}
|
||||
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid darken($eve-solar-system-status-color-home, 30%);
|
||||
background-image: linear-gradient(275deg, $eve-solar-system-status-friendly, transparent);
|
||||
|
||||
&.selected {
|
||||
border-color: $eve-solar-system-status-color-home;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-friendly {
|
||||
border: 1px solid darken($eve-solar-system-status-color-friendly, 20%);
|
||||
background-image: linear-gradient(275deg, darken($eve-solar-system-status-friendly, 30%), transparent);
|
||||
|
||||
&.selected {
|
||||
border-color: darken($eve-solar-system-status-color-friendly, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-lookingFor {
|
||||
border: 1px solid darken($eve-solar-system-status-color-lookingFor, 15%);
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-warning {
|
||||
background-image: linear-gradient(275deg, $eve-solar-system-status-warning, transparent);
|
||||
}
|
||||
|
||||
&.eve-system-status-dangerous {
|
||||
background-image: linear-gradient(275deg, $eve-solar-system-status-dangerous, transparent);
|
||||
}
|
||||
|
||||
&.eve-system-status-target {
|
||||
background-image: linear-gradient(275deg, $eve-solar-system-status-target, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.Bookmarks {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
left: 4px;
|
||||
|
||||
& > .Bookmark {
|
||||
min-width: 13px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -13px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
//background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.BookmarkWithIcon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
|
||||
padding-right: 2px;
|
||||
|
||||
& > .icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-top: 1px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.HeadRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
.classTitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
|
||||
}
|
||||
|
||||
.TagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
|
||||
color: #ffb01d;
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
//font-weight: bold;
|
||||
}
|
||||
|
||||
.solarSystemName {
|
||||
}
|
||||
}
|
||||
|
||||
.BottomRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
.localCounter {
|
||||
display: flex;
|
||||
//align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 500;
|
||||
//margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.effect {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -2px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.statics {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
font-size: 8px;
|
||||
|
||||
& > * {
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
|
||||
&.HandleTop {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -2px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Handle, Position, WrapNodeProps } from 'reactflow';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import classes from './SolarSystemNode.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
LABELS_INFO,
|
||||
LABELS_ORDER,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts';
|
||||
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
|
||||
|
||||
const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Caldari]: classes.Caldaria,
|
||||
[Spaces.Matar]: classes.Mataria,
|
||||
[Spaces.Amarr]: classes.Amarria,
|
||||
[Spaces.Gallente]: classes.Gallente,
|
||||
};
|
||||
|
||||
const sortedLabels = (labels: string[]) => {
|
||||
if (!labels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
||||
};
|
||||
|
||||
export const getActivityType = (count: number) => {
|
||||
if (count <= 5) {
|
||||
return 'activityNormal';
|
||||
}
|
||||
|
||||
if (count <= 30) {
|
||||
return 'activityWarn';
|
||||
}
|
||||
|
||||
return 'activityDanger';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarSystemType>) => {
|
||||
const {
|
||||
system_class,
|
||||
security,
|
||||
class_title,
|
||||
solar_system_id,
|
||||
statics,
|
||||
effect_name,
|
||||
region_name,
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
} = data.system_static_info;
|
||||
|
||||
const { locked, name, tag, status, labels, id } = data || {};
|
||||
|
||||
const customName = solar_system_name !== name ? name : undefined;
|
||||
|
||||
const {
|
||||
data: {
|
||||
characters,
|
||||
presentCharacters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
userCharacters,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
},
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
||||
// eslint-disable-next-line
|
||||
}, [characters, presentCharacters, solar_system_id]);
|
||||
|
||||
const isWormhole = isWormholeSpace(system_class);
|
||||
const classTitleColor = useMemo(
|
||||
() => getSystemClassStyles({ systemClass: system_class, security }),
|
||||
[security, system_class],
|
||||
);
|
||||
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);
|
||||
const lebM = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
|
||||
const labelsInfo = useMemo(() => sortedLabels(lebM.list), [lebM]);
|
||||
const labelCustom = useMemo(() => lebM.customLabel, [lebM]);
|
||||
|
||||
const killsCount = useMemo(() => {
|
||||
const systemKills = kills[solar_system_id];
|
||||
if (!systemKills) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return systemKills;
|
||||
}, [kills, solar_system_id]);
|
||||
|
||||
const hasUserCharacters = useMemo(() => {
|
||||
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
|
||||
}, [charactersInSystem, userCharacters]);
|
||||
|
||||
const dbClick = useDoubleClick(() => {
|
||||
outCommand({
|
||||
type: OutCommand.openSettings,
|
||||
data: {
|
||||
system_id: solar_system_id.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const showHandlers = isConnecting || hoverNodeId === id;
|
||||
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{is_shattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[getActivityType(killsCount)])}>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(classes.RootCustomNode, regionClass, classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
>
|
||||
{visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
|
||||
{class_title ?? '-'}
|
||||
</div>
|
||||
{tag != null && tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classSystemName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
|
||||
)}
|
||||
>
|
||||
{solar_system_name}
|
||||
</div>
|
||||
|
||||
{isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{sortedStatics.map(x => (
|
||||
<WormholeClassComp key={x} id={x} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effect_name !== null && isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effect_name])}></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isWormhole && !customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
{region_name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isWormhole && !customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i>}
|
||||
|
||||
{hubs.includes(solar_system_id.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i>
|
||||
)}
|
||||
|
||||
{charactersInSystem.length > 0 && (
|
||||
<div className={clsx(classes.localCounter, { ['text-amber-300']: hasUserCharacters })}>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }}></i>
|
||||
<span className="font-sans">{charactersInSystem.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div onMouseDownCapture={dbClick} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, { [classes.selected]: selected })}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, { [classes.selected]: selected })}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, { [classes.selected]: selected })}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, { [classes.selected]: selected })}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,415 +0,0 @@
|
||||
@use "sass:color";
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: rgb(30, 161, 255);
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020;
|
||||
|
||||
$neon-color-1: rgb(27, 132, 236);
|
||||
$neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
|
||||
@keyframes move-stripes {
|
||||
from {
|
||||
background-position: 0 0;
|
||||
}
|
||||
to {
|
||||
background-position: 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
|
||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
|
||||
&.Pochven,
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
&.Caldaria {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::after {
|
||||
background-image: url('/images/mataria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::after {
|
||||
background-image: url('/images/caldaria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::after {
|
||||
opacity: 0.45;
|
||||
background-image: url('/images/amarr-180.png');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::after {
|
||||
opacity: 0.5;
|
||||
background-image: url('/images/gallente-180.png');
|
||||
background-position-x: 1px;
|
||||
background-position-y: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.Pochven {
|
||||
&::after {
|
||||
opacity: 0.8;
|
||||
background-image: url('/images/pochven.webp');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
&.rally {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
|
||||
border-color: $neon-color-1;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$neon-color-3 0px,
|
||||
$neon-color-3 8px,
|
||||
transparent 8px,
|
||||
transparent 21px
|
||||
);
|
||||
background-size: 30px 30px;
|
||||
animation: move-stripes 3s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-friendly {
|
||||
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
|
||||
background-image: linear-gradient(275deg, var(--eve-solar-system-status-friendly-dark30), transparent);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-friendly-dark5);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-lookingFor {
|
||||
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-warning {
|
||||
background-image: linear-gradient(275deg, var(--eve-solar-system-status-warning), transparent);
|
||||
}
|
||||
|
||||
&.eve-system-status-dangerous {
|
||||
background-image: linear-gradient(275deg, var(--eve-solar-system-status-dangerous), transparent);
|
||||
}
|
||||
|
||||
&.eve-system-status-target {
|
||||
background-image: linear-gradient(275deg, var(--eve-solar-system-status-target), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.Bookmarks {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
left: 4px;
|
||||
|
||||
& > .Bookmark {
|
||||
min-width: 13px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -13px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.BookmarkWithIcon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
|
||||
padding-right: 2px;
|
||||
|
||||
& > .icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-top: 1px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Unsplashed {
|
||||
position: absolute;
|
||||
width: calc(50% - 4px);
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
left: 2px;
|
||||
|
||||
&--right {
|
||||
left: calc(50% + 6px);
|
||||
}
|
||||
|
||||
& > .Signature {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
display: block;
|
||||
|
||||
background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.HeadRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
.classTitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
|
||||
}
|
||||
|
||||
.TagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
color: var(--rf-tag-color, #38bdf8);
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.BottomRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
.hasLocalCounter {
|
||||
margin-right: 2px;
|
||||
&.countAbove9 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.lockIcon {
|
||||
font-size: 0.45rem;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mapMarker {
|
||||
font-size: 0.45rem;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.effect {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -2px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.statics {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
font-size: 8px;
|
||||
|
||||
& > * {
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
pointer-events: auto;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
|
||||
&.HandleTop {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -2px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -2px;
|
||||
}
|
||||
|
||||
&.Tick {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
||||
&.HandleTop {
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -3px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ShatteredIcon {
|
||||
position: relative;
|
||||
//top: -1px;
|
||||
left: -1px;
|
||||
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
background-image: url(/images/chart-network-svgrepo-com.svg)
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
|
||||
|
||||
// let render = 0;
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
|
||||
nodeVars.solarSystemId,
|
||||
);
|
||||
|
||||
// console.log('JOipP', `render ${nodeVars.id}`, render++);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodeVars.visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
|
||||
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
|
||||
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={localKillsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{localKillsCount}</span>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.rally]: nodeVars.isRally,
|
||||
},
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<Tag
|
||||
value={nodeVars.tag}
|
||||
severity="warning"
|
||||
className="py-0 px-[2px] text-[9px] [&_.p-tag-value]:leading-[1.3]"
|
||||
></Tag>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classSystemName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
|
||||
)}
|
||||
>
|
||||
{nodeVars.systemName}
|
||||
</div>
|
||||
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SolarSystemNodeDefault.displayName = 'SolarSystemNodeDefault';
|
||||
@@ -1,20 +0,0 @@
|
||||
@use './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
/* ---------------------------------------------
|
||||
Only override what's different from the base
|
||||
Currently none required
|
||||
---------------------------------------------- */
|
||||
|
||||
.RootCustomNode {
|
||||
&.eve-system-status-home {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
|
||||
|
||||
// let render = 0;
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
|
||||
nodeVars.solarSystemId,
|
||||
);
|
||||
|
||||
// console.log('JOipP', `render ${nodeVars.id}`, render++);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodeVars.visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
|
||||
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
|
||||
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={localKillsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
|
||||
>
|
||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{localKillsCount}</span>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.rally]: nodeVars.isRally,
|
||||
},
|
||||
)}
|
||||
onMouseDownCapture={e => nodeVars.dbClick(e)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classSystemName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
{nodeVars.systemName}
|
||||
</div>
|
||||
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{nodeVars.regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<div className={clsx('flex items-center gap-1')}>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SolarSystemNodeTheme.displayName = 'SolarSystemNodeTheme';
|
||||
@@ -1,2 +1 @@
|
||||
export * from './SolarSystemNodeDefault';
|
||||
export * from './SolarSystemNodeTheme';
|
||||
export * from './SolarSystemNode';
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.Signature {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: block;
|
||||
|
||||
& > .Box {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .Eol {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
}
|
||||
export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) => {
|
||||
const {
|
||||
data: { wormholesData },
|
||||
} = useMapRootState();
|
||||
|
||||
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
|
||||
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
|
||||
|
||||
const customInfo = useMemo(() => {
|
||||
return parseSignatureCustomInfo(signature.custom_info);
|
||||
}, [signature]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!customInfo?.k162Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return K162_TYPES_MAP[customInfo.k162Type];
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.isEOL;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
if (signature.type === 'K162' && k162TypeOption) {
|
||||
const k162Data = wormholesData[k162TypeOption.whClassName];
|
||||
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
|
||||
return k162Class ? WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID] : '';
|
||||
}
|
||||
return whClass ? WORMHOLE_CLASS_STYLES[whClass.wormholeClassID] : '';
|
||||
}, [signature, whClass, k162TypeOption, wormholesData]);
|
||||
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
className={clsx(classes.Signature)}
|
||||
// @ts-ignore
|
||||
content={
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
|
||||
</svg>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './UnsplashedSignature.tsx';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConnectionType, MassState, ShipSizeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState } from '@/hooks/Mapper/types';
|
||||
|
||||
export enum SOLAR_SYSTEM_CLASS_IDS {
|
||||
ccp1 = -1,
|
||||
@@ -16,7 +16,7 @@ export enum SOLAR_SYSTEM_CLASS_IDS {
|
||||
thera = 12,
|
||||
c13 = 13,
|
||||
sentinel = 14,
|
||||
barbican = 15,
|
||||
baribican = 15,
|
||||
vidette = 16,
|
||||
conflux = 17,
|
||||
redoubt = 18,
|
||||
@@ -82,7 +82,7 @@ export const SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS = {
|
||||
thera: SOLAR_SYSTEM_CLASS_GROUPS.thera,
|
||||
c13: SOLAR_SYSTEM_CLASS_GROUPS.c13,
|
||||
sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
barbican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
baribican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
|
||||
@@ -217,7 +217,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
|
||||
wormholeClassID: 14,
|
||||
effectPower: 2,
|
||||
title: 'Class 14 (Sentinel Drifter)',
|
||||
shortTitle: 'Sentinel MZ',
|
||||
shortTitle: 'Sentinel',
|
||||
},
|
||||
{
|
||||
id: 'barbican',
|
||||
@@ -225,7 +225,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
|
||||
wormholeClassID: 15,
|
||||
effectPower: 2,
|
||||
title: 'Class 15 (Barbican Drifter)',
|
||||
shortTitle: 'Liberated Barbican',
|
||||
shortTitle: 'Barbican',
|
||||
},
|
||||
{
|
||||
id: 'vidette',
|
||||
@@ -233,7 +233,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
|
||||
wormholeClassID: 16,
|
||||
effectPower: 2,
|
||||
title: 'Class 16 (Vidette Drifter)',
|
||||
shortTitle: 'Sanctified Vidette',
|
||||
shortTitle: 'Vidette',
|
||||
},
|
||||
{
|
||||
id: 'conflux',
|
||||
@@ -241,7 +241,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
|
||||
wormholeClassID: 17,
|
||||
effectPower: 2,
|
||||
title: 'Class 17 (Conflux Drifter)',
|
||||
shortTitle: 'Conflux Eyrie',
|
||||
shortTitle: 'Conflux',
|
||||
},
|
||||
{
|
||||
id: 'redoubt',
|
||||
@@ -249,7 +249,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
|
||||
wormholeClassID: 18,
|
||||
effectPower: 2,
|
||||
title: 'Class 18 (Redoubt Drifter)',
|
||||
shortTitle: 'Azdaja Redoubt',
|
||||
shortTitle: 'Redoubt',
|
||||
},
|
||||
{
|
||||
id: 'a1',
|
||||
@@ -322,9 +322,6 @@ export const WORMHOLES_ADDITIONAL_INFO: Record<string, WormholesAdditionalInfoTy
|
||||
export const WORMHOLES_ADDITIONAL_INFO_BY_CLASS_ID: Record<string, WormholesAdditionalInfoType> =
|
||||
WORMHOLES_ADDITIONAL_INFO_RAW.reduce((acc, x) => ({ ...acc, [x.wormholeClassID]: x }), {});
|
||||
|
||||
export const WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME: Record<string, WormholesAdditionalInfoType> =
|
||||
WORMHOLES_ADDITIONAL_INFO_RAW.reduce((acc, x) => ({ ...acc, [x.shortName]: x }), {});
|
||||
|
||||
// export const SOLAR_SYSTEM_CLASS_NAMES = {
|
||||
// ccp1 = ,
|
||||
// c1 = ,
|
||||
@@ -653,7 +650,6 @@ export enum LABELS {
|
||||
l3 = '3',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const LABELS_INFO: Record<string, any> = {
|
||||
[LABELS.clear]: { id: 'clear', name: 'Clear', shortName: '', icon: '' },
|
||||
[LABELS.la]: { id: 'la', name: 'Label A', shortName: 'A', icon: '' },
|
||||
@@ -716,13 +712,6 @@ export const STATUS_CLASSES: Record<number, string> = {
|
||||
[STATUSES.dangerous]: 'eve-system-status-dangerous',
|
||||
};
|
||||
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
|
||||
|
||||
export const TYPE_NAMES = {
|
||||
[ConnectionType.wormhole]: 'Wormhole',
|
||||
[ConnectionType.gate]: 'Gate',
|
||||
};
|
||||
|
||||
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];
|
||||
|
||||
export const MASS_STATE_NAMES = {
|
||||
@@ -731,52 +720,16 @@ export const MASS_STATE_NAMES = {
|
||||
[MassState.verge]: 'Verge of collapse',
|
||||
};
|
||||
|
||||
export const SHIP_SIZES_NAMES_ORDER = [
|
||||
ShipSizeStatus.small,
|
||||
ShipSizeStatus.medium,
|
||||
ShipSizeStatus.large,
|
||||
ShipSizeStatus.freight,
|
||||
ShipSizeStatus.capital,
|
||||
];
|
||||
|
||||
export const SHIP_SIZES_NAMES = {
|
||||
[ShipSizeStatus.small]: 'Frigate',
|
||||
[ShipSizeStatus.medium]: 'Medium',
|
||||
[ShipSizeStatus.large]: 'Normal',
|
||||
[ShipSizeStatus.freight]: 'Huge',
|
||||
[ShipSizeStatus.capital]: 'Capital',
|
||||
};
|
||||
export const SHIP_SIZES_SIZE = {
|
||||
[ShipSizeStatus.small]: '5K',
|
||||
[ShipSizeStatus.medium]: '62K',
|
||||
[ShipSizeStatus.large]: '375K',
|
||||
[ShipSizeStatus.freight]: '1M',
|
||||
[ShipSizeStatus.capital]: '2M',
|
||||
};
|
||||
|
||||
export const SHIP_MASSES_SIZE: Record<number, ShipSizeStatus> = {
|
||||
5_000_000: ShipSizeStatus.small,
|
||||
62_000_000: ShipSizeStatus.medium,
|
||||
300_000_000: ShipSizeStatus.large,
|
||||
375_000_000: ShipSizeStatus.large,
|
||||
1_000_000_000: ShipSizeStatus.freight,
|
||||
1_350_000_000: ShipSizeStatus.capital,
|
||||
1_800_000_000: ShipSizeStatus.capital,
|
||||
2_000_000_000: ShipSizeStatus.capital,
|
||||
};
|
||||
|
||||
export const SHIP_SIZES_DESCRIPTION = {
|
||||
[ShipSizeStatus.small]: 'Frigate wormhole - up to Destroyer | 5K t.',
|
||||
[ShipSizeStatus.medium]: 'Cruise wormhole - up to Battlecruiser | 62K t.',
|
||||
[ShipSizeStatus.large]: 'Large wormhole - up to Battleship | 375K t.',
|
||||
[ShipSizeStatus.freight]: 'Huge wormhole - up to Freighter | 1M t.',
|
||||
[ShipSizeStatus.capital]: 'Capital wormhole - up to Capital | 2M t.',
|
||||
};
|
||||
|
||||
export const SHIP_SIZES_NAMES_SHORT = {
|
||||
[ShipSizeStatus.small]: 'S',
|
||||
[ShipSizeStatus.medium]: 'M',
|
||||
[ShipSizeStatus.large]: 'L',
|
||||
[ShipSizeStatus.freight]: 'H',
|
||||
[ShipSizeStatus.capital]: 'XL',
|
||||
};
|
||||
// export const SHIP_SIZES_NAMES_ORDER = [
|
||||
// ShipSizeStatus.small,
|
||||
// ShipSizeStatus.normal,
|
||||
// // ShipSizeStatus.large,
|
||||
// // ShipSizeStatus.capital,
|
||||
// ];
|
||||
//
|
||||
// export const SHIP_SIZES_NAMES = {
|
||||
// [ShipSizeStatus.small]: 'Frigate',
|
||||
// [ShipSizeStatus.normal]: 'Normal',
|
||||
// // [ShipSizeStatus.large]: 'Normal',
|
||||
// // [ShipSizeStatus.capital]: 'Normal',
|
||||
// };
|
||||
|
||||
@@ -10,6 +10,5 @@ export const convertSystem2Node = (sys: SolarSystemRawType): Node => {
|
||||
position: sys.position,
|
||||
data: sys,
|
||||
draggable: !sys.locked,
|
||||
deletable: !sys.locked,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { SolarSystemNodeDefault, SolarSystemNodeTheme } from '../components/SolarSystemNode';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
import type { ComponentType } from 'react';
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { ConnectionMode } from 'reactflow';
|
||||
|
||||
export type SolarSystemNodeComponent = ComponentType<NodeProps<MapSolarSystemType>>;
|
||||
|
||||
interface ThemeBehavior {
|
||||
isPanAndDrag: boolean;
|
||||
nodeComponent: SolarSystemNodeComponent;
|
||||
connectionMode: ConnectionMode;
|
||||
}
|
||||
|
||||
const THEME_BEHAVIORS: {
|
||||
[key: string]: ThemeBehavior;
|
||||
} = {
|
||||
default: {
|
||||
isPanAndDrag: false,
|
||||
nodeComponent: SolarSystemNodeDefault,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
pathfinder: {
|
||||
isPanAndDrag: true,
|
||||
nodeComponent: SolarSystemNodeTheme,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
};
|
||||
|
||||
export function getBehaviorForTheme(themeName: string) {
|
||||
return THEME_BEHAVIORS[themeName] ?? THEME_BEHAVIORS.default;
|
||||
}
|
||||
@@ -3,4 +3,3 @@ export * from './convertSystem2Node';
|
||||
export * from './getSystemClassStyles';
|
||||
export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const isWormholeSpace = (wormholeClassID: number) => {
|
||||
case SOLAR_SYSTEM_CLASS_IDS.c6:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.c13:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.thera:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.barbican:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.baribican:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.vidette:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.conflux:
|
||||
case SOLAR_SYSTEM_CLASS_IDS.redoubt:
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Helper function to split an array into chunks of size
|
||||
const chunkArray = (array: any[], size: number) => {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
|
||||
export const prepareUnsplashedChunks = (items: any[]) => {
|
||||
// Split the items into chunks of 4
|
||||
const chunks = chunkArray(items, 4);
|
||||
|
||||
// Get the column elements
|
||||
const leftColumn: any[] = [];
|
||||
const rightColumn: any[] = [];
|
||||
|
||||
chunks.forEach((chunk, index) => {
|
||||
const column = index % 2 === 0 ? leftColumn : rightColumn;
|
||||
|
||||
chunk.forEach(item => {
|
||||
column.push(item);
|
||||
});
|
||||
});
|
||||
|
||||
return [leftColumn, rightColumn];
|
||||
};
|
||||
@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCenterSystem';
|
||||
export * from './useSelectSystems';
|
||||
export * from './useSelectSystem';
|
||||
export * from './useMapCommands';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandCharacterAdded,
|
||||
CommandCharacterRemoved,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
CommandCharacterUpdated,
|
||||
CommandPresentCharacters,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useCommandsCharacters = () => {
|
||||
const { update } = useMapState();
|
||||
|
||||
@@ -2,24 +2,17 @@ import { Node, useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandAddSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { convertSystem2Node } from '../../helpers';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
export const useMapAddSystems = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
|
||||
return useCallback((systems: CommandAddSystems) => {
|
||||
const { rf } = ref.current;
|
||||
const nodes = rf.getNodes();
|
||||
|
||||
const newSystems = systems.filter(x => !nodes.some(y => x.id === y.id));
|
||||
newSystems.forEach(x => addSystemStatic(x.system_static_info));
|
||||
|
||||
const prepared: Node[] = newSystems.map(convertSystem2Node);
|
||||
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
|
||||
rf.addNodes(prepared);
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
|
||||
|
||||
export const useMapCommands = () => {
|
||||
const { update } = useMapState();
|
||||
@@ -8,21 +8,13 @@ export const useMapCommands = () => {
|
||||
const ref = useRef({ update });
|
||||
ref.current = { update };
|
||||
|
||||
const mapUpdated = useCallback(({ hubs, system_signatures, kills }: CommandMapUpdated) => {
|
||||
const mapUpdated = useCallback(({ hubs }: CommandMapUpdated) => {
|
||||
const out: Partial<MapData> = {};
|
||||
|
||||
if (hubs) {
|
||||
out.hubs = hubs;
|
||||
}
|
||||
|
||||
if (system_signatures) {
|
||||
out.systemSignatures = system_signatures;
|
||||
}
|
||||
|
||||
if (kills) {
|
||||
out.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
|
||||
}
|
||||
|
||||
ref.current.update(out);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const rf = useReactFlow();
|
||||
@@ -14,7 +14,6 @@ export const useMapInit = () => {
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
system_signatures,
|
||||
kills,
|
||||
connections,
|
||||
wormholes,
|
||||
@@ -52,10 +51,6 @@ export const useMapInit = () => {
|
||||
updateData.systems = systems;
|
||||
}
|
||||
|
||||
if (system_signatures) {
|
||||
updateData.systemSignatures = system_signatures;
|
||||
}
|
||||
|
||||
if (kills) {
|
||||
updateData.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
|
||||
}
|
||||
|
||||
@@ -5,21 +5,24 @@ import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts
|
||||
|
||||
export const useMapRemoveSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
const ref = useRef({ onSelectionChange, rf });
|
||||
ref.current = { onSelectionChange, rf };
|
||||
const ref = useRef({ onSelectionChange });
|
||||
ref.current = { onSelectionChange };
|
||||
|
||||
return useCallback((systems: CommandRemoveSystems) => {
|
||||
ref.current.rf.deleteElements({ nodes: systems.map(x => ({ id: `${x}` })) });
|
||||
return useCallback(
|
||||
(systems: CommandRemoveSystems) => {
|
||||
rf.deleteElements({ nodes: systems.map(x => ({ id: `${x}` })) });
|
||||
|
||||
const newSelection = ref.current.rf
|
||||
.getNodes()
|
||||
.filter(x => !systems.includes(parseInt(x.id)))
|
||||
.filter(x => x.selected)
|
||||
.map(x => x.id);
|
||||
const newSelection = rf
|
||||
.getNodes()
|
||||
.filter(x => !systems.includes(parseInt(x.id)))
|
||||
.filter(x => x.selected)
|
||||
.map(x => x.id);
|
||||
|
||||
ref.current.onSelectionChange({
|
||||
systems: newSelection,
|
||||
connections: [],
|
||||
});
|
||||
}, []);
|
||||
ref.current.onSelectionChange({
|
||||
systems: newSelection,
|
||||
connections: [],
|
||||
});
|
||||
},
|
||||
[rf],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const useMapUpdateSystems = () => {
|
||||
return newSystem;
|
||||
});
|
||||
|
||||
update({ systems: out }, true);
|
||||
update({ systems: out });
|
||||
},
|
||||
[rf, update],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystem } from '@/hooks/Mapper/types';
|
||||
|
||||
export const useSelectSystem = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
|
||||
return useCallback((systemId: CommandSelectSystem) => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: node.id === systemId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf, onSelectionChange });
|
||||
ref.current = { rf, onSelectionChange };
|
||||
|
||||
return useCallback(({ systems, delay }: CommandSelectSystems) => {
|
||||
const run = () => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: systems.includes(node.id),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (delay == null || delay === 0) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(run, delay);
|
||||
}, []);
|
||||
};
|
||||
@@ -1,11 +1,2 @@
|
||||
export * from './useMapHandlers';
|
||||
export * from './useUpdateNodes';
|
||||
export * from './useNodesEdgesState';
|
||||
export * from './useBackgroundVars';
|
||||
export * from './useKillsCounter';
|
||||
export * from './useSystemName';
|
||||
export * from './useNodesEdgesState';
|
||||
export * from './useSolarSystemNode';
|
||||
export * from './useUnsplashedSignatures';
|
||||
export * from './useUpdateNodes';
|
||||
export * from './useNodeKillsCount';
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BackgroundVariant } from 'reactflow';
|
||||
|
||||
export function useBackgroundVars(themeName?: string) {
|
||||
const [variant, setVariant] = useState<BackgroundVariant>(BackgroundVariant.Dots);
|
||||
const [gap, setGap] = useState<number>(16);
|
||||
const [size, setSize] = useState<number>(1);
|
||||
const [color, setColor] = useState('#81818b');
|
||||
const [snapSize, setSnapSize] = useState<number>(25);
|
||||
|
||||
useEffect(() => {
|
||||
// match any element whose entire `class` attribute ends with "-theme"
|
||||
let themeEl = document.querySelector('[class$="-theme"]');
|
||||
|
||||
// If none is found, fall back to the <html> element
|
||||
if (!themeEl) {
|
||||
themeEl = document.documentElement;
|
||||
}
|
||||
|
||||
const style = getComputedStyle(themeEl as HTMLElement);
|
||||
|
||||
const rawVariant = style.getPropertyValue('--rf-bg-variant').replace(/['"]/g, '').trim().toLowerCase();
|
||||
let finalVariant: BackgroundVariant = BackgroundVariant.Dots;
|
||||
|
||||
if (rawVariant === 'lines') {
|
||||
finalVariant = BackgroundVariant.Lines;
|
||||
} else if (rawVariant === 'cross') {
|
||||
finalVariant = BackgroundVariant.Cross;
|
||||
}
|
||||
|
||||
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
|
||||
const cssVarSize = style.getPropertyValue('--rf-bg-size');
|
||||
const cssVarSnapSize = style.getPropertyValue('--rf-snap-size');
|
||||
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
|
||||
|
||||
const gapNum = parseInt(cssVarGap, 10) || 16;
|
||||
const sizeNum = parseInt(cssVarSize, 10) || 1;
|
||||
const snapSize = parseInt(cssVarSnapSize, 10) || 25; //react-flow default
|
||||
|
||||
setVariant(finalVariant);
|
||||
setGap(gapNum);
|
||||
setSize(sizeNum);
|
||||
setColor(cssColor);
|
||||
setSnapSize(snapSize);
|
||||
}, [themeName]);
|
||||
|
||||
return { variant, gap, size, color, snapSize };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user