Compare commits

..

1 Commits

Author SHA1 Message Date
achichenkov
ef26d7129a fix(Map): Fix icons of main, follow and shattered 2025-04-13 17:08:15 +03:00
602 changed files with 17665 additions and 61881 deletions

View File

@@ -13,8 +13,8 @@
## list of tools (see `mix check` docs for a list of default curated tools) ## list of tools (see `mix check` docs for a list of default curated tools)
tools: [ tools: [
## Allow compilation warnings for now (error budget: unlimited warnings) ## curated tools may be disabled (e.g. the check for compilation warnings)
{:compiler, "mix compile"}, {:compiler, false},
## ...or have command & args adjusted (e.g. enable skip comments for sobelow) ## ...or have command & args adjusted (e.g. enable skip comments for sobelow)
# {:sobelow, "mix sobelow --exit --skip"}, # {:sobelow, "mix sobelow --exit --skip"},
@@ -22,15 +22,10 @@
## ...or reordered (e.g. to see output from dialyzer before others) ## ...or reordered (e.g. to see output from dialyzer before others)
# {:dialyzer, order: -1}, # {:dialyzer, order: -1},
## Credo with relaxed error budget: max 200 issues ## ...or reconfigured (e.g. disable parallel execution of ex_unit in umbrella)
{:credo, "mix credo --strict --max-issues 200"},
## 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}, {:doctor, false},
{:ex_unit, false},
{:npm_test, false}, {:npm_test, false},
{:sobelow, false} {:sobelow, false}

View File

@@ -82,6 +82,8 @@
# You can customize the priority of any check # You can customize the priority of any check
# Priority values are: `low, normal, high, higher` # 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. # You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just # If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero). # set this value to 0 (zero).
@@ -97,9 +99,10 @@
{Credo.Check.Readability.LargeNumbers, []}, {Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []}, {Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, false}, {Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []}, {Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []}, {Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, {Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []}, {Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []}, {Credo.Check.Readability.PreferImplicitTry, []},
@@ -118,12 +121,14 @@
# #
{Credo.Check.Refactor.Apply, []}, {Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []}, {Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FunctionArity, []}, {Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []}, {Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.MapJoin, []}, {Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.UnlessWithElse, []}, {Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []}, {Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.FilterFilter, []},
@@ -191,19 +196,10 @@
{Credo.Check.Warning.LeakyEnvironment, []}, {Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []}, {Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []}, {Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}, {Credo.Check.Warning.UnsafeToAtom, []}
# {Credo.Check.Refactor.MapInto, []}, # {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`. # Custom checks can be created using `mix credo.gen.check`.
# #

View File

@@ -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, []}
]
}
]
}

View File

@@ -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"

View File

@@ -8,9 +8,4 @@ export GIT_SHA="1111"
export WANDERER_INVITES="false" export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false" export WANDERER_PUBLIC_API_DISABLED="false"
export WANDERER_CHARACTER_API_DISABLED="false" export WANDERER_CHARACTER_API_DISABLED="false"
export WANDERER_KILLS_SERVICE_ENABLED="true" export WANDERER_ZKILL_PRELOAD_DISABLED="false"
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"

View File

@@ -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

View File

@@ -4,8 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- develop - "releases/*"
env: env:
MIX_ENV: prod MIX_ENV: prod
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
@@ -19,10 +18,51 @@ permissions:
contents: write contents: write
jobs: 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 }}
manual-approval:
name: Manual Approval
runs-on: ubuntu-latest
needs: deploy-test
if: success()
permissions:
issues: write
steps:
- name: Await Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: DmitryPopov
minimum-approvals: 1
issue-title: "Manual Approval Required for Release"
issue-body: "Please approve or deny the deployment."
build: build:
name: 🛠 Build name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04 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: permissions:
checks: write checks: write
contents: write contents: write
@@ -37,7 +77,7 @@ jobs:
elixir: ["1.17"] elixir: ["1.17"]
node-version: ["18.x"] node-version: ["18.x"]
outputs: 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: steps:
- name: Prepare - name: Prepare
run: | run: |
@@ -53,7 +93,6 @@ jobs:
- name: ⬇️ Checkout repo - name: ⬇️ Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ssh-key: "${{ secrets.COMMIT_KEY }}"
fetch-depth: 0 fetch-depth: 0
- name: 😅 Cache deps - name: 😅 Cache deps
id: cache-deps id: cache-deps
@@ -91,26 +130,20 @@ jobs:
- name: Generate Changelog & Update Tag Version - name: Generate Changelog & Update Tag Version
id: generate-changelog id: generate-changelog
if: github.ref == 'refs/heads/main'
run: | run: |
git config --global user.name 'CI' git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com' git config --global user.email 'ci@users.noreply.github.com'
mix git_ops.release --force-patch --yes mix git_ops.release --force-patch --yes
git commit --allow-empty -m 'chore: [skip ci]'
git push --follow-tags git push --follow-tags
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 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: docker:
name: 🛠 Build Docker Images name: 🛠 Build Docker Images
if: github.ref == 'refs/heads/develop'
needs: build needs: build
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions: permissions:
checks: write checks: write
contents: write contents: write
@@ -137,6 +170,17 @@ jobs:
ref: ${{ needs.build.outputs.commit_hash }} ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0 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 - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -185,6 +229,24 @@ jobs:
if-no-files-found: error if-no-files-found: error
retention-days: 1 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: merge:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
@@ -215,8 +277,9 @@ jobs:
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }} type=semver,pattern={{version}}
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
- name: Create manifest list and push - name: Create manifest list and push
working-directory: /tmp/digests working-directory: /tmp/digests
@@ -231,25 +294,19 @@ jobs:
create-release: create-release:
name: 🏷 Create Release name: 🏷 Create Release
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [docker, merge]
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
needs: build
steps: steps:
- name: ⬇️ Checkout repo - name: ⬇️ Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 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: 🏷 Create Draft Release - name: 🏷 Create Draft Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ steps.get-latest-tag.outputs.tag }} tag_name: ${{ needs.docker.outputs.release-tag }}
name: Release ${{ steps.get-latest-tag.outputs.tag }} name: Release ${{ needs.docker.outputs.release-tag }}
body: | body: |
## Info ## Info
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}). Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
@@ -259,3 +316,9 @@ jobs:
## How to Promote? ## How to Promote?
In order to promote this to prod, edit the draft and press **"Publish release"**. In order to promote this to prod, edit the draft and press **"Publish release"**.
draft: true draft: true
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

View File

@@ -1,189 +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 🎉
[wandererltd/community-edition-arm:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition-arm/tags)
**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 }}

View File

@@ -1,189 +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 🎉
[wandererltd/community-edition:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition/tags)
**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 }}

View File

@@ -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

View File

@@ -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.

6
.gitignore vendored
View File

@@ -4,8 +4,7 @@
*.iml *.iml
*.key *.key
.repomixignore
repomix*
/.idea/ /.idea/
/node_modules/ /node_modules/
/assets/node_modules/ /assets/node_modules/
@@ -18,9 +17,6 @@ repomix*
/priv/static/*.js /priv/static/*.js
/priv/static/*.css /priv/static/*.css
# Dialyzer PLT files
/priv/plts/
.DS_Store .DS_Store
**/.DS_Store **/.DS_Store

File diff suppressed because it is too large Load Diff

View File

@@ -21,17 +21,21 @@ RUN mkdir config
# to ensure any relevant config change will trigger the dependencies # to ensure any relevant config change will trigger the dependencies
# to be re-compiled. # to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/ COPY config/config.exs config/${MIX_ENV}.exs config/
COPY priv priv COPY priv priv
COPY lib lib COPY lib lib
COPY assets assets COPY assets assets
RUN mix assets.deploy
RUN mix compile RUN mix compile
RUN mix assets.deploy
# Changes to config/runtime.exs don't require recompiling the code # Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/ COPY config/runtime.exs config/
COPY rel rel
COPY rel rel
RUN mix release RUN mix release
# start a new build stage so that the final image will only contain # start a new build stage so that the final image will only contain

View File

@@ -17,6 +17,5 @@ module.exports = {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
}, },
}; };

View File

@@ -7,5 +7,5 @@
"semi": true, "semi": true,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"endOfLine": "auto" "endOfLine": "lf"
} }

View File

@@ -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
},
};

View File

@@ -1,14 +1,14 @@
import { PrimeReactProvider } from 'primereact/api';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { PrimeReactProvider } from 'primereact/api';
import { ReactFlowProvider } from 'reactflow';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts'; import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ErrorInfo, useCallback, useEffect, useRef } from 'react'; import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { useMapperHandlers } from './useMapperHandlers'; 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 './common-styles/main.scss';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
const ErrorFallback = () => { const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>; return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -20,7 +20,7 @@ export default function MapRoot({ hooks }) {
const mapperHandlerRefs = useRef([providerRef]); const mapperHandlerRefs = useRef([providerRef]);
const { handleCommand, handleMapEvent } = useMapperHandlers(mapperHandlerRefs.current, hooksRef); const { handleCommand, handleMapEvent, handleMapEvents } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
const logError = useCallback((error: Error, info: ErrorInfo) => { const logError = useCallback((error: Error, info: ErrorInfo) => {
if (!hooksRef.current) { if (!hooksRef.current) {
@@ -35,6 +35,7 @@ export default function MapRoot({ hooks }) {
} }
hooksRef.current.handleEvent('map_event', handleMapEvent); hooksRef.current.handleEvent('map_event', handleMapEvent);
hooksRef.current.handleEvent('map_events', handleMapEvents);
}, []); }, []);
return ( return (

View File

@@ -99,11 +99,6 @@
.p-dropdown-item { .p-dropdown-item {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
font-size: 14px; font-size: 14px;
width: 100%;
.p-dropdown-item-label {
width: 100%;
}
} }
.p-dropdown-item-group { .p-dropdown-item-group {
@@ -185,102 +180,3 @@
.p-datatable .p-datatable-tbody > tr.p-highlight { .p-datatable .p-datatable-tbody > tr.p-highlight {
background: initial; 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;
}

View File

@@ -64,9 +64,9 @@ body .p-dialog {
} }
.p-dialog-footer { .p-dialog-footer {
padding: .75rem 1rem; padding: 1rem;
border-top: none !important; border-top: 1px solid #ddd;
//background: #f4f4f4; background: #f4f4f4;
} }
.p-dialog-header-close { .p-dialog-header-close {

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container { .vertical-tabs-container {
display: flex; display: flex;
width: 100%; width: 100%;
min-height: 400px; min-height: 300px;
.p-tabview { .p-tabview {
width: 100%; width: 100%;
@@ -68,28 +68,6 @@
} }
} }
&.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;
}
}
}
} }
} }

View File

@@ -1,13 +1,14 @@
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 { useCallback } from 'react';
import clsx from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './Characters.module.scss'; import classes from './Characters.module.scss';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { PrimeIcons } from 'primereact/api';
interface CharactersProps { interface CharactersProps {
data: CharacterTypeRaw[]; data: CharacterTypeRaw[];
} }
@@ -16,22 +17,13 @@ export const Characters = ({ data }: CharactersProps) => {
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
const { const {
outCommand,
data: { mainCharacterEveId, followingCharacterEveId }, data: { mainCharacterEveId, followingCharacterEveId },
} = useMapRootState(); } = useMapRootState();
const handleSelect = useCallback(async (character: CharacterTypeRaw) => { const handleSelect = useCallback((character: CharacterTypeRaw) => {
if (!character) {
return;
}
await outCommand({
type: OutCommand.startTracking,
data: { character_eve_id: character.eve_id },
});
emitMapEvent({ emitMapEvent({
name: Commands.centerSystem, name: Commands.centerSystem,
data: character.location?.solar_system_id?.toString(), data: character?.location?.solar_system_id?.toString(),
}); });
}, []); }, []);
@@ -45,26 +37,14 @@ export const Characters = ({ data }: CharactersProps) => {
className={clsx( className={clsx(
'overflow-hidden relative', 'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer', 'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250 hover:bg-stone-300/90', 'transition-colors duration-250',
{ {
['border-stone-800/90']: !character.online, ['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online, ['border-lime-600/70']: character.online,
}, },
)} )}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name} title={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 && ( {mainCharacterEveId === character.eve_id && (
<span <span
className={clsx( className={clsx(
@@ -75,7 +55,6 @@ export const Characters = ({ data }: CharactersProps) => {
)} )}
/> />
)} )}
{followingCharacterEveId === character.eve_id && ( {followingCharacterEveId === character.eve_id && (
<span <span
className={clsx( className={clsx(

View File

@@ -1,12 +1,11 @@
import React, { RefObject } from 'react'; import React, { RefObject } from 'react';
import { ContextMenu } from 'primereact/contextmenu'; 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 { useContextMenuSystemItems } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
export interface ContextMenuSystemProps { export interface ContextMenuSystemProps {
hubs: string[]; hubs: string[];
userHubs: string[];
contextMenuRef: RefObject<ContextMenu>; contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined; systemId: string | undefined;
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
@@ -14,12 +13,10 @@ export interface ContextMenuSystemProps {
onLockToggle(): void; onLockToggle(): void;
onOpenSettings(): void; onOpenSettings(): void;
onHubToggle(): void; onHubToggle(): void;
onUserHubToggle(): void;
onSystemTag(val?: string): void; onSystemTag(val?: string): void;
onSystemStatus(val: number): void; onSystemStatus(val: number): void;
onSystemLabels(val: string): void; onSystemLabels(val: string): void;
onCustomLabelDialog(): void; onCustomLabelDialog(): void;
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
onWaypointSet: WaypointSetContextHandler; onWaypointSet: WaypointSetContextHandler;
} }
@@ -28,7 +25,7 @@ export const ContextMenuSystem: React.FC<ContextMenuSystemProps> = ({ contextMen
return ( return (
<> <>
<ContextMenu className="min-w-[200px]" model={items} ref={contextMenuRef} breakpoint="767px" /> <ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
</> </>
); );
}; };

View File

@@ -1,4 +1,3 @@
export * from './useTagMenu'; export * from './useTagMenu';
export * from './useStatusMenu'; export * from './useStatusMenu';
export * from './useLabelsMenu'; export * from './useLabelsMenu';
export * from './useUserRoute';

View File

@@ -1 +1 @@
export * from './useTagMenu.tsx'; export * from './useTagMenu.ts';

View File

@@ -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;
}, []);
};

View File

@@ -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;
}, []);
};

View File

@@ -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]);
};

View File

@@ -5,29 +5,22 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks'; import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
interface UseContextMenuSystemHandlersProps { interface UseContextMenuSystemHandlersProps {
hubs: string[]; hubs: string[];
userHubs: string[];
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
outCommand: OutCommandHandler; outCommand: OutCommandHandler;
} }
export const useContextMenuSystemHandlers = ({ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
systems,
hubs,
userHubs,
outCommand,
}: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null); const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>(); const [system, setSystem] = useState<string>();
const { deleteSystems } = useDeleteSystems(); const { deleteSystems } = useDeleteSystems();
const ref = useRef({ hubs, userHubs, system, systems, outCommand, deleteSystems }); const ref = useRef({ hubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, userHubs, system, systems, outCommand, deleteSystems }; ref.current = { hubs, system, systems, outCommand, deleteSystems };
const open = useCallback((ev: any, systemId: string) => { const open = useCallback((ev: any, systemId: string) => {
setSystem(systemId); setSystem(systemId);
@@ -79,37 +72,6 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined); 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 onSystemTag = useCallback((tag?: string) => {
const { system, outCommand } = ref.current; const { system, outCommand } = ref.current;
if (!system) { if (!system) {
@@ -142,6 +104,7 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined); setSystem(undefined);
}, []); }, []);
const onSystemStatus = useCallback((status: number) => { const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current; const { system, outCommand } = ref.current;
if (!system) { if (!system) {
@@ -214,8 +177,6 @@ export const useContextMenuSystemHandlers = ({
onDeleteSystem, onDeleteSystem,
onLockToggle, onLockToggle,
onHubToggle, onHubToggle,
onUserHubToggle,
// onTogglePingRally,
onSystemTag, onSystemTag,
onSystemTemporaryName, onSystemTemporaryName,
onSystemStatus, onSystemStatus,

View File

@@ -1,9 +1,4 @@
import { import { useLabelsMenu, useStatusMenu, useTagMenu } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
useLabelsMenu,
useStatusMenu,
useTagMenu,
useUserRoute,
} from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers'; import { getSystemById } from '@/hooks/Mapper/helpers';
import classes from './ContextMenuSystem.module.scss'; import classes from './ContextMenuSystem.module.scss';
@@ -15,19 +10,11 @@ import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts'; import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic'; 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 = ({ export const useContextMenuSystemItems = ({
onDeleteSystem, onDeleteSystem,
onLockToggle, onLockToggle,
onHubToggle, onHubToggle,
onUserHubToggle,
onTogglePing,
onSystemTag, onSystemTag,
onSystemStatus, onSystemStatus,
onSystemLabels, onSystemLabels,
@@ -36,7 +23,6 @@ export const useContextMenuSystemItems = ({
onWaypointSet, onWaypointSet,
systemId, systemId,
hubs, hubs,
userHubs,
systems, systems,
}: Omit<ContextMenuSystemProps, 'contextMenuRef'>) => { }: Omit<ContextMenuSystemProps, 'contextMenuRef'>) => {
const getTags = useTagMenu(systems, systemId, onSystemTag); const getTags = useTagMenu(systems, systemId, onSystemTag);
@@ -44,33 +30,11 @@ export const useContextMenuSystemItems = ({
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog); const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
const getWaypointMenu = useWaypointMenu(onWaypointSet); const getWaypointMenu = useWaypointMenu(onWaypointSet);
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]); 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 { return useMemo(() => {
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[] => {
const system = systemId ? getSystemById(systems, systemId) : undefined; const system = systemId ? getSystemById(systems, systemId) : undefined;
const systemStaticInfo = getSystemStaticInfo(systemId)!; const systemStaticInfo = getSystemStaticInfo(systemId)!;
const hasPing = ping?.solar_system_id === systemId;
if (!system || !systemId) { if (!system || !systemId) {
return []; return [];
} }
@@ -97,104 +61,50 @@ export const useContextMenuSystemItems = ({
...getLabels(), ...getLabels(),
...getWaypointMenu(systemId, systemStaticInfo.system_class), ...getWaypointMenu(systemId, systemStaticInfo.system_class),
{ {
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route', label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: !hubs.includes(systemId) ? ( icon: PrimeIcons.MAP_MARKER,
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onHubToggle, command: onHubToggle,
}, },
...getUserRoutes(), ...(system.locked
? canLockSystem
{ separator: true }, ? [
{ {
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing), label: 'Unlock',
disabled: !isShowPingBtn, icon: PrimeIcons.LOCK_OPEN,
template: () => { command: onLockToggle,
const iconClasses = clsx({ },
'pi text-cyan-400 hero-signal': !hasPing, ]
'pi text-red-400 hero-signal-slash': hasPing, : []
}); : [
...(canLockSystem
if (isShowPingBtn) { ? [
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>; {
} label: 'Lock',
icon: PrimeIcons.LOCK,
return ( command: onLockToggle,
<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
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []),
...(!system.locked && canManageSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
...(canDeleteSystem && !system.locked
? [
{ separator: true }, { separator: true },
{ {
label: 'Delete',
icon: PrimeIcons.TRASH,
command: onDeleteSystem, 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, canLockSystem,
systems, systems,
systemId,
getTags, getTags,
getStatus, getStatus,
getLabels, getLabels,
getWaypointMenu, getWaypointMenu,
getUserRoutes,
hubs, hubs,
onHubToggle, onHubToggle,
canLockSystem,
onLockToggle,
canDeleteSystem,
onDeleteSystem,
onOpenSettings, onOpenSettings,
onTogglePing, onLockToggle,
ping, onDeleteSystem,
isShowPingBtn,
]); ]);
}; };

View File

@@ -11,7 +11,6 @@ import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks'; import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { Route } from '@/hooks/Mapper/types/routes.ts'; import { Route } from '@/hooks/Mapper/types/routes.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
export interface ContextMenuSystemInfoProps { export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>; systemStatics: Map<number, SolarSystemStaticInfoRaw>;
@@ -70,12 +69,8 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
...getJumpPlannerMenu(system, routes), ...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class), ...getWaypointMenu(systemId, system.system_class),
{ {
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route', label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: !hubs.includes(systemId) ? ( icon: PrimeIcons.MAP_MARKER,
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onHubToggle, command: onHubToggle,
}, },
...(!systemOnMap ...(!systemOnMap

View File

@@ -1,25 +1,25 @@
import * as React from 'react'; import * as React from 'react';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu'; import { ContextMenu } from 'primereact/contextmenu';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; import { Commands, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types'; import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events'; 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 = () => { interface UseContextMenuSystemHandlersProps {
const { outCommand } = useMapRootState(); hubs: string[];
const { hubs = [], toggleHubCommand } = useRouteProvider(); outCommand: OutCommandHandler;
}
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null); const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>(); const [system, setSystem] = useState<string>();
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]); const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
const ref = useRef({ hubs, system, outCommand, toggleHubCommand }); const ref = useRef({ hubs, system, outCommand });
ref.current = { hubs, system, outCommand, toggleHubCommand }; ref.current = { hubs, system, outCommand };
const open = useCallback( const open = useCallback(
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => { (ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
@@ -33,12 +33,17 @@ export const useContextMenuSystemInfoHandlers = () => {
); );
const onHubToggle = useCallback(() => { const onHubToggle = useCallback(() => {
const { system } = ref.current; const { hubs, system, outCommand } = ref.current;
if (!system) { if (!system) {
return; return;
} }
ref.current.toggleHubCommand(system); outCommand({
type: !hubs.includes(system) ? OutCommand.addHub : OutCommand.deleteHub,
data: {
system_id: system,
},
});
setSystem(undefined); setSystem(undefined);
}, []); }, []);
@@ -54,8 +59,6 @@ export const useContextMenuSystemInfoHandlers = () => {
system_id: solarSystemId, system_id: solarSystemId,
}, },
}); });
// TODO add it to some queue
setTimeout(() => { setTimeout(() => {
emitMapEvent({ emitMapEvent({
name: Commands.centerSystem, name: Commands.centerSystem,

View File

@@ -1,24 +1,17 @@
import { Node } from 'reactflow'; import { Node } from 'reactflow';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu'; import { ContextMenu } from 'primereact/contextmenu';
import { SolarSystemRawType } from '@/hooks/Mapper/types'; import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks'; import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useContextMenuSystemMultipleHandlers = () => { export const useContextMenuSystemMultipleHandlers = () => {
const {
data: { pings },
} = useMapRootState();
const contextMenuRef = useRef<ContextMenu | null>(null); const contextMenuRef = useRef<ContextMenu | null>(null);
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>(); const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
const { deleteSystems } = useDeleteSystems(); const { deleteSystems } = useDeleteSystems();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => { const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
setSystems(systems_); setSystems(systems_);
ev.preventDefault(); ev.preventDefault();
@@ -31,17 +24,13 @@ export const useContextMenuSystemMultipleHandlers = () => {
return; return;
} }
const sysToDel = systems const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
.filter(x => !x.data.locked)
.filter(x => x.id !== ping?.solar_system_id)
.map(x => x.id);
if (sysToDel.length === 0) { if (sysToDel.length === 0) {
return; return;
} }
deleteSystems(sysToDel); deleteSystems(sysToDel);
}, [deleteSystems, systems, ping]); }, [deleteSystems, systems]);
return { return {
handleSystemMultipleContext, handleSystemMultipleContext,

View File

@@ -1,6 +1,6 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit'; import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons'; import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons.ts';
import classes from './FastSystemActions.module.scss'; import classes from './FastSystemActions.module.scss';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -59,21 +59,9 @@ export const FastSystemActions = ({
return ( return (
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}> <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)}> <div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
<WdImgButton <WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} />
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }} <WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} />
source={ZKB_ICON} <WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} />
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}
/>
</div> </div>
<div className="flex gap-2 items-center pl-1"> <div className="flex gap-2 items-center pl-1">
@@ -81,14 +69,14 @@ export const FastSystemActions = ({
textSize={WdImageSize.off} textSize={WdImageSize.off}
className={PrimeIcons.COPY} className={PrimeIcons.COPY}
onClick={copySystemNameToClipboard} onClick={copySystemNameToClipboard}
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }} tooltip={{ content: 'Copy system name' }}
/> />
{showEdit && ( {showEdit && (
<WdImgButton <WdImgButton
textSize={WdImageSize.off} textSize={WdImageSize.off}
className="pi pi-pen-to-square text-base" className="pi pi-pen-to-square text-base"
onClick={onOpenSettings} onClick={onOpenSettings}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }} tooltip={{ content: 'Edit system name and description' }}
/> />
)} )}
</div> </div>

View File

@@ -1 +0,0 @@
export * from './parseMapUserSettings.ts';

View File

@@ -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);

View File

@@ -1,4 +1,3 @@
export * from './useSystemInfo'; export * from './useSystemInfo';
export * from './useGetOwnOnlineCharacters'; export * from './useGetOwnOnlineCharacters';
export * from './useElementWidth'; export * from './useElementWidth';
export * from './useDetectSettingsChanged';

View File

@@ -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;
};

View File

@@ -1,6 +1,6 @@
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic'; import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic';
interface UseSystemInfoProps { interface UseSystemInfoProps {
@@ -17,7 +17,7 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
const dynamicInfo = getSystemById(systems, systemId); const dynamicInfo = getSystemById(systems, systemId);
if (!staticInfo || !dynamicInfo) { if (!staticInfo || !dynamicInfo) {
return { dynamicInfo, staticInfo, leadsTo: [] }; throw new Error(`Error on getting system ${systemId}`);
} }
const leadsTo = connections const leadsTo = connections

View File

@@ -28,12 +28,11 @@ import {
import { getBehaviorForTheme } from './helpers/getThemeBehavior'; import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types'; import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts'; 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 { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx'; import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars'; import { useBackgroundVars } from './hooks/useBackgroundVars';
import type { PanelPosition } from '@reactflow/core';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 }; const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -96,9 +95,6 @@ interface MapCompProps {
isShowBackgroundPattern?: boolean; isShowBackgroundPattern?: boolean;
isSoftBackground?: boolean; isSoftBackground?: boolean;
theme?: string; theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
} }
const MapComp = ({ const MapComp = ({
@@ -116,9 +112,6 @@ const MapComp = ({
isSoftBackground, isSoftBackground,
theme, theme,
onAddSystem, onAddSystem,
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
}: MapCompProps) => { }: MapCompProps) => {
const { getNodes } = useReactFlow(); const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes); const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -213,10 +206,8 @@ const MapComp = ({
...x, ...x,
showKSpaceBG: showKSpaceBG, showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections, isThickConnections: isThickConnections,
pings,
localShowShipName,
})); }));
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]); }, [showKSpaceBG, isThickConnections, update]);
return ( return (
<> <>
@@ -279,9 +270,7 @@ const MapComp = ({
// onlyRenderVisibleElements // onlyRenderVisibleElements
selectionMode={SelectionMode.Partial} selectionMode={SelectionMode.Partial}
> >
{isShowMinimap && ( {isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
)}
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />} {isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
</ReactFlow> </ReactFlow>
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}> {/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>

View File

@@ -10,7 +10,6 @@ export type MapData = MapUnionTypes & {
showKSpaceBG: boolean; showKSpaceBG: boolean;
isThickConnections: boolean; isThickConnections: boolean;
linkedSigEveId: string; linkedSigEveId: string;
localShowShipName: boolean;
}; };
interface MapProviderProps { interface MapProviderProps {
@@ -39,11 +38,6 @@ const INITIAL_DATA: MapData = {
systemSignatures: {} as Record<string, SystemSignature[]>, systemSignatures: {} as Record<string, SystemSignature[]>,
options: {} as Record<string, string | boolean>, options: {} as Record<string, string | boolean>,
isSubscriptionActive: false, isSubscriptionActive: false,
mainCharacterEveId: null,
followingCharacterEveId: null,
userHubs: [],
pings: [],
localShowShipName: false,
}; };
export interface MapContextProps { export interface MapContextProps {

View File

@@ -1 +0,0 @@
export * from './KillsCounter.tsx';

View File

@@ -1 +0,0 @@
export * from './LocalCounter';

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss'; import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow'; import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts'; import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx'; import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types'; import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -51,11 +51,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => { 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 offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getBezierPath; const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({ const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x, sourceX: sx - offset.x,

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useKillsCounter } from '../../hooks/useKillsCounter.ts'; import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts'; import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import { import {
KILLS_ROW_HEIGHT, KILLS_ROW_HEIGHT,
SystemKillsList, SystemKillsList,
@@ -26,7 +26,11 @@ export const KillsCounter = ({
children, children,
size = TooltipSize.xs, size = TooltipSize.xs,
}: KillsBookmarkTooltipProps) => { }: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills } = useKillsCounter({ const {
isLoading,
kills: detailedKills,
systemNameMap,
} = useKillsCounter({
realSystemId: systemId, realSystemId: systemId,
}); });
@@ -49,7 +53,7 @@ export const KillsCounter = ({
content={ content={
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}> <div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
<div className="flex-1 h-full"> <div className="flex-1 h-full">
<SystemKillsList kills={limitedKills} onlyOneSystem timeRange={1} /> <SystemKillsList kills={limitedKills} onlyOneSystem />
</div> </div>
</div> </div>
} }

View File

@@ -3,11 +3,11 @@ import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper'; import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip'; import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components'; import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts'; 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 { interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>; localCounterCharacters: Array<CharItemProps>;
@@ -16,10 +16,8 @@ interface LocalCounterProps {
} }
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => { export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const { const [settings] = useLocalCharacterWidgetSettings();
data: { localShowShipName }, const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
} = useMapState();
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
const theme = useTheme(); const theme = useTheme();
const pilotTooltipContent = useMemo(() => { const pilotTooltipContent = useMemo(() => {

View File

@@ -1,23 +1,11 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables'; @import '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a; $pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255); $pastel-pink: #d291bc;
$dark-bg: #2d2d2d; $dark-bg: #2d2d2d;
$text-color: #ffffff; $text-color: #ffffff;
$tooltip-bg: #202020; $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 { .RootCustomNode {
display: flex; display: flex;
width: 130px; width: 130px;
@@ -40,12 +28,11 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
z-index: 3; z-index: 3;
overflow: hidden; overflow: hidden;
&.Pochven,
&.Mataria, &.Mataria,
&.Amarria, &.Amarria,
&.Gallente, &.Gallente,
&.Caldaria { &.Caldaria {
&::after { &::before {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
@@ -61,7 +48,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
} }
&.Mataria { &.Mataria {
&::after { &::before {
background-image: url('/images/mataria-180.png'); background-image: url('/images/mataria-180.png');
opacity: 0.6; opacity: 0.6;
background-position-x: 1px; background-position-x: 1px;
@@ -70,7 +57,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
} }
&.Caldaria { &.Caldaria {
&::after { &::before {
background-image: url('/images/caldaria-180.png'); background-image: url('/images/caldaria-180.png');
opacity: 0.6; opacity: 0.6;
background-position-x: 1px; background-position-x: 1px;
@@ -79,7 +66,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
} }
&.Amarria { &.Amarria {
&::after { &::before {
opacity: 0.45; opacity: 0.45;
background-image: url('/images/amarr-180.png'); background-image: url('/images/amarr-180.png');
background-position-x: 0; background-position-x: 0;
@@ -88,7 +75,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
} }
&.Gallente { &.Gallente {
&::after { &::before {
opacity: 0.5; opacity: 0.5;
background-image: url('/images/gallente-180.png'); background-image: url('/images/gallente-180.png');
background-position-x: 1px; background-position-x: 1px;
@@ -96,43 +83,11 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
} }
} }
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -13px;
}
}
&.selected { &.selected {
border-color: $pastel-pink; border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2; 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 { &.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30); border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent); background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);

View File

@@ -12,26 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants'; } from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp'; import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature'; import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts'; import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit'; 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>) => { export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props); const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars); const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount( const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
return ( return (
<> <>
{nodeVars.visible && ( {nodeVars.visible && (
<div className={classes.Bookmarks}> <div className={classes.Bookmarks}>
{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.isShattered && ( {nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}> <div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}> <WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -40,27 +40,21 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div> </div>
)} )}
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && ( {localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter <KillsCounter
killsCount={localKillsCount} killsCount={localKillsCount}
systemId={nodeVars.solarSystemId} systemId={nodeVars.solarSystemId}
size={TooltipSize.lg} size={TooltipSize.lg}
killsActivityType={localKillsActivityType} killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
> >
<div className={clsx(classes.BookmarkWithIcon)}> <div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} /> <span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span> <span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div> </div>
</KillsCounter> </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 => ( {nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}> <div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName} {x.shortName}
@@ -73,11 +67,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
className={clsx( className={clsx(
classes.RootCustomNode, classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass], nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]], nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ { [classes.selected]: nodeVars.selected },
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
)} )}
onMouseDownCapture={e => nodeVars.dbClick(e)} onMouseDownCapture={e => nodeVars.dbClick(e)}
> >
@@ -95,11 +86,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div> </div>
{nodeVars.tag != null && nodeVars.tag !== '' && ( {nodeVars.tag != null && nodeVars.tag !== '' && (
<Tag <div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
value={nodeVars.tag}
severity="warning"
className="py-0 px-[2px] text-[9px] [&_.p-tag-value]:leading-[1.3]"
></Tag>
)} )}
<div <div

View File

@@ -12,25 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants'; } from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp'; import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature'; import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit'; import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts'; 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>) => { export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props); const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars); const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount( const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
return ( return (
<> <>
{nodeVars.visible && ( {nodeVars.visible && (
<div className={classes.Bookmarks}> <div className={classes.Bookmarks}>
{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.isShattered && ( {nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}> <div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}> <WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -39,27 +40,21 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</div> </div>
)} )}
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && ( {localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter <KillsCounter
killsCount={localKillsCount} killsCount={localKillsCount}
systemId={nodeVars.solarSystemId} systemId={nodeVars.solarSystemId}
size={TooltipSize.lg} size={TooltipSize.lg}
killsActivityType={localKillsActivityType} killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
> >
<div className={clsx(classes.BookmarkWithIcon)}> <div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} /> <span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span> <span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div> </div>
</KillsCounter> </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 => ( {nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}> <div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName} {x.shortName}
@@ -73,10 +68,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
classes.RootCustomNode, classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass], nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '', nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ { [classes.selected]: nodeVars.selected },
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
)} )}
onMouseDownCapture={e => nodeVars.dbClick(e)} onMouseDownCapture={e => nodeVars.dbClick(e)}
> >
@@ -121,13 +113,23 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}> <div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && ( {nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5"> <div
className={clsx(
classes.CustomName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.customName} {nodeVars.customName}
</div> </div>
)} )}
{!nodeVars.isWormhole && !nodeVars.customName && ( {!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"> <div
className={clsx(
classes.RegionName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.regionName} {nodeVars.regionName}
</div> </div>
)} )}

View File

@@ -16,7 +16,7 @@ export enum SOLAR_SYSTEM_CLASS_IDS {
thera = 12, thera = 12,
c13 = 13, c13 = 13,
sentinel = 14, sentinel = 14,
barbican = 15, baribican = 15,
vidette = 16, vidette = 16,
conflux = 17, conflux = 17,
redoubt = 18, redoubt = 18,
@@ -82,7 +82,7 @@ export const SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS = {
thera: SOLAR_SYSTEM_CLASS_GROUPS.thera, thera: SOLAR_SYSTEM_CLASS_GROUPS.thera,
c13: SOLAR_SYSTEM_CLASS_GROUPS.c13, c13: SOLAR_SYSTEM_CLASS_GROUPS.c13,
sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter, sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
barbican: SOLAR_SYSTEM_CLASS_GROUPS.drifter, baribican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter, vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter, conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter, redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
@@ -217,7 +217,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 14, wormholeClassID: 14,
effectPower: 2, effectPower: 2,
title: 'Class 14 (Sentinel Drifter)', title: 'Class 14 (Sentinel Drifter)',
shortTitle: 'Sentinel MZ', shortTitle: 'Sentinel',
}, },
{ {
id: 'barbican', id: 'barbican',
@@ -225,7 +225,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 15, wormholeClassID: 15,
effectPower: 2, effectPower: 2,
title: 'Class 15 (Barbican Drifter)', title: 'Class 15 (Barbican Drifter)',
shortTitle: 'Liberated Barbican', shortTitle: 'Barbican',
}, },
{ {
id: 'vidette', id: 'vidette',
@@ -233,7 +233,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 16, wormholeClassID: 16,
effectPower: 2, effectPower: 2,
title: 'Class 16 (Vidette Drifter)', title: 'Class 16 (Vidette Drifter)',
shortTitle: 'Sanctified Vidette', shortTitle: 'Vidette',
}, },
{ {
id: 'conflux', id: 'conflux',
@@ -241,7 +241,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 17, wormholeClassID: 17,
effectPower: 2, effectPower: 2,
title: 'Class 17 (Conflux Drifter)', title: 'Class 17 (Conflux Drifter)',
shortTitle: 'Conflux Eyrie', shortTitle: 'Conflux',
}, },
{ {
id: 'redoubt', id: 'redoubt',
@@ -249,7 +249,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 18, wormholeClassID: 18,
effectPower: 2, effectPower: 2,
title: 'Class 18 (Redoubt Drifter)', title: 'Class 18 (Redoubt Drifter)',
shortTitle: 'Azdaja Redoubt', shortTitle: 'Redoubt',
}, },
{ {
id: 'a1', id: 'a1',

View File

@@ -10,7 +10,7 @@ export const isWormholeSpace = (wormholeClassID: number) => {
case SOLAR_SYSTEM_CLASS_IDS.c6: case SOLAR_SYSTEM_CLASS_IDS.c6:
case SOLAR_SYSTEM_CLASS_IDS.c13: case SOLAR_SYSTEM_CLASS_IDS.c13:
case SOLAR_SYSTEM_CLASS_IDS.thera: 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.vidette:
case SOLAR_SYSTEM_CLASS_IDS.conflux: case SOLAR_SYSTEM_CLASS_IDS.conflux:
case SOLAR_SYSTEM_CLASS_IDS.redoubt: case SOLAR_SYSTEM_CLASS_IDS.redoubt:

View File

@@ -1,4 +1,5 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx'; import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import { import {
CommandCharacterAdded, CommandCharacterAdded,
CommandCharacterRemoved, CommandCharacterRemoved,
@@ -6,7 +7,6 @@ import {
CommandCharacterUpdated, CommandCharacterUpdated,
CommandPresentCharacters, CommandPresentCharacters,
} from '@/hooks/Mapper/types'; } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => { export const useCommandsCharacters = () => {
const { update } = useMapState(); const { update } = useMapState();

View File

@@ -1,6 +1,6 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx'; import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
export const useMapCommands = () => { export const useMapCommands = () => {
const { update } = useMapState(); const { update } = useMapState();
@@ -8,21 +8,13 @@ export const useMapCommands = () => {
const ref = useRef({ update }); const ref = useRef({ update });
ref.current = { update }; ref.current = { update };
const mapUpdated = useCallback(({ hubs, system_signatures, kills }: CommandMapUpdated) => { const mapUpdated = useCallback(({ hubs }: CommandMapUpdated) => {
const out: Partial<MapData> = {}; const out: Partial<MapData> = {};
if (hubs) { if (hubs) {
out.hubs = 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); ref.current.update(out);
}, []); }, []);

View File

@@ -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 { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers'; import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapInit = () => { export const useMapInit = () => {
const rf = useReactFlow(); const rf = useReactFlow();

View File

@@ -22,7 +22,6 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
systemId: realSystemId, systemId: realSystemId,
outCommand, outCommand,
showAllVisible: false, showAllVisible: false,
sinceHours: 1,
}); });
const filteredKills = useMemo(() => { const filteredKills = useMemo(() => {

View File

@@ -115,18 +115,35 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
}, 500); }, 500);
break; break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes: case Commands.routes:
// do nothing here
break;
case Commands.signaturesUpdated: case Commands.signaturesUpdated:
// do nothing here
break;
case Commands.linkSignatureToSystem: case Commands.linkSignatureToSystem:
// do nothing here
break;
case Commands.detailedKillsUpdated: case Commands.detailedKillsUpdated:
// do nothing here
break;
case Commands.characterActivityData: case Commands.characterActivityData:
break;
case Commands.trackingCharactersData: case Commands.trackingCharactersData:
break;
case Commands.updateActivity: case Commands.updateActivity:
break;
case Commands.updateTracking: case Commands.updateTracking:
break;
case Commands.userSettingsUpdated: case Commands.userSettingsUpdated:
// do nothing
break; break;
default: default:

View File

@@ -1,7 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { useMapEventListener } from '@/hooks/Mapper/events'; import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types'; import { Commands } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface Kill { interface Kill {
solar_system_id: number | string; solar_system_id: number | string;
@@ -10,78 +9,34 @@ interface Kill {
interface MapEvent { interface MapEvent {
name: Commands; name: Commands;
data?: unknown; data?: any;
payload?: Kill[]; payload?: Kill[];
} }
function getActivityType(count: number): string { export function useNodeKillsCount(
if (count <= 5) return 'activityNormal'; systemId: number | string,
if (count <= 30) return 'activityWarn'; initialKillsCount: number | null
return 'activityDanger'; ): number | null {
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null = null): { killsCount: number | null; killsActivityType: string | null } {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount); const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
const { data: mapData } = useMapRootState();
const { detailedKills = {} } = mapData;
// Calculate 1-hour kill count from detailed kills
const oneHourKillCount = useMemo(() => {
const systemKills = detailedKills[systemId] || [];
// If we have detailed kills data (even if empty), use it for counting
if (Object.prototype.hasOwnProperty.call(detailedKills, systemId)) {
const oneHourAgo = Date.now() - 60 * 60 * 1000; // 1 hour in milliseconds
const recentKills = systemKills.filter(kill => {
if (!kill.kill_time) return false;
const killTime = new Date(kill.kill_time).getTime();
if (isNaN(killTime)) return false;
return killTime >= oneHourAgo;
});
return recentKills.length; // Return 0 if no recent kills, not null
}
// Return null only if we don't have detailed kills data for this system
return null;
}, [detailedKills, systemId]);
useEffect(() => { useEffect(() => {
// Always prefer the calculated 1-hour count over initial count setKillsCount(initialKillsCount);
// This ensures we properly expire old kills }, [initialKillsCount]);
if (oneHourKillCount !== null) {
setKillsCount(oneHourKillCount);
} else if (detailedKills[systemId] && detailedKills[systemId].length === 0) {
// If we have detailed kills data but it's empty, set to 0
setKillsCount(0);
} else {
// Only fall back to initial count if we have no detailed kills data at all
setKillsCount(initialKillsCount);
}
}, [oneHourKillCount, initialKillsCount, detailedKills, systemId]);
const handleEvent = useCallback( const handleEvent = useCallback((event: MapEvent): boolean => {
(event: MapEvent): boolean => { if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) { const killForSystem = event.payload.find(
const killForSystem = event.payload.find(kill => kill.solar_system_id.toString() === systemId.toString()); kill => kill.solar_system_id.toString() === systemId.toString()
if (killForSystem && typeof killForSystem.kills === 'number') { );
// Only update if we don't have detailed kills data if (killForSystem && typeof killForSystem.kills === 'number') {
if (!detailedKills[systemId] || detailedKills[systemId].length === 0) { setKillsCount(killForSystem.kills);
setKillsCount(killForSystem.kills);
}
}
return true;
} }
return false; return true;
}, }
[systemId, detailedKills], return false;
); }, [systemId]);
useMapEventListener(handleEvent); useMapEventListener(handleEvent);
const killsActivityType = useMemo(() => { return killsCount;
return killsCount !== null && killsCount > 0 ? getActivityType(killsCount) : null;
}, [killsCount]);
return { killsCount, killsActivityType };
} }

View File

@@ -5,51 +5,20 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api'; import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider'; import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick'; import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants'; import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers'; import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers'; import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types'; import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
import { useUnsplashedSignatures } from './useUnsplashedSignatures'; import { useUnsplashedSignatures } from './useUnsplashedSignatures';
import { useSystemName } from './useSystemName'; import { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo'; import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic'; import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export interface SolarSystemNodeVars { function getActivityType(count: number): string {
id: string; if (count <= 5) return 'activityNormal';
selected: boolean; if (count <= 30) return 'activityWarn';
visible: boolean; return 'activityDanger';
isWormhole: boolean;
classTitleColor: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
} }
const SpaceToClass: Record<string, string> = { const SpaceToClass: Record<string, string> = {
@@ -57,7 +26,6 @@ const SpaceToClass: Record<string, string> = {
[Spaces.Matar]: 'Mataria', [Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria', [Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente', [Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
}; };
export function useLocalCounter(nodeVars: SolarSystemNodeVars) { export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
@@ -73,7 +41,7 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
return { localCounterCharacters }; return { localCounterCharacters };
} }
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => { export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props; const { id, data, selected } = props;
const { const {
id: solar_system_id, id: solar_system_id,
@@ -87,14 +55,10 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
} = data; } = data;
const { const {
storedSettings: { interfaceSettings }, interfaceSettings,
data: { systemSignatures: mapSystemSignatures }, data: { systemSignatures: mapSystemSignatures },
} = useMapRootState(); } = useMapRootState();
const systemStaticInfo = useMemo(() => {
return getSystemStaticInfo(solar_system_id)!;
}, [solar_system_id]);
const { const {
system_class, system_class,
security, security,
@@ -105,8 +69,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
region_id, region_id,
is_shattered, is_shattered,
solar_system_name, solar_system_name,
constellation_name, } = useMemo(() => {
} = systemStaticInfo; return getSystemStaticInfo(parseInt(solar_system_id))!;
}, [solar_system_id]);
const { isShowUnsplashedSignatures } = interfaceSettings; const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true'; const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
@@ -118,13 +83,13 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
characters, characters,
wormholesData, wormholesData,
hubs, hubs,
kills,
userCharacters, userCharacters,
isConnecting, isConnecting,
hoverNodeId, hoverNodeId,
visibleNodes, visibleNodes,
showKSpaceBG, showKSpaceBG,
isThickConnections, isThickConnections,
pings,
}, },
outCommand, outCommand,
} = useMapState(); } = useMapState();
@@ -154,6 +119,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isShowLinkedSigId, isShowLinkedSigId,
}); });
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo( const hasUserCharacters = useMemo(
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)), () => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
[charactersInSystem, userCharacters], [charactersInSystem, userCharacters],
@@ -162,7 +130,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const dbClick = useDoubleClick(() => { const dbClick = useDoubleClick(() => {
outCommand({ outCommand({
type: OutCommand.openSettings, type: OutCommand.openSettings,
data: { system_id: solar_system_id }, data: { system_id: solar_system_id.toString() },
}); });
}); });
@@ -174,35 +142,24 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const { systemName, computedTemporaryName, customName } = useSystemName({ const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled, isTempSystemNameEnabled,
temporary_name, temporary_name,
solar_system_name: solar_system_name || '',
isShowLinkedSigIdTempName, isShowLinkedSigIdTempName,
linkedSigPrefix, linkedSigPrefix,
name, name,
systemStaticInfo,
}); });
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures); const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]); const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const isRally = useMemo(
() => !!pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
[pings, solar_system_id],
);
const regionName = useMemo(() => {
if (region_id === Regions.Pochven) {
return constellation_name;
}
return region_name;
}, [constellation_name, region_id, region_name]);
const nodeVars: SolarSystemNodeVars = { const nodeVars: SolarSystemNodeVars = {
id, id,
selected, selected,
visible, visible,
isWormhole, isWormhole,
classTitleColor, classTitleColor,
killsCount,
killsActivityType,
hasUserCharacters, hasUserCharacters,
userCharacters, userCharacters,
showHandlers, showHandlers,
@@ -229,10 +186,47 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections, isThickConnections,
classTitle: class_title, classTitle: class_title,
temporaryName: computedTemporaryName, temporaryName: computedTemporaryName,
regionName, regionName: region_name,
solarSystemName: solar_system_name, solarSystemName: solar_system_name,
isRally,
}; };
return nodeVars; return nodeVars;
}; }
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
classTitle: string | null;
temporaryName?: string | null;
}

View File

@@ -1,34 +1,30 @@
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types'; // useSystemName.ts
import { useMemo } from 'react'; import { useMemo } from 'react';
interface UseSystemNameParams { interface UseSystemNameParams {
isTempSystemNameEnabled: boolean; isTempSystemNameEnabled: boolean;
temporary_name?: string | null; temporary_name?: string | null;
solar_system_name: string;
isShowLinkedSigIdTempName: boolean; isShowLinkedSigIdTempName: boolean;
linkedSigPrefix: string | null; linkedSigPrefix: string | null;
name?: string | null; name?: string | null;
systemStaticInfo: SolarSystemStaticInfoRaw;
} }
export const useSystemName = ({ export function useSystemName({
isTempSystemNameEnabled, isTempSystemNameEnabled,
temporary_name, temporary_name,
solar_system_name,
isShowLinkedSigIdTempName, isShowLinkedSigIdTempName,
linkedSigPrefix, linkedSigPrefix,
name, name,
systemStaticInfo, }: UseSystemNameParams) {
}: UseSystemNameParams) => {
const { solar_system_name = '' } = systemStaticInfo;
const computedTemporaryName = useMemo(() => { const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) { if (!isTempSystemNameEnabled) {
return ''; return '';
} }
if (isShowLinkedSigIdTempName && linkedSigPrefix) { if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}:${temporary_name}` : `${linkedSigPrefix}:${solar_system_name}`; return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
} }
return temporary_name ?? ''; return temporary_name ?? '';
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]); }, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
@@ -36,7 +32,6 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName) { if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName; return computedTemporaryName;
} }
return solar_system_name; return solar_system_name;
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]); }, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
@@ -44,13 +39,11 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName && name) { if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name; return name;
} }
if (solar_system_name !== name && name) { if (solar_system_name !== name && name) {
return name; return name;
} }
return null; return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]); }, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
return { systemName, computedTemporaryName, customName }; return { systemName, computedTemporaryName, customName };
}; }

View File

@@ -1,48 +1,37 @@
import { Position, internalsSymbol, Node } from 'reactflow'; import { Position, internalsSymbol } from 'reactflow';
type Coords = [number, number]; // returns the position (top,right,bottom or right) passed node compared to
type CoordsWithPosition = [number, number, Position]; function getParams(nodeA, nodeB) {
function segmentsIntersect(a1: number, a2: number, b1: number, b2: number): boolean {
const [minA, maxA] = a1 < a2 ? [a1, a2] : [a2, a1];
const [minB, maxB] = b1 < b2 ? [b1, b2] : [b2, b1];
return maxA >= minB && maxB >= minA;
}
function getParams(nodeA: Node, nodeB: Node): CoordsWithPosition {
const centerA = getNodeCenter(nodeA); const centerA = getNodeCenter(nodeA);
const centerB = getNodeCenter(nodeB); const centerB = getNodeCenter(nodeB);
const horizontalDiff = Math.abs(centerA.x - centerB.x);
const verticalDiff = Math.abs(centerA.y - centerB.y);
let position: Position; let position: Position;
if ( // when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
segmentsIntersect( if (horizontalDiff > verticalDiff) {
nodeA.positionAbsolute!.x - 10,
nodeA.positionAbsolute!.x - 10 + nodeA.width! + 20,
nodeB.positionAbsolute!.x,
nodeB.positionAbsolute!.x + nodeB.width!,
)
) {
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
} else {
position = centerA.x > centerB.x ? Position.Left : Position.Right; position = centerA.x > centerB.x ? Position.Left : Position.Right;
} else {
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
} }
const [x, y] = getHandleCoordsByPosition(nodeA, position); const [x, y] = getHandleCoordsByPosition(nodeA, position);
return [x, y, position]; return [x, y, position];
} }
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords { function getHandleCoordsByPosition(node, handlePosition) {
const handle = node[internalsSymbol]!.handleBounds!.source!.find(h => h.position === handlePosition); // all handles are from type source, that's why we use handleBounds.source here
const handle = node[internalsSymbol].handleBounds.source.find(h => h.position === handlePosition);
if (!handle) {
throw new Error(`Handle with position ${handlePosition} not found on node ${node.id}`);
}
let offsetX = handle.width / 2; let offsetX = handle.width / 2;
let offsetY = handle.height / 2; let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) { switch (handlePosition) {
case Position.Left: case Position.Left:
offsetX = 0; offsetX = 0;
@@ -58,20 +47,21 @@ function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords
break; break;
} }
const x = node.positionAbsolute!.x + handle.x + offsetX; const x = node.positionAbsolute.x + handle.x + offsetX;
const y = node.positionAbsolute!.y + handle.y + offsetY; const y = node.positionAbsolute.y + handle.y + offsetY;
return [x, y]; return [x, y];
} }
function getNodeCenter(node: Node): { x: number; y: number } { function getNodeCenter(node) {
return { return {
x: node.positionAbsolute!.x + node.width! / 2, x: node.positionAbsolute.x + node.width / 2,
y: node.positionAbsolute!.y + node.height! / 2, y: node.positionAbsolute.y + node.height / 2,
}; };
} }
export function getEdgeParams(source: Node, target: Node) { // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source, target) {
const [sx, sy, sourcePos] = getParams(source, target); const [sx, sy, sourcePos] = getParams(source, target);
const [tx, ty, targetPos] = getParams(target, source); const [tx, ty, targetPos] = getParams(target, source);

View File

@@ -1,12 +1,9 @@
import classes from './MarkdownComment.module.scss'; import classes from './MarkdownComment.module.scss';
import clsx from 'clsx'; import clsx from 'clsx';
import { import Markdown from 'react-markdown';
InfoDrawer, import remarkGfm from 'remark-gfm';
MarkdownTextViewer, import { InfoDrawer, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
TimeAgo, import remarkBreaks from 'remark-breaks';
TooltipPosition,
WdImgButton,
} from '@/hooks/Mapper/components/ui-kit';
import { useGetCacheCharacter } from '@/hooks/Mapper/mapRootProvider/hooks/api'; import { useGetCacheCharacter } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { WdTransition } from '@/hooks/Mapper/components/ui-kit/WdTransition/WdTransition.tsx'; import { WdTransition } from '@/hooks/Mapper/components/ui-kit/WdTransition/WdTransition.tsx';
@@ -14,9 +11,9 @@ import { PrimeIcons } from 'primereact/api';
import { ConfirmPopup } from 'primereact/confirmpopup'; import { ConfirmPopup } from 'primereact/confirmpopup';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types'; import { OutCommand } from '@/hooks/Mapper/types';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top }; const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
export interface MarkdownCommentProps { export interface MarkdownCommentProps {
text: string; text: string;
@@ -29,7 +26,8 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const char = useGetCacheCharacter(characterEveId); const char = useGetCacheCharacter(characterEveId);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup(); const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const { outCommand } = useMapRootState(); const { outCommand } = useMapRootState();
const ref = useRef({ outCommand, id }); const ref = useRef({ outCommand, id });
@@ -45,6 +43,9 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const handleMouseEnter = useCallback(() => setHovered(true), []); const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []); const handleMouseLeave = useCallback(() => setHovered(false), []);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
return ( return (
<> <>
<InfoDrawer <InfoDrawer
@@ -65,11 +66,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
{!hovered && <TimeAgo timestamp={time} />} {!hovered && <TimeAgo timestamp={time} />}
{hovered && ( {hovered && (
// @ts-ignore // @ts-ignore
<div ref={cfRef}> <div ref={cpRemoveBtnRef}>
<WdImgButton <WdImgButton
className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')} className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')}
tooltip={TOOLTIP_PROPS} tooltip={TOOLTIP_PROPS}
onClick={cfShow} onClick={handleShowCP}
/> />
</div> </div>
)} )}
@@ -78,13 +79,13 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
</div> </div>
} }
> >
<MarkdownTextViewer>{text}</MarkdownTextViewer> <Markdown remarkPlugins={REMARK_PLUGINS}>{text}</Markdown>
</InfoDrawer> </InfoDrawer>
<ConfirmPopup <ConfirmPopup
target={cfRef.current} target={cpRemoveBtnRef.current}
visible={cfVisible} visible={cpRemoveVisible}
onHide={cfHide} onHide={handleHideCP}
message="Are you sure you want to delete?" message="Are you sure you want to delete?"
icon="pi pi-exclamation-triangle" icon="pi pi-exclamation-triangle"
accept={handleDelete} accept={handleDelete}

View File

@@ -9,7 +9,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsEditorProps {} export interface CommentsEditorProps {}
// eslint-disable-next-line no-empty-pattern
export const CommentsEditor = ({}: CommentsEditorProps) => { export const CommentsEditor = ({}: CommentsEditorProps) => {
const [textVal, setTextVal] = useState(''); const [textVal, setTextVal] = useState('');

View File

@@ -1,49 +0,0 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import { RoutesList } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesList';
export const PingRoute = () => {
const {
data: { routes, pings, loadingPublicRoutes },
} = useMapRootState();
const route = useMemo(() => {
const [ping] = pings;
if (!ping) {
return null;
}
return routes?.routes.find(x => ping.solar_system_id === x.destination.toString()) ?? null;
}, [routes, pings]);
const preparedRoute = useMemo(() => {
if (!route) {
return null;
}
return {
...route,
mapped_systems:
route.systems?.map(solar_system_id =>
routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id,
),
) ?? [],
};
}, [route, routes?.systems_static_data]);
if (loadingPublicRoutes) {
return <span className="m-0 text-[12px]">Loading...</span>;
}
if (!preparedRoute || preparedRoute.origin === preparedRoute.destination) {
return null;
}
return (
<div className="m-0 flex gap-2 items-center text-[12px]">
{preparedRoute.has_connection && <div className="text-[12px]">{preparedRoute.systems?.length ?? 2}</div>}
<RoutesList data={preparedRoute} />
</div>
);
};

View File

@@ -1,280 +0,0 @@
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import {
CharacterCardById,
SystemView,
TimeAgo,
TooltipPosition,
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import useRefState from 'react-usestateref';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
[PingsPlacement.leftTop]: 'top-left',
[PingsPlacement.rightBottom]: 'bottom-right',
[PingsPlacement.leftBottom]: 'bottom-left',
};
const PING_PLACEMENT_MAP_OFFSETS = {
[PingsPlacement.rightTop]: { default: '!top-[56px]', withLeftMenu: '!top-[56px] !right-[64px]' },
[PingsPlacement.rightBottom]: { default: '!bottom-[15px]', withLeftMenu: '!bottom-[15px] !right-[64px]' },
[PingsPlacement.leftTop]: { default: '!top-[56px] !left-[64px]', withLeftMenu: '!top-[56px] !left-[64px]' },
[PingsPlacement.leftBottom]: { default: '!left-[64px] !bottom-[15px]', withLeftMenu: '!bottom-[15px]' },
};
const CLOSE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Hide',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const NAVIGATE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Navigate To',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const DELETE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Remove',
position: TooltipPosition.top,
className: '!leading-[0]',
};
// const TOOLTIP_WAYPOINT_PROPS: WdImgButtonTooltip = {
// content: 'Waypoint',
// position: TooltipPosition.bottom,
// className: '!leading-[0]',
// };
const TITLES = {
[PingType.Alert]: 'Alert',
[PingType.Rally]: 'Rally Point',
};
const ICONS = {
[PingType.Alert]: 'pi-bell',
[PingType.Rally]: 'pi-bell',
};
export interface PingsInterfaceProps {
hasLeftOffset?: boolean;
}
// TODO: right now can be one ping. But in future will be multiple pings then:
// 1. we will use this as container
// 2. we will create PingInstance (which will contains ping Button and Toast
// 3. ADD Context menu
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const toast = useRef<Toast>(null);
const [isShow, setIsShow, isShowRef] = useRefState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const {
storedSettings: { interfaceSettings },
data: { pings, selectedSystems },
outCommand,
} = useMapRootState();
const selectedSystem = useMemo(() => {
if (selectedSystems.length !== 1) {
return null;
}
return selectedSystems[0];
}, [selectedSystems]);
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
const navigateTo = useCallback(() => {
if (!ping) {
return;
}
emitMapEvent({
name: Commands.centerSystem,
data: ping.solar_system_id?.toString(),
});
}, [ping]);
const removePing = useCallback(async () => {
if (!ping) {
return;
}
await outCommand({
type: OutCommand.cancelPing,
data: { type: ping.type, id: ping.id },
});
}, [outCommand, ping]);
useEffect(() => {
if (!ping) {
return;
}
const tid = setTimeout(() => {
toast.current?.replace({ severity: 'warn', detail: ping.message });
setIsShow(true);
}, 200);
return () => clearTimeout(tid);
}, [ping]);
const handleClickShow = useCallback(() => {
if (!ping) {
return;
}
if (!isShowRef.current) {
toast.current?.show({ severity: 'warn', detail: ping.message });
setIsShow(true);
return;
}
toast.current?.clear();
setIsShow(false);
}, [ping]);
const handleClickHide = useCallback(() => {
toast.current?.clear();
setIsShow(false);
}, []);
const { placement, offsets } = useMemo(() => {
const rawPlacement =
interfaceSettings.pingsPlacement == null ? PingsPlacement.rightTop : interfaceSettings.pingsPlacement;
return {
placement: PING_PLACEMENT_MAP[rawPlacement],
offsets: PING_PLACEMENT_MAP_OFFSETS[rawPlacement],
};
}, [interfaceSettings]);
if (!ping) {
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
return (
<>
<Toast
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full">
<div className="flex justify-between">
<div>
<div className="m-0 font-semibold text-base text-white">{TITLES[ping.type]}</div>
<div className="flex gap-1 items-center">
{isShowSelectedSystem && (
<>
<SystemView systemId={selectedSystem} />
<span className="pi pi-angle-double-right text-[10px] relative top-[1px] text-stone-400" />
</>
)}
<SystemView systemId={ping.solar_system_id} />
{isShowSelectedSystem && (
<WdImgButton
className={clsx(PrimeIcons.QUESTION_CIRCLE, 'ml-[2px] relative top-[-2px] !text-[10px]')}
tooltip={{
position: TooltipPosition.top,
content: (
<div className="flex flex-col gap-1">
The settings for the route are taken from the Routes settings and can be configured
through them.
</div>
),
}}
/>
)}
</div>
</div>
<div className="flex flex-col items-end">
<CharacterCardById className="" characterId={ping.character_eve_id} simpleMode />
<TimeAgo timestamp={ping.inserted_at.toString()} className="text-stone-400 text-[11px]" />
</div>
</div>
{selectedSystem != null && <PingRoute />}
<p className="m-0 text-[13px] text-stone-200 min-h-[20px] pr-[16px]">{message.detail}</p>
</div>
<WdImgButton
className={clsx(PrimeIcons.TIMES, 'hover:text-red-400 mt-[3px]')}
tooltip={CLOSE_TOOLTIP_PROPS}
onClick={handleClickHide}
/>
</div>
{/*Button bar*/}
<div className="flex justify-end items-center gap-2 h-0 relative top-[-8px]">
<WdImgButton
className={clsx('pi-compass', 'hover:text-red-400 mt-[3px]')}
tooltip={NAVIGATE_TOOLTIP_PROPS}
onClick={navigateTo}
/>
{/*@ts-ignore*/}
<div ref={cfRef}>
<WdImgButton
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
tooltip={DELETE_TOOLTIP_PROPS}
onClick={cfShow}
/>
</div>
{/* TODO ADD solar system menu*/}
{/*<WdImgButton*/}
{/* className={clsx('pi-map-marker', 'hover:text-red-400 mt-[3px]')}*/}
{/* tooltip={TOOLTIP_WAYPOINT_PROPS}*/}
{/* onClick={handleClickHide}*/}
{/*/>*/}
</div>
</section>
)}
></Toast>
<Button
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
);
};

View File

@@ -1 +0,0 @@
export * from './PingsInterface.tsx';

View File

@@ -1,20 +1,23 @@
import { useCallback, useMemo, useRef } from 'react';
import { Dialog } from 'primereact/dialog'; import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks'; import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import { import {
SOLAR_SYSTEM_CLASS_IDS, SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS, SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME, WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts'; } from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts'; import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize'; import {
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo'; SETTINGS_KEYS,
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; SignatureSettingsType,
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types'; } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName; const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -46,9 +49,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
ref.current = { outCommand }; ref.current = { outCommand };
// Get system info for the target system // Get system info for the target system
const { staticInfo: targetSystemInfo, dynamicInfo: targetSystemDynamicInfo } = useSystemInfo({ const { staticInfo: targetSystemInfo } = useSystemInfo({ systemId: `${data.solar_system_target}` });
systemId: `${data.solar_system_target}`,
});
// Get the system class group for the target system // Get the system class group for the target system
const targetSystemClassGroup = useMemo(() => { const targetSystemClassGroup = useMemo(() => {
@@ -143,7 +144,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
} }
const whShipSize = getWhSize(wormholes, signature.type); const whShipSize = getWhSize(wormholes, signature.type);
if (whShipSize !== undefined && whShipSize !== null) { if (whShipSize) {
await outCommand({ await outCommand({
type: OutCommand.updateConnectionShipSizeType, type: OutCommand.updateConnectionShipSizeType,
data: { data: {
@@ -159,12 +160,6 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
[data, setVisible, wormholes], [data, setVisible, wormholes],
); );
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
}
}, [targetSystemDynamicInfo]);
return ( return (
<Dialog <Dialog
header="Select signature to link" header="Select signature to link"

View File

@@ -1,101 +0,0 @@
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
[PingType.Alert]: 'ALERT',
};
interface SystemPingDialogProps {
systemId: string;
type: PingType;
visible: boolean;
setVisible: (visible: boolean) => void;
}
export const SystemPingDialog = ({ systemId, type, visible, setVisible }: SystemPingDialogProps) => {
const { outCommand } = useMapRootState();
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement>();
const ref = useRef({ message, outCommand, systemId, type });
ref.current = { message, outCommand, systemId, type };
const handleSave = useCallback(() => {
const { message, outCommand, systemId, type } = ref.current;
outCommand({
type: OutCommand.addPing,
data: {
solar_system_id: systemId,
type,
message,
},
});
setVisible(false);
}, [setVisible]);
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
return (
<Dialog
header={
<div className="flex gap-1 text-[13px] items-center text-stone-300">
<div>Ping:{` `}</div>
<div
className={clsx({
['text-cyan-400']: type === PingType.Rally,
})}
>
{PING_TITLES[type]}
</div>
<div className="text-[11px]">in</div> <SystemView systemId={systemId} className="relative top-[1px]" />
</div>
}
visible={visible}
draggable={false}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
if (!visible) {
return;
}
setVisible(false);
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-1">
<label className="text-[11px]" htmlFor="username">
Message
</label>
<InputTextarea
// @ts-ignore
ref={inputRef}
autoResize
rows={3}
cols={30}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
</div>
</div>
</form>
</Dialog>
);
};

View File

@@ -1 +0,0 @@
export * from './SystemPingDialog';

View File

@@ -206,7 +206,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
aria-describedby="temporaryName" aria-describedby="temporaryName"
autoComplete="off" autoComplete="off"
value={temporaryName} value={temporaryName}
maxLength={12} maxLength={10}
onChange={e => setTemporaryName(e.target.value)} onChange={e => setTemporaryName(e.target.value)}
/> />
</IconField> </IconField>

View File

@@ -2,4 +2,3 @@ export * from './Widget';
export * from './SystemSettingsDialog'; export * from './SystemSettingsDialog';
export * from './SystemCustomLabelDialog'; export * from './SystemCustomLabelDialog';
export * from './SystemLinkSignatureDialog'; export * from './SystemLinkSignatureDialog';
export * from './PingsInterface';

View File

@@ -1,14 +1,13 @@
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts'; import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import { import {
CommentsWidget,
LocalCharacters, LocalCharacters,
RoutesWidget,
SystemInfo, SystemInfo,
SystemSignatures, SystemSignatures,
SystemStructures, SystemStructures,
WRoutesPublic,
WRoutesUser,
WSystemKills, WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets'; } from '@/hooks/Mapper/components/mapInterface/widgets';
import { CommentsWidget } from '@/hooks/Mapper/components/mapInterface/widgets/CommentsWidget';
export const CURRENT_WINDOWS_VERSION = 9; export const CURRENT_WINDOWS_VERSION = 9;
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2'; export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
@@ -21,7 +20,6 @@ export enum WidgetsIds {
structures = 'structures', structures = 'structures',
kills = 'kills', kills = 'kills',
comments = 'comments', comments = 'comments',
userRoutes = 'userRoutes',
} }
export const STORED_VISIBLE_WIDGETS_DEFAULT = [ export const STORED_VISIBLE_WIDGETS_DEFAULT = [
@@ -58,14 +56,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
position: { x: 10, y: 530 }, position: { x: 10, y: 530 },
size: { width: 510, height: 200 }, size: { width: 510, height: 200 },
zIndex: 0, zIndex: 0,
content: () => <WRoutesPublic />, content: () => <RoutesWidget />,
},
{
id: WidgetsIds.userRoutes,
position: { x: 10, y: 10 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesUser />,
}, },
{ {
id: WidgetsIds.structures, id: WidgetsIds.structures,
@@ -112,10 +103,6 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes, id: WidgetsIds.routes,
label: 'Routes', label: 'Routes',
}, },
{
id: WidgetsIds.userRoutes,
label: 'User Routes',
},
{ {
id: WidgetsIds.structures, id: WidgetsIds.structures,
label: 'Structures', label: 'Structures',

View File

@@ -1,10 +1,6 @@
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types'; import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
export const sortCharacters = (a: CharacterTypeRaw & WithIsOwnCharacter, b: CharacterTypeRaw & WithIsOwnCharacter) => { export const sortCharacters = (a: CharacterTypeRaw & WithIsOwnCharacter, b: CharacterTypeRaw & WithIsOwnCharacter) => {
if (a.online === b.online) {
return a.name.localeCompare(b.name);
}
if (a.online !== b.online) { if (a.online !== b.online) {
return a.online && !b.online ? -1 : 1; return a.online && !b.online ? -1 : 1;
} }

View File

@@ -6,6 +6,7 @@ import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootP
import { UserPermission } from '@/hooks/Mapper/types/permissions'; import { UserPermission } from '@/hooks/Mapper/types/permissions';
import { LocalCharactersList } from './components/LocalCharactersList'; import { LocalCharactersList } from './components/LocalCharactersList';
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters'; import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
import { LocalCharactersHeader } from './components/LocalCharactersHeader'; import { LocalCharactersHeader } from './components/LocalCharactersHeader';
import classes from './LocalCharacters.module.scss'; import classes from './LocalCharacters.module.scss';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -13,9 +14,9 @@ import clsx from 'clsx';
export const LocalCharacters = () => { export const LocalCharacters = () => {
const { const {
data: { characters, userCharacters, selectedSystems }, data: { characters, userCharacters, selectedSystems },
storedSettings: { settingsLocal, settingsLocalUpdate },
} = useMapRootState(); } = useMapRootState();
const [settings, setSettings] = useLocalCharacterWidgetSettings();
const [systemId] = selectedSystems; const [systemId] = selectedSystems;
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing'); const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]); const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
@@ -30,12 +31,12 @@ export const LocalCharacters = () => {
.map(x => ({ .map(x => ({
...x, ...x,
isOwn: userCharacters.includes(x.eve_id), isOwn: userCharacters.includes(x.eve_id),
compact: settingsLocal.compact, compact: settings.compact,
showShipName: settingsLocal.showShipName, showShipName: settings.showShipName,
})) }))
.sort(sortCharacters); .sort(sortCharacters);
if (!showOffline || !settingsLocal.showOffline) { if (!showOffline || !settings.showOffline) {
return filtered.filter(c => c.online); return filtered.filter(c => c.online);
} }
return filtered; return filtered;
@@ -43,9 +44,9 @@ export const LocalCharacters = () => {
characters, characters,
systemId, systemId,
userCharacters, userCharacters,
settingsLocal.compact, settings.compact,
settingsLocal.showOffline, settings.showOffline,
settingsLocal.showShipName, settings.showShipName,
showOffline, showOffline,
]); ]);
@@ -53,7 +54,7 @@ export const LocalCharacters = () => {
const isNotSelectedSystem = selectedSystems.length !== 1; const isNotSelectedSystem = selectedSystems.length !== 1;
const showList = sorted.length > 0 && selectedSystems.length === 1; const showList = sorted.length > 0 && selectedSystems.length === 1;
const itemTemplate = useLocalCharactersItemTemplate(settingsLocal.showShipName); const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
return ( return (
<Widget <Widget
@@ -62,8 +63,8 @@ export const LocalCharacters = () => {
sortedCount={sorted.length} sortedCount={sorted.length}
showList={showList} showList={showList}
showOffline={showOffline} showOffline={showOffline}
settings={settingsLocal} settings={settings}
setSettings={settingsLocalUpdate} setSettings={setSettings}
/> />
} }
> >
@@ -80,7 +81,7 @@ export const LocalCharacters = () => {
{showList && ( {showList && (
<LocalCharactersList <LocalCharactersList
items={sorted} items={sorted}
itemSize={settingsLocal.compact ? 26 : 41} itemSize={settings.compact ? 26 : 41}
itemTemplate={itemTemplate} itemTemplate={itemTemplate}
containerClassName={clsx( containerClassName={clsx(
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none', 'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',

View File

@@ -1,8 +1,8 @@
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import classes from './LocalCharactersItemTemplate.module.scss'; import classes from './LocalCharactersItemTemplate.module.scss';
import clsx from 'clsx';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps & export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
VirtualScrollerTemplateOptions; VirtualScrollerTemplateOptions;
@@ -22,7 +22,7 @@ export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalC
)} )}
style={{ height: `${options.props.itemSize}px` }} style={{ height: `${options.props.itemSize}px` }}
> >
<CharacterCard showShipName={showShipName} showTicker showShip {...options} /> <CharacterCard showShipName={showShipName} {...options} />
</div> </div>
); );
}; };

View File

@@ -0,0 +1,21 @@
import useLocalStorageState from 'use-local-storage-state';
export interface LocalCharacterWidgetSettings {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
}
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export function useLocalCharacterWidgetSettings() {
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
});
}

View File

@@ -1,31 +1,79 @@
import React, { createContext, forwardRef, useContext } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
RoutesImperativeHandle, import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
RoutesProviderInnerProps,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
type MapProviderProps = { export type RoutesType = {
path_type: 'shortest' | 'secure' | 'insecure';
include_mass_crit: boolean;
include_eol: boolean;
include_frig: boolean;
include_cruise: boolean;
include_thera: boolean;
avoid_wormholes: boolean;
avoid_pochven: boolean;
avoid_edencom: boolean;
avoid_triglavian: boolean;
avoid: number[];
};
interface MapProviderProps {
children: React.ReactNode; children: React.ReactNode;
} & RoutesWidgetProps; }
const RoutesContext = createContext<RoutesProviderInnerProps>({ export const DEFAULT_SETTINGS: RoutesType = {
path_type: 'shortest',
include_mass_crit: true,
include_eol: true,
include_frig: true,
include_cruise: true,
include_thera: true,
avoid_wormholes: false,
avoid_pochven: false,
avoid_edencom: false,
avoid_triglavian: false,
avoid: [],
};
export interface MapContextProps {
update: ContextStoreDataUpdate<RoutesType>;
data: RoutesType;
}
const RoutesContext = createContext<MapContextProps>({
update: () => {}, update: () => {},
// @ts-ignore data: { ...DEFAULT_SETTINGS },
data: {},
}); });
// INFO: this component have imperative handler but now it not using. export const RoutesProvider: React.FC<MapProviderProps> = ({ children }) => {
export const RoutesProvider = forwardRef<RoutesImperativeHandle, MapProviderProps>( const { update, ref } = useContextStore<RoutesType>(
({ children, ...props } /*, ref*/) => { { ...DEFAULT_SETTINGS },
// useImperativeHandle(ref, () => ({})); {
onAfterAUpdate: values => {
localStorage.setItem(SESSION_KEY.routes, JSON.stringify(values));
},
},
);
return <RoutesContext.Provider value={{ ...props /*, loading, setLoading*/ }}>{children}</RoutesContext.Provider>; useEffect(() => {
}, const items = localStorage.getItem(SESSION_KEY.routes);
); if (items) {
RoutesProvider.displayName = 'RoutesProvider'; update(JSON.parse(items));
}
}, [update]);
return (
<RoutesContext.Provider
value={{
update,
data: ref,
}}
>
{children}
</RoutesContext.Provider>
);
};
export const useRouteProvider = () => { export const useRouteProvider = () => {
const context = useContext<RoutesProviderInnerProps>(RoutesContext); const context = useContext<MapContextProps>(RoutesContext);
return context; return context;
}; };

View File

@@ -2,16 +2,16 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { import {
LayoutEventBlocker, LayoutEventBlocker,
LoadingWrapper, SystemViewStandalone,
SystemView,
TooltipPosition, TooltipPosition,
WdCheckbox, WdCheckbox,
WdImgButton, WdImgButton,
} from '@/hooks/Mapper/components/ui-kit'; } from '@/hooks/Mapper/components/ui-kit';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts'; import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { forwardRef, MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getSystemById } from '@/hooks/Mapper/helpers/getSystemById.ts';
import classes from './RoutesWidget.module.scss'; import classes from './RoutesWidget.module.scss';
import { useLoadRoutes } from './hooks';
import { RoutesList } from './RoutesList'; import { RoutesList } from './RoutesList';
import clsx from 'clsx'; import clsx from 'clsx';
import { Route } from '@/hooks/Mapper/types/routes.ts'; import { Route } from '@/hooks/Mapper/types/routes.ts';
@@ -25,10 +25,7 @@ import {
AddSystemDialog, AddSystemDialog,
SearchOnSubmitCallback, SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog'; } from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { import { OutCommand } from '@/hooks/Mapper/types';
RoutesImperativeHandle,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
const sortByDist = (a: Route, b: Route) => { const sortByDist = (a: Route, b: Route) => {
const distA = a.has_connection ? a.systems?.length || 0 : Infinity; const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
@@ -39,31 +36,45 @@ const sortByDist = (a: Route, b: Route) => {
export const RoutesWidgetContent = () => { export const RoutesWidgetContent = () => {
const { const {
data: { selectedSystems, systems, isSubscriptionActive }, data: { selectedSystems, hubs = [], systems, routes },
outCommand,
} = useMapRootState(); } = useMapRootState();
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
const [systemId] = selectedSystems; const [systemId] = selectedSystems;
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] }); const { loading } = useLoadRoutes();
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers();
const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
outCommand,
hubs,
});
const preparedHubs = useMemo(() => {
return hubs.map(x => {
const sys = getSystemById(systems, x.toString());
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hubs, systems, systemStatics, lastUpdateKey]);
const preparedRoutes: Route[] = useMemo(() => { const preparedRoutes: Route[] = useMemo(() => {
return ( return (
routesList?.routes routes?.routes
.sort(sortByDist) .sort(sortByDist)
// .filter(x => x.destination.toString() !== systemId) .filter(x => x.destination.toString() !== systemId)
.map(route => ({ .map(route => ({
...route, ...route,
mapped_systems: mapped_systems:
route.systems?.map(solar_system_id => route.systems?.map(solar_system_id =>
routesList?.systems_static_data.find( routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id, system_static_data => system_static_data.solar_system_id === solar_system_id,
), ),
) ?? [], ) ?? [],
})) ?? [] })) ?? []
); );
}, [routesList?.routes, routesList?.systems_static_data, systemId]); }, [routes?.routes, routes?.systems_static_data, systemId]);
const refData = useRef({ open, loadSystems, preparedRoutes }); const refData = useRef({ open, loadSystems, preparedRoutes });
refData.current = { open, loadSystems, preparedRoutes }; refData.current = { open, loadSystems, preparedRoutes };
@@ -86,13 +97,9 @@ export const RoutesWidgetContent = () => {
[handleClick], [handleClick],
); );
if (isRestricted && !isSubscriptionActive) { if (loading) {
return ( return (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex justify-center items-center select-none text-center">Loading routes...</div>
<span className="select-none text-center text-stone-400/80 text-sm">
User Routes available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
); );
} }
@@ -110,10 +117,12 @@ export const RoutesWidgetContent = () => {
return ( return (
<> <>
<LoadingWrapper loading={loading}> {systemId !== undefined && routes && (
<div className={clsx(classes.RoutesGrid, 'px-2 py-2')}> <div className={clsx(classes.RoutesGrid, 'px-2 py-2')}>
{preparedRoutes.map(route => { {preparedRoutes.map(route => {
// TODO do not delete this console log const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
// TODO do not delte this console log
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('JOipP', `Check sys [${route.destination}]:`, sys); // console.log('JOipP', `Check sys [${route.destination}]:`, sys);
@@ -123,19 +132,15 @@ export const RoutesWidgetContent = () => {
<WdImgButton <WdImgButton
className={clsx(PrimeIcons.BARS, classes.RemoveBtn)} className={clsx(PrimeIcons.BARS, classes.RemoveBtn)}
onClick={e => handleClick(e, route.destination.toString())} onClick={e => handleClick(e, route.destination.toString())}
tooltip={{ tooltip={{ content: 'Click here to open system menu', position: TooltipPosition.top, offset: 10 }}
content: 'Click here to open system menu',
position: TooltipPosition.top,
offset: 10,
}}
/> />
<SystemView <SystemViewStandalone
systemId={route.destination.toString()} key={route.destination}
className={clsx('select-none text-center cursor-context-menu')} className={clsx('select-none text-center cursor-context-menu')}
hideRegion hideRegion
compact compact
showCustomName {...sys}
/> />
</div> </div>
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div> <div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
@@ -146,7 +151,7 @@ export const RoutesWidgetContent = () => {
); );
})} })}
</div> </div>
</LoadingWrapper> )}
<ContextMenuSystemInfo <ContextMenuSystemInfo
hubs={hubs} hubs={hubs}
@@ -160,13 +165,15 @@ export const RoutesWidgetContent = () => {
); );
}; };
type RoutesWidgetCompProps = { export const RoutesWidgetComp = () => {
title: ReactNode | string;
};
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false); const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider(); const { data, update } = useRouteProvider();
const {
data: { hubs = [] },
outCommand,
} = useMapRootState();
const preparedHubs = useMemo(() => hubs.map(x => parseInt(x)), [hubs]);
const isSecure = data.path_type === 'secure'; const isSecure = data.path_type === 'secure';
const handleSecureChange = useCallback(() => { const handleSecureChange = useCallback(() => {
@@ -183,15 +190,24 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const onAddSystem = useCallback(() => setOpenAddSystem(true), []); const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback( const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand(item.value.toString()), async item => {
[addHubCommand], if (preparedHubs.includes(item.value)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: item.value },
});
},
[hubs, outCommand],
); );
return ( return (
<Widget <Widget
label={ label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}> <div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">{title}</span> <span className="select-none">Routes</span>
<LayoutEventBlocker className="flex items-center gap-2"> <LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton <WdImgButton
className={PrimeIcons.PLUS_CIRCLE} className={PrimeIcons.PLUS_CIRCLE}
@@ -215,7 +231,6 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
className={PrimeIcons.SLIDERS_H} className={PrimeIcons.SLIDERS_H}
onClick={() => setRouteSettingsVisible(true)} onClick={() => setRouteSettingsVisible(true)}
tooltip={{ tooltip={{
position: TooltipPosition.top,
content: 'Click here to open Routes settings', content: 'Click here to open Routes settings',
}} }}
/> />
@@ -236,13 +251,10 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
); );
}; };
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>( export const RoutesWidget = () => {
({ title, ...props }, ref) => { return (
return ( <RoutesProvider>
<RoutesProvider {...props} ref={ref}> <RoutesWidgetComp />
<RoutesWidgetComp title={title} /> </RoutesProvider>
</RoutesProvider> );
); };
},
);
RoutesWidget.displayName = 'RoutesWidget';

View File

@@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts'; import {
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts'; RoutesType,
import { RoutesList } from '@/hooks/Mapper/types/routes.ts'; useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
function usePrevious<T>(value: T): T | undefined { function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>(); const ref = useRef<T>();
@@ -14,25 +16,13 @@ function usePrevious<T>(value: T): T | undefined {
return ref.current; return ref.current;
} }
type UseLoadRoutesProps = { export const useLoadRoutes = () => {
loadRoutesCommand: LoadRoutesCommand;
hubs: string[];
routesList: RoutesList | undefined;
data: RoutesType;
deps?: unknown[];
};
export const useLoadRoutes = ({
data: routesSettings,
loadRoutesCommand,
hubs,
routesList,
deps = [],
}: UseLoadRoutesProps) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { data: routesSettings } = useRouteProvider();
const { const {
data: { selectedSystems, systems, connections }, outCommand,
data: { selectedSystems, hubs, systems, connections },
} = useMapRootState(); } = useMapRootState();
const prevSys = usePrevious(systems); const prevSys = usePrevious(systems);
@@ -41,16 +31,17 @@ export const useLoadRoutes = ({
const loadRoutes = useCallback( const loadRoutes = useCallback(
(systemId: string, routesSettings: RoutesType) => { (systemId: string, routesSettings: RoutesType) => {
loadRoutesCommand(systemId, routesSettings); outCommand({
setLoading(true); type: OutCommand.getRoutes,
data: {
system_id: systemId,
routes_settings: routesSettings,
},
});
}, },
[loadRoutesCommand], [outCommand],
); );
useEffect(() => {
setLoading(false);
}, [routesList]);
useEffect(() => { useEffect(() => {
if (selectedSystems.length !== 1) { if (selectedSystems.length !== 1) {
return; return;
@@ -70,8 +61,7 @@ export const useLoadRoutes = ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
.map(x => routesSettings[x]), .map(x => routesSettings[x]),
...deps,
]); ]);
return { loading, loadRoutes, setLoading }; return { loading, loadRoutes };
}; };

View File

@@ -1,24 +0,0 @@
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
export type LoadRoutesCommand = (systemId: string, routesSettings: RoutesType) => Promise<void>;
export type AddHubCommand = (systemId: string) => Promise<void>;
export type ToggleHubCommand = (systemId: string) => Promise<void>;
export type RoutesWidgetProps = {
data: RoutesType;
update: (d: RoutesType) => void;
hubs: string[];
routesList: RoutesList | undefined;
loading: boolean;
addHubCommand: AddHubCommand;
toggleHubCommand: ToggleHubCommand;
isRestricted?: boolean;
};
export type RoutesProviderInnerProps = RoutesWidgetProps;
export type RoutesImperativeHandle = {
stopLoading: () => void;
};

View File

@@ -1,9 +1,9 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit'; import { LayoutEventBlocker, SystemView, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent'; import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api'; import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react'; import { useState, useCallback } from 'react';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx'; import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons'; import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic'; import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
@@ -42,7 +42,7 @@ export const SystemInfo = () => {
<WdImgButton <WdImgButton
className="pi pi-pen-to-square" className="pi pi-pen-to-square"
onClick={() => setVisible(true)} onClick={() => setVisible(true)}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }} tooltip={{ content: 'Edit system name and description' }}
/> />
</LayoutEventBlocker> </LayoutEventBlocker>
</div> </div>

View File

@@ -10,7 +10,7 @@ export interface SignatureViewProps {
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => { export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
const isWormhole = signature?.group === SignatureGroup.Wormhole; const isWormhole = signature?.group === SignatureGroup.Wormhole;
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id; const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature; const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
const characterName = signature.character_name || 'Unknown character'; const characterName = signature.character_name || 'Unknown character';
return ( return (

View File

@@ -19,7 +19,7 @@ export type HeaderProps = {
lazyDeleteValue: boolean; lazyDeleteValue: boolean;
onLazyDeleteChange: (checked: boolean) => void; onLazyDeleteChange: (checked: boolean) => void;
pendingCount: number; pendingCount: number;
undoCountdown?: number; pendingTimeRemaining?: number; // Time remaining in ms
onUndoClick: () => void; onUndoClick: () => void;
onSettingsClick: () => void; onSettingsClick: () => void;
}; };
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
lazyDeleteValue, lazyDeleteValue,
onLazyDeleteChange, onLazyDeleteChange,
pendingCount, pendingCount,
undoCountdown, pendingTimeRemaining,
onUndoClick, onUndoClick,
onSettingsClick, onSettingsClick,
}: HeaderProps) => { }: HeaderProps) => {
@@ -43,6 +43,13 @@ export const SystemSignaturesHeader = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH); const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
// Format time remaining as seconds
const formatTimeRemaining = () => {
if (!pendingTimeRemaining) return '';
const seconds = Math.ceil(pendingTimeRemaining / 1000);
return ` (${seconds}s remaining)`;
};
return ( return (
<div ref={containerRef} className="w-full"> <div ref={containerRef} className="w-full">
<div className="flex justify-between items-center text-xs w-full h-full"> <div className="flex justify-between items-center text-xs w-full h-full">
@@ -71,9 +78,7 @@ export const SystemSignaturesHeader = ({
<WdImgButton <WdImgButton
className={PrimeIcons.UNDO} className={PrimeIcons.UNDO}
style={{ color: 'red' }} style={{ color: 'red' }}
tooltip={{ tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? `${undoCountdown}s left` : ''}`,
}}
onClick={onUndoClick} onClick={onUndoClick}
/> />
)} )}

View File

@@ -8,8 +8,8 @@ import {
Setting, Setting,
SettingsTypes, SettingsTypes,
SIGNATURE_SETTINGS, SIGNATURE_SETTINGS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
interface SystemSignatureSettingsDialogProps { interface SystemSignatureSettingsDialogProps {
settings: SignatureSettingsType; settings: SignatureSettingsType;

View File

@@ -1,205 +1,139 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent'; import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog'; import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { SystemSignaturesHeader } from './SystemSignatureHeader'; import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey'; import useLocalStorageState from 'use-local-storage-state';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; import {
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers'; SETTINGS_KEYS,
import { ExtendedSystemSignature } from '@/hooks/Mapper/types'; SETTINGS_VALUES,
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures'; SIGNATURE_DELETION_TIMEOUTS,
SIGNATURE_SETTING_STORE_KEY,
/** SIGNATURE_WINDOW_ID,
* Custom hook for managing pending signature deletions and undo countdown. SIGNATURES_DELETION_TIMING,
*/ SignatureSettingsType,
function useSignatureUndo( } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
systemId: string | undefined, import { calculateTimeRemaining } from './helpers';
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => { export const SystemSignatures = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0); const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
const undoPendingFnRef = useRef<() => void>(() => {});
const { const {
data: { selectedSystems }, data: { selectedSystems },
outCommand,
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState(); } = useMapRootState();
const [systemId] = selectedSystems; const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]); defaultValue: SETTINGS_VALUES,
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
settingsSignatures,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
}); });
const handleCountChange = useCallback((count: number) => { const handleSigCountChange = useCallback((count: number) => {
setSigCount(count); setSigCount(count);
}, []); }, []);
const handleSettingsSave = useCallback( const [systemId] = selectedSystems;
(newSettings: SignatureSettingsType) => { const isNotSelectedSystem = selectedSystems.length !== 1;
settingsSignaturesUpdate(newSettings);
setVisible(false); const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
}, []);
useHotkey(true, ['z'], event => {
if (pendingSigs.length > 0) {
event.preventDefault();
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const handlePendingChange = useCallback(
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
setPendingSigs(() => {
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
});
undoPendingFnRef.current = newUndo;
}, },
[settingsSignaturesUpdate], [],
); );
const handleLazyDeleteToggle = useCallback( // Calculate the minimum time remaining for any pending signature
(value: boolean) => { useEffect(() => {
settingsSignaturesUpdate(prev => ({ if (pendingSigs.length === 0) {
...prev, setPendingTimeRemaining(undefined);
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value, return;
})); }
},
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setVisible(true), []); const calculate = () => {
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
};
calculate();
const interval = setInterval(calculate, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
return ( return (
<Widget <Widget
label={ label={
<SystemSignaturesHeader <SystemSignaturesHeader
sigCount={sigCount} sigCount={sigCount}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean} lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingIds.size} pendingCount={pendingSigs.length}
undoCountdown={countdown} pendingTimeRemaining={pendingTimeRemaining}
onLazyDeleteChange={handleLazyDeleteToggle} onLazyDeleteChange={handleLazyDeleteChange}
onUndoClick={handleUndo} onUndoClick={handleUndoClick}
onSettingsClick={openSettings} onSettingsClick={handleSettingsButtonClick}
/> />
} }
windowId={SIGNATURE_WINDOW_ID} windowId={SIGNATURE_WINDOW_ID}
> >
{!isSystemSelected ? ( {isNotSelectedSystem ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm"> <div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected System is not selected
</div> </div>
) : ( ) : (
<SystemSignaturesContent <SystemSignaturesContent
systemId={systemId} systemId={systemId}
settings={settingsSignatures} settings={currentSettings}
deletedSignatures={deletedSignatures} deletionTiming={
onLazyDeleteChange={handleLazyDeleteToggle} SIGNATURE_DELETION_TIMEOUTS[
onCountChange={handleCountChange} (currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
onSignatureDeleted={addDeleted} SIGNATURES_DELETION_TIMING.DEFAULT
] as number
}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}
/> />
)} )}
{visible && ( {visible && (
<SystemSignatureSettingsDialog <SystemSignatureSettingsDialog
settings={settingsSignatures} settings={currentSettings}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}
onSave={handleSettingsSave} onSave={handleSettingsChange}
/> />
)} )}
</Widget> </Widget>

View File

@@ -8,6 +8,7 @@ import {
SortOrder, SortOrder,
} from 'primereact/datatable'; } from 'primereact/datatable';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView'; import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
import { import {
@@ -16,6 +17,9 @@ import {
GROUPS_LIST, GROUPS_LIST,
MEDIUM_MAX_WIDTH, MEDIUM_MAX_WIDTH,
OTHER_COLUMNS_WIDTH, OTHER_COLUMNS_WIDTH,
SETTINGS_KEYS,
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants'; } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings'; import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit'; import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
@@ -32,11 +36,19 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth'; import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { getSignatureRowClass } from '../helpers/rowStyles'; import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData'; import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig); const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
type SystemSignaturesSortSettings = {
sortField: string;
sortOrder: SortOrder;
};
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
sortField: 'inserted_at',
sortOrder: -1,
};
interface SystemSignaturesContentProps { interface SystemSignaturesContentProps {
systemId: string; systemId: string;
settings: SignatureSettingsType; settings: SignatureSettingsType;
@@ -45,9 +57,12 @@ interface SystemSignaturesContentProps {
onSelect?: (signature: SystemSignature) => void; onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void; onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void; onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
deletionTiming?: number;
filterSignature?: (signature: SystemSignature) => boolean; filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
} }
export const SystemSignaturesContent = ({ export const SystemSignaturesContent = ({
@@ -58,19 +73,15 @@ export const SystemSignaturesContent = ({
onSelect, onSelect,
onLazyDeleteChange, onLazyDeleteChange,
onCountChange, onCountChange,
onPendingChange,
deletionTiming,
filterSignature, filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => { }: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null); const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false); const [showSignatureSettings, setShowSignatureSettings] = useState(false);
const [nameColumnWidth, setNameColumnWidth] = useState('auto'); const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null); const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
const {
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
const tableRef = useRef<HTMLDivElement>(null); const tableRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<WdTooltipHandlers>(null); const tooltipRef = useRef<WdTooltipHandlers>(null);
@@ -79,21 +90,20 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard(); const { clipboardContent, setClipboardContent } = useClipboard();
const { const [sortSettings, setSortSettings] = useLocalStorageState<{ sortField: string; sortOrder: SortOrder }>(
signatures, 'window:signatures:sort',
selectedSignatures, { defaultValue: SORT_DEFAULT_VALUES },
setSelectedSignatures, );
handleDeleteSelected,
handleSelectAll, const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
handlePaste, useSystemSignaturesData({
hasUnsupportedLanguage, systemId,
} = useSystemSignaturesData({ settings,
systemId, onCountChange,
settings, onPendingChange,
onCountChange, onLazyDeleteChange,
onLazyDeleteChange, deletionTiming,
onSignatureDeleted, });
});
useEffect(() => { useEffect(() => {
if (selectable) return; if (selectable) return;
@@ -115,8 +125,6 @@ export const SystemSignaturesContent = ({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
// Delete key should always immediately delete, never show pending deletions
handleDeleteSelected(); handleDeleteSelected();
}); });
@@ -145,16 +153,9 @@ export const SystemSignaturesContent = ({
const handleSelectSignatures = useCallback( const handleSelectSignatures = useCallback(
(e: { value: SystemSignature[] }) => { (e: { value: SystemSignature[] }) => {
// Filter out deleted signatures from selection selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
const selectableSignatures = e.value.filter(
sig => !deletedSignatures.some(deleted => deleted.eve_id === sig.eve_id),
);
selectable
? onSelect?.(selectableSignatures[0])
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
}, },
[onSelect, selectable, setSelectedSignatures, deletedSignatures], [selectable],
); );
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo( const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
@@ -168,11 +169,7 @@ export const SystemSignaturesContent = ({
); );
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => { const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
// Get the set of deleted signature IDs for quick lookup return signatures.filter(sig => {
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
if (filterSignature && !filterSignature(sig)) { if (filterSignature && !filterSignature(sig)) {
return false; return false;
} }
@@ -191,37 +188,15 @@ export const SystemSignaturesContent = ({
x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS], x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS],
); );
const mappedGroup = getGroupIdByRawGroup(sig.group); return enabledGroups.includes(getGroupIdByRawGroup(sig.group));
if (!mappedGroup) {
return true; // If we can't determine the group, still show it
}
return enabledGroups.includes(mappedGroup);
} }
return true; return true;
} }
return settings[sig.kind] as boolean; return settings[sig.kind];
};
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
}); });
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => { const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature); setHoveredSignature(e.data as SystemSignature);
@@ -233,8 +208,8 @@ export const SystemSignaturesContent = ({
tooltipRef.current?.hide(); tooltipRef.current?.hide();
}, []); }, []);
const refVars = useRef({ settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate }); const refVars = useRef({ settings, selectedSignatures, setSortSettings });
refVars.current = { settings, selectedSignatures, settingsSignatures, settingsSignaturesUpdate }; refVars.current = { settings, selectedSignatures, setSortSettings };
// @ts-ignore // @ts-ignore
const getRowClassName = useCallback(rowData => { const getRowClassName = useCallback(rowData => {
@@ -250,12 +225,7 @@ export const SystemSignaturesContent = ({
}, []); }, []);
const handleSortSettings = useCallback( const handleSortSettings = useCallback(
(e: DataTableStateEvent) => (e: DataTableStateEvent) => refVars.current.setSortSettings({ sortField: e.sortField, sortOrder: e.sortOrder }),
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[], [],
); );
@@ -266,122 +236,113 @@ export const SystemSignaturesContent = ({
No signatures No signatures
</div> </div>
) : ( ) : (
<> <DataTable
{hasUnsupportedLanguage && ( value={filteredSignatures}
<div className="w-full flex justify-center items-center text-amber-500 text-xs p-1 bg-amber-950/20 border-b border-amber-800/30"> size="small"
<i className={PrimeIcons.EXCLAMATION_TRIANGLE + ' mr-1'} /> selectionMode="multiple"
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature selection={selectedSignatures}
details. metaKeySelection
</div> onSelectionChange={handleSelectSignatures}
)} dataKey="eve_id"
<DataTable className="w-full select-none"
value={filteredSignatures} resizableColumns={false}
size="small" rowHover
selectionMode="multiple" selectAll
selection={selectedSignatures} onRowDoubleClick={handleRowClick}
metaKeySelection sortField={sortSettings.sortField}
onSelectionChange={handleSelectSignatures} sortOrder={sortSettings.sortOrder}
dataKey="eve_id" onSort={handleSortSettings}
className="w-full select-none" onRowMouseEnter={onRowMouseEnter}
resizableColumns={false} onRowMouseLeave={onRowMouseLeave}
rowHover // @ts-ignore
selectAll rowClassName={getRowClassName}
onRowDoubleClick={handleRowClick} >
sortField={settingsSignatures[SETTINGS_KEYS.SORT_FIELD] as string} <Column
sortOrder={settingsSignatures[SETTINGS_KEYS.SORT_ORDER] as SortOrder} field="icon"
onSort={handleSortSettings} header=""
onRowMouseEnter={onRowMouseEnter} body={renderColIcon}
onRowMouseLeave={onRowMouseLeave} bodyClassName="p-0 px-1"
// @ts-ignore style={{ maxWidth: 26, minWidth: 26, width: 26 }}
rowClassName={getRowClassName} />
> <Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
<Column
field="info"
header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column <Column
field="icon" field="description"
header="" header="Description"
body={renderColIcon}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
/>
<Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact} hidden={isCompact}
body={renderDescription}
sortable sortable
/> />
)}
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showUpdatedColumn && (
<Column <Column
field="info" field="updated_at"
header="Info" header="Updated"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={isCompact}
body={renderDescription}
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
dataType="date" dataType="date"
body={renderAddedTimeLeft} body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }} style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable sortable
/> />
{showUpdatedColumn && ( )}
<Column
field="updated_at"
header="Updated"
dataType="date"
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
{showCharacterColumn && ( {showCharacterColumn && (
<Column <Column
field="character_name" field="character_name"
header="Character" header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable sortable
></Column> ></Column>
)} )}
{!selectable && ( {!selectable && (
<Column <Column
header="" header=""
body={() => ( body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]"> <div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature"> <WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} /> <span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper> </WdTooltipWrapper>
</div> </div>
)} )}
style={{ maxWidth: 26, minWidth: 26, width: 26 }} style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2" bodyClassName="p-0 pl-1 pr-2"
/> />
)} )}
</DataTable> </DataTable>
</>
)} )}
<WdTooltip <WdTooltip

View File

@@ -1,17 +1,12 @@
import { import {
GroupType, GroupType,
SignatureGroup, SignatureGroup,
SignatureGroupDE,
SignatureGroupENG, SignatureGroupENG,
SignatureGroupFR,
SignatureGroupRU, SignatureGroupRU,
SignatureKind, SignatureKind,
SignatureKindDE,
SignatureKindENG, SignatureKindENG,
SignatureKindFR,
SignatureKindRU, SignatureKindRU,
} from '@/hooks/Mapper/types'; } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export const TIME_ONE_MINUTE = 1000 * 60; export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10; export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
@@ -45,73 +40,99 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 }, [SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
}; };
export const LANGUAGE_GROUP_MAPPINGS = { export const MAPPING_GROUP_TO_ENG = {
EN: { // ENGLISH
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite, [SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite, [SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite, [SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite, [SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite, [SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole, [SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature, [SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
},
RU: { // RUSSIAN
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite, [SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite, [SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite, [SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite, [SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite, [SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole, [SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature, [SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
},
FR: {
[SignatureGroupFR.GasSite]: SignatureGroup.GasSite,
[SignatureGroupFR.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupFR.DataSite]: SignatureGroup.DataSite,
[SignatureGroupFR.OreSite]: SignatureGroup.OreSite,
[SignatureGroupFR.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupFR.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupFR.CosmicSignature]: SignatureGroup.CosmicSignature,
},
DE: {
[SignatureGroupDE.GasSite]: SignatureGroup.GasSite,
[SignatureGroupDE.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupDE.DataSite]: SignatureGroup.DataSite,
[SignatureGroupDE.OreSite]: SignatureGroup.OreSite,
[SignatureGroupDE.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupDE.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupDE.CosmicSignature]: SignatureGroup.CosmicSignature,
},
}; };
// Flatten the structure for backward compatibility export const MAPPING_TYPE_TO_ENG = {
export const MAPPING_GROUP_TO_ENG: Record<string, SignatureGroup> = (() => { // ENGLISH
const flattened: Record<string, SignatureGroup> = {}; [SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
for (const [, mappings] of Object.entries(LANGUAGE_GROUP_MAPPINGS)) { [SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
Object.assign(flattened, mappings); [SignatureKindENG.Structure]: SignatureKind.Structure,
} [SignatureKindENG.Ship]: SignatureKind.Ship,
return flattened; [SignatureKindENG.Deployable]: SignatureKind.Deployable,
})(); [SignatureKindENG.Drone]: SignatureKind.Drone,
export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined => { // RUSSIAN
return MAPPING_GROUP_TO_ENG[val] || undefined; [SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
}; };
export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup];
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
export enum SETTINGS_KEYS {
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
COSMIC_SIGNATURE = SignatureKind.CosmicSignature,
DEPLOYABLE = SignatureKind.Deployable,
STRUCTURE = SignatureKind.Structure,
STARBASE = SignatureKind.Starbase,
SHIP = SignatureKind.Ship,
DRONE = SignatureKind.Drone,
// From SignatureGroup
WORMHOLE = SignatureGroup.Wormhole,
RELIC_SITE = SignatureGroup.RelicSite,
DATA_SITE = SignatureGroup.DataSite,
ORE_SITE = SignatureGroup.OreSite,
GAS_SITE = SignatureGroup.GasSite,
COMBAT_SITE = SignatureGroup.CombatSite,
}
export enum SettingsTypes { export enum SettingsTypes {
flag, flag,
dropdown, dropdown,
} }
export type SignatureSettingsType = { [key in SETTINGS_KEYS]?: unknown };
export type Setting = { export type Setting = {
key: SETTINGS_KEYS; key: SETTINGS_KEYS;
name: string; name: string;
type: SettingsTypes; type: SettingsTypes;
isSeparator?: boolean; isSeparator?: boolean;
options?: { label: string; value: number | string | boolean }[]; options?: { label: string; value: any }[];
}; };
// Now use a stricter type: every timing key maps to a number export enum SIGNATURES_DELETION_TIMING {
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>; IMMEDIATE,
DEFAULT,
EXTENDED,
}
export type SignatureDeletionTimingType = { [key in SIGNATURES_DELETION_TIMING]?: unknown };
export const SIGNATURE_SETTINGS = { export const SIGNATURE_SETTINGS = {
filterFlags: [ filterFlags: [
@@ -156,73 +177,34 @@ export const SIGNATURE_SETTINGS = {
], ],
}; };
// Now this map is strongly typed as “number” for each timing enum export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: true,
[SETTINGS_KEYS.KEEP_LAZY_DELETE]: false,
[SETTINGS_KEYS.DELETION_TIMING]: SIGNATURES_DELETION_TIMING.DEFAULT,
[SETTINGS_KEYS.COLOR_BY_TYPE]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT]: true,
[SETTINGS_KEYS.COSMIC_ANOMALY]: true,
[SETTINGS_KEYS.COSMIC_SIGNATURE]: true,
[SETTINGS_KEYS.DEPLOYABLE]: true,
[SETTINGS_KEYS.STRUCTURE]: true,
[SETTINGS_KEYS.STARBASE]: true,
[SETTINGS_KEYS.SHIP]: true,
[SETTINGS_KEYS.DRONE]: true,
[SETTINGS_KEYS.WORMHOLE]: true,
[SETTINGS_KEYS.RELIC_SITE]: true,
[SETTINGS_KEYS.DATA_SITE]: true,
[SETTINGS_KEYS.ORE_SITE]: true,
[SETTINGS_KEYS.GAS_SITE]: true,
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = { export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000, [SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000, [SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
}; };
/**
* Helper function to extract the deletion timeout in milliseconds from settings
*/
export function getDeletionTimeoutMs(settings: SignatureSettingsType): number {
const raw = settings[SETTINGS_KEYS.DELETION_TIMING];
const timing =
raw && typeof raw === 'object' && 'value' in raw
? (raw as { value: SIGNATURES_DELETION_TIMING }).value
: (raw as SIGNATURES_DELETION_TIMING | undefined);
const validTiming = typeof timing === 'number' ? timing : SIGNATURES_DELETION_TIMING.DEFAULT;
return SIGNATURE_DELETION_TIMEOUTS[validTiming];
}
// Replace the flat structure with a nested structure by language
export const LANGUAGE_TYPE_MAPPINGS = {
EN: {
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindENG.Structure]: SignatureKind.Structure,
[SignatureKindENG.Ship]: SignatureKind.Ship,
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
[SignatureKindENG.Drone]: SignatureKind.Drone,
[SignatureKindENG.Starbase]: SignatureKind.Starbase,
},
RU: {
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
[SignatureKindRU.Starbase]: SignatureKind.Starbase,
},
FR: {
[SignatureKindFR.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindFR.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindFR.Structure]: SignatureKind.Structure,
[SignatureKindFR.Ship]: SignatureKind.Ship,
[SignatureKindFR.Deployable]: SignatureKind.Deployable,
[SignatureKindFR.Drone]: SignatureKind.Drone,
[SignatureKindFR.Starbase]: SignatureKind.Starbase,
},
DE: {
[SignatureKindDE.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindDE.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindDE.Structure]: SignatureKind.Structure,
[SignatureKindDE.Ship]: SignatureKind.Ship,
[SignatureKindDE.Deployable]: SignatureKind.Deployable,
[SignatureKindDE.Drone]: SignatureKind.Drone,
[SignatureKindDE.Starbase]: SignatureKind.Starbase,
},
};
// Flatten the structure for backward compatibility
export const MAPPING_TYPE_TO_ENG: Record<string, SignatureKind> = (() => {
const flattened: Record<string, SignatureKind> = {};
for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) {
Object.assign(flattened, mappings);
}
return flattened;
})();

View File

@@ -1,5 +1,5 @@
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SystemSignature } from '@/hooks/Mapper/types'; import { SystemSignature } from '@/hooks/Mapper/types';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { getState } from './getState'; import { getState } from './getState';
/** /**
@@ -22,7 +22,6 @@ export const getActualSigs = (
oldSignatures.forEach(oldSig => { oldSignatures.forEach(oldSig => {
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id); const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
if (newSig) { if (newSig) {
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig); const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
const mergedSig = { ...oldSig }; const mergedSig = { ...oldSig };

View File

@@ -1,52 +0,0 @@
import { getState } from './getState';
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
describe('getState', () => {
const mockSignaturesMatch: string[] = []; // This parameter is not used in the function
it('should return 0 if group is undefined', () => {
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: undefined } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
});
it('should return 0 if group is CosmicSignature', () => {
const newSig: SystemSignature = { id: '1', name: 'Test Sig', group: SignatureGroup.CosmicSignature } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(0);
});
it('should return 1 if group is not CosmicSignature and name is undefined', () => {
const newSig: SystemSignature = { id: '1', name: undefined, group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 1 if group is not CosmicSignature and name is empty', () => {
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 1 if group is not CosmicSignature and name is UNKNOWN_SIGNATURE_NAME', () => {
const newSig: SystemSignature = { id: '1', name: UNKNOWN_SIGNATURE_NAME, group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
it('should return 2 if group is not CosmicSignature and name is a non-empty string', () => {
const newSig: SystemSignature = { id: '1', name: 'Custom Name', group: SignatureGroup.Wormhole } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
});
// According to the current implementation, state = -1 is unreachable
// because the conditions for 0, 1, and 2 cover all possibilities for the given inputs.
// If the logic of getState were to change to make -1 possible, a test case should be added here.
// For now, we can test a scenario that should lead to one of the valid states,
// for example, if group is something other than CosmicSignature and name is valid.
it('should handle other valid signature groups correctly, leading to state 2 with a valid name', () => {
const newSig: SystemSignature = { id: '1', name: 'Combat Site', group: SignatureGroup.CombatSite } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(2);
});
it('should handle other valid signature groups correctly, leading to state 1 with an empty name', () => {
const newSig: SystemSignature = { id: '1', name: '', group: SignatureGroup.DataSite } as SystemSignature;
expect(getState(mockSignaturesMatch, newSig)).toBe(1);
});
});

View File

@@ -1,15 +1,13 @@
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers'; import { SystemSignature } from '@/hooks/Mapper/types';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
export const getState = (_: string[], newSig: SystemSignature) => { export const getState = (_: string[], newSig: SystemSignature) => {
let state = -1; let state = -1;
if (!newSig.group || newSig.group === SignatureGroup.CosmicSignature) { if (!newSig.group) {
state = 0; state = 0;
} else if (!newSig.name || newSig.name === '' || newSig.name === UNKNOWN_SIGNATURE_NAME) { } else if (!newSig.name || newSig.name === '') {
state = 1; state = 1;
} else if (newSig.name !== '') { } else if (newSig.name !== '') {
state = 2; state = 2;
} }
return state; return state;
}; };

View File

@@ -1,5 +1,5 @@
import { SignatureSettingsType } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types'; import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export interface UseSystemSignaturesDataProps { export interface UseSystemSignaturesDataProps {
systemId: string; systemId: string;

View File

@@ -1,18 +1,23 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef, useEffect } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers'; import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload } from '../helpers'; import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
import { UsePendingDeletionParams } from './types'; import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types'; import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({ export function usePendingDeletions({
systemId, systemId,
setSignatures, setSignatures,
deletionTiming,
onPendingChange, onPendingChange,
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) { }: UsePendingDeletionParams) {
const { outCommand } = useMapRootState(); const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({}); const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processRemovedSignatures = useCallback( const processRemovedSignatures = useCallback(
async ( async (
removed: ExtendedSystemSignature[], removed: ExtendedSystemSignature[],
@@ -20,15 +25,63 @@ export function usePendingDeletions({
updated: ExtendedSystemSignature[], updated: ExtendedSystemSignature[],
) => { ) => {
if (!removed.length) return; if (!removed.length) return;
await outCommand({
type: OutCommand.updateSignatures, // If deletion timing is 0, immediately delete without pending state
data: prepareUpdatePayload(systemId, added, updated, removed), if (finalDuration === 0) {
}); await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
return;
}
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingUntil: now + finalDuration,
}));
pendingDeletionMapRef.current = {
...pendingDeletionMapRef.current,
...processedRemoved.reduce((acc: any, sig) => {
acc[sig.eve_id] = sig;
return acc;
}, {}),
};
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
}
return sig;
}),
);
scheduleLazyTimers(
processedRemoved,
pendingDeletionMapRef,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
delete pendingDeletionMapRef.current[sig.eve_id];
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
},
finalDuration,
);
}, },
[systemId, outCommand], [systemId, outCommand, finalDuration],
); );
const clearPendingDeletions = useCallback(() => { const clearPendingDeletions = useCallback(() => {
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingDeletionMapRef.current = {}; pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x))); setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions); onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);

View File

@@ -1,17 +1,16 @@
import { useMapEventListener } from '@/hooks/Mapper/events';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref'; import useRefState from 'react-usestateref';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers'; import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching'; import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts'; import { usePendingDeletions } from './usePendingDeletions';
import { UseSystemSignaturesDataProps } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SETTINGS_KEYS } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
export const useSystemSignaturesData = ({ export const useSystemSignaturesData = ({
systemId, systemId,
@@ -19,18 +18,16 @@ export const useSystemSignaturesData = ({
onCountChange, onCountChange,
onPendingChange, onPendingChange,
onLazyDeleteChange, onLazyDeleteChange,
onSignatureDeleted, deletionTiming,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & { }: UseSystemSignaturesDataProps) => {
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
const { outCommand } = useMapRootState(); const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]); const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]); const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({ const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId, systemId,
setSignatures, setSignatures,
deletionTiming,
onPendingChange, onPendingChange,
}); });
@@ -45,42 +42,19 @@ export const useSystemSignaturesData = ({
async (clipboardString: string) => { async (clipboardString: string) => {
const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean; const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
// Parse the incoming signatures
const incomingSignatures = parseSignatures( const incomingSignatures = parseSignatures(
clipboardString, clipboardString,
Object.keys(settings).filter(skey => skey in SignatureKind), Object.keys(settings).filter(skey => skey in SignatureKind),
) as ExtendedSystemSignature[]; ) as ExtendedSystemSignature[];
if (incomingSignatures.length === 0) {
return;
}
// Check if any signatures might be using unsupported languages
// This is a basic heuristic: if we have signatures where the original group wasn't mapped
const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== '');
const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length;
// If we detected valid IDs but got fewer parsed signatures, we might have language issues
if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) {
setHasUnsupportedLanguage(true);
} else {
setHasUnsupportedLanguage(false);
}
const currentNonPending = lazyDeleteValue const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion) ? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition); : signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false); const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
if (removed.length > 0) { if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated); await processRemovedSignatures(removed, added, updated);
// Show pending deletions if lazy deletion is enabled
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
if (onSignatureDeleted && lazyDeleteValue) {
onSignatureDeleted(removed);
}
} }
if (updated.length !== 0 || added.length !== 0) { if (updated.length !== 0 || added.length !== 0) {
@@ -100,23 +74,17 @@ export const useSystemSignaturesData = ({
onLazyDeleteChange?.(false); onLazyDeleteChange?.(false);
} }
}, },
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted], [settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
); );
const handleDeleteSelected = useCallback(async () => { const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return; if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id); const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id)); const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true); await handleUpdateSignatures(finalList, false, true);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]); setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]); }, [selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => { const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures); setSelectedSignatures(signatures);
@@ -147,12 +115,11 @@ export const useSystemSignaturesData = ({
}, [signatures]); }, [signatures]);
return { return {
signatures: signatures.filter(sig => !sig.deleted), signatures,
selectedSignatures, selectedSignatures,
setSelectedSignatures, setSelectedSignatures,
handleDeleteSelected, handleDeleteSelected,
handleSelectAll, handleSelectAll,
handlePaste, handlePaste,
hasUnsupportedLanguage,
}; };
}; };

View File

@@ -1,69 +0,0 @@
import { Commands, OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
AddHubCommand,
RoutesImperativeHandle,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useCallback, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { useMapEventListener } from '@/hooks/Mapper/events';
export const WRoutesPublic = () => {
const {
outCommand,
storedSettings: { settingsRoutes, settingsRoutesUpdate },
data: { hubs, routes, loadingPublicRoutes },
} = useMapRootState();
const ref = useRef<RoutesImperativeHandle>(null);
const addHubCommand: AddHubCommand = useCallback(
async systemId => {
if (hubs.includes(systemId)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: systemId },
});
},
[hubs, outCommand],
);
const toggleHubCommand: AddHubCommand = useCallback(
async (systemId: string | undefined) => {
if (!systemId) {
return;
}
outCommand({
type: !hubs.includes(systemId) ? OutCommand.addHub : OutCommand.deleteHub,
data: {
system_id: systemId,
},
});
},
[hubs, outCommand],
);
useMapEventListener(event => {
if (event.name === Commands.routes) {
ref.current?.stopLoading();
}
});
return (
<RoutesWidget
ref={ref}
title="Routes"
data={settingsRoutes}
loading={loadingPublicRoutes}
update={settingsRoutesUpdate}
hubs={hubs}
routesList={routes}
addHubCommand={addHubCommand}
toggleHubCommand={toggleHubCommand}
/>
);
};

View File

@@ -1 +0,0 @@
export * from './WRoutesPublic';

View File

@@ -1,94 +0,0 @@
import { Commands, OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
AddHubCommand,
LoadRoutesCommand,
RoutesImperativeHandle,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useCallback, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { useLoadRoutes } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
export const WRoutesUser = () => {
const {
outCommand,
storedSettings: { settingsRoutes, settingsRoutesUpdate },
data: { userHubs, userRoutes },
} = useMapRootState();
const ref = useRef<RoutesImperativeHandle>(null);
const loadRoutesCommand: LoadRoutesCommand = useCallback(
async (systemId, routesSettings) => {
outCommand({
type: OutCommand.getUserRoutes,
data: {
system_id: systemId,
routes_settings: routesSettings,
},
});
},
[outCommand],
);
const addHubCommand: AddHubCommand = useCallback(
async systemId => {
if (userHubs.includes(systemId)) {
return;
}
await outCommand({
type: OutCommand.addUserHub,
data: { system_id: systemId },
});
},
[userHubs, outCommand],
);
const toggleHubCommand: AddHubCommand = useCallback(
async (systemId: string | undefined) => {
if (!systemId) {
return;
}
outCommand({
type: !userHubs.includes(systemId) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
data: {
system_id: systemId,
},
});
},
[userHubs, outCommand],
);
// INFO: User routes loading only if open widget with user routes
const { loading, setLoading } = useLoadRoutes({
data: settingsRoutes,
hubs: userHubs,
loadRoutesCommand,
routesList: userRoutes,
});
useMapEventListener(event => {
if (event.name === Commands.userRoutes) {
setLoading(false);
}
return true;
});
return (
<RoutesWidget
ref={ref}
title="User Routes"
data={settingsRoutes}
update={settingsRoutesUpdate}
hubs={userHubs}
routesList={userRoutes}
loading={loading}
addHubCommand={addHubCommand}
toggleHubCommand={toggleHubCommand}
isRestricted
/>
);
};

View File

@@ -1 +0,0 @@
export * from './WRoutesUser';

View File

@@ -3,6 +3,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemKillsList } from './SystemKillsList'; import { SystemKillsList } from './SystemKillsList';
import { KillsHeader } from './components/SystemKillsHeader'; import { KillsHeader } from './components/SystemKillsHeader';
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
import { useSystemKills } from './hooks/useSystemKills'; import { useSystemKills } from './hooks/useSystemKills';
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog'; import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace'; import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
@@ -12,25 +13,27 @@ const SystemKillsContent = () => {
const { const {
data: { selectedSystems, isSubscriptionActive }, data: { selectedSystems, isSubscriptionActive },
outCommand, outCommand,
storedSettings: { settingsKills },
} = useMapRootState(); } = useMapRootState();
const [systemId] = selectedSystems || []; const [systemId] = selectedSystems || [];
const systemStaticInfo = getSystemStaticInfo(systemId)!; const systemStaticInfo = getSystemStaticInfo(systemId)!;
const [settings] = useKillsWidgetSettings();
const visible = settings.showAll;
const { kills, isLoading, error } = useSystemKills({ const { kills, isLoading, error } = useSystemKills({
systemId, systemId,
outCommand, outCommand,
showAllVisible: settingsKills.showAll, showAllVisible: visible,
sinceHours: settingsKills.timeRange, sinceHours: settings.timeRange,
}); });
const isNothingSelected = !systemId && !settingsKills.showAll; const isNothingSelected = !systemId && !visible;
const showLoading = isLoading && kills.length === 0; const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => { const filteredKills = useMemo(() => {
if (!settingsKills.whOnly || !settingsKills.showAll) return kills; if (!settings.whOnly || !visible) return kills;
return kills.filter(kill => { return kills.filter(kill => {
if (!systemStaticInfo) { if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`); console.warn(`System with id ${kill.solar_system_id} not found.`);
@@ -38,7 +41,7 @@ const SystemKillsContent = () => {
} }
return isWormholeSpace(systemStaticInfo.system_class); return isWormholeSpace(systemStaticInfo.system_class);
}); });
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]); }, [kills, settings.whOnly, systemStaticInfo, visible]);
if (!isSubscriptionActive) { if (!isSubscriptionActive) {
return ( return (
@@ -84,9 +87,7 @@ const SystemKillsContent = () => {
); );
} }
return ( return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
<SystemKillsList kills={filteredKills} onlyOneSystem={!settingsKills.showAll} timeRange={settingsKills.timeRange} />
);
}; };
export const WSystemKills = () => { export const WSystemKills = () => {

View File

@@ -17,91 +17,67 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { WithClassName } from '@/hooks/Mapper/types/common.ts'; import { WithClassName } from '@/hooks/Mapper/types/common.ts';
export type CompactKillRowProps = { export type CompactKillRowProps = {
killDetails?: DetailedKill | null; killDetails: DetailedKill;
systemName: string; systemName: string;
onlyOneSystem: boolean; onlyOneSystem: boolean;
} & WithClassName; } & WithClassName;
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => { export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
const { const {
killmail_id, killmail_id = 0,
// Victim data // Victim data
victim_char_name, victim_char_name = 'Unknown Pilot',
victim_alliance_ticker, victim_alliance_ticker = '',
victim_corp_ticker, victim_corp_ticker = '',
victim_ship_name, victim_ship_name = 'Unknown Ship',
victim_corp_name, victim_corp_name = '',
victim_alliance_name, victim_alliance_name = '',
victim_char_id, victim_char_id = 0,
victim_corp_id, victim_corp_id = 0,
victim_alliance_id, victim_alliance_id = 0,
victim_ship_type_id, victim_ship_type_id = 0,
// Attacker data // Attacker data
final_blow_char_id, final_blow_char_id = 0,
final_blow_char_name, final_blow_char_name = '',
final_blow_alliance_ticker, final_blow_alliance_ticker = '',
final_blow_alliance_name, final_blow_alliance_name = '',
final_blow_alliance_id, final_blow_alliance_id = 0,
final_blow_corp_ticker, final_blow_corp_ticker = '',
final_blow_corp_id, final_blow_corp_id = 0,
final_blow_corp_name, final_blow_corp_name = '',
final_blow_ship_type_id, final_blow_ship_type_id = 0,
kill_time, kill_time = '',
total_value, total_value = 0,
} = killDetails || {}; } = killDetails || {};
// Apply fallback values using nullish coalescing to handle both null and undefined const attackerIsNpc = final_blow_char_id === 0;
const safeKillmailId = killmail_id ?? 0;
const safeVictimCharName = victim_char_name ?? 'Unknown Pilot';
const safeVictimAllianceTicker = victim_alliance_ticker ?? '';
const safeVictimCorpTicker = victim_corp_ticker ?? '';
const safeVictimShipName = victim_ship_name ?? 'Unknown Ship';
const safeVictimCorpName = victim_corp_name ?? '';
const safeVictimAllianceName = victim_alliance_name ?? '';
const safeVictimCharId = victim_char_id ?? 0;
const safeVictimCorpId = victim_corp_id ?? 0;
const safeVictimAllianceId = victim_alliance_id ?? 0;
const safeVictimShipTypeId = victim_ship_type_id ?? 0;
const safeFinalBlowCharId = final_blow_char_id ?? 0;
const safeFinalBlowCharName = final_blow_char_name ?? '';
const safeFinalBlowAllianceTicker = final_blow_alliance_ticker ?? '';
const safeFinalBlowAllianceName = final_blow_alliance_name ?? '';
const safeFinalBlowAllianceId = final_blow_alliance_id ?? 0;
const safeFinalBlowCorpTicker = final_blow_corp_ticker ?? '';
const safeFinalBlowCorpId = final_blow_corp_id ?? 0;
const safeFinalBlowCorpName = final_blow_corp_name ?? '';
const safeFinalBlowShipTypeId = final_blow_ship_type_id ?? 0;
const safeKillTime = kill_time ?? '';
const safeTotalValue = total_value ?? 0;
const attackerIsNpc = safeFinalBlowCharId === 0;
// Define victim affiliation ticker. // Define victim affiliation ticker.
const victimAffiliationTicker = safeVictimAllianceTicker || safeVictimCorpTicker || 'No Ticker'; const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
const killValueFormatted = safeTotalValue != null && safeTotalValue > 0 ? `${formatISK(safeTotalValue)} ISK` : null; const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const killTimeAgo = safeKillTime ? formatTimeMixed(safeKillTime) : '0h ago'; const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
const attackerSubscript = killDetails ? getAttackerSubscript(killDetails) : undefined; const attackerSubscript = getAttackerSubscript(killDetails);
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({ const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id: safeVictimCharId, victim_char_id,
victim_ship_type_id: safeVictimShipTypeId, victim_ship_type_id,
victim_corp_id: safeVictimCorpId, victim_corp_id,
victim_alliance_id: safeVictimAllianceId, victim_alliance_id,
}); });
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({ const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id: safeFinalBlowCharId, final_blow_char_id,
final_blow_corp_id: safeFinalBlowCorpId, final_blow_corp_id,
final_blow_alliance_id: safeFinalBlowAllianceId, final_blow_alliance_id,
}); });
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip( const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl, victimAllianceLogoUrl,
victimCorpLogoUrl, victimCorpLogoUrl,
safeVictimAllianceName, victim_alliance_name,
safeVictimCorpName, victim_corp_name,
'Victim', 'Victim',
); );
@@ -111,25 +87,25 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
attackerIsNpc, attackerIsNpc,
attackerAllianceLogoUrl, attackerAllianceLogoUrl,
attackerCorpLogoUrl, attackerCorpLogoUrl,
safeFinalBlowAllianceName, final_blow_alliance_name,
safeFinalBlowCorpName, final_blow_corp_name,
safeFinalBlowShipTypeId, final_blow_ship_type_id,
), ),
[ [
attackerAllianceLogoUrl, attackerAllianceLogoUrl,
attackerCorpLogoUrl, attackerCorpLogoUrl,
attackerIsNpc, attackerIsNpc,
safeFinalBlowAllianceName, final_blow_alliance_name,
safeFinalBlowCorpName, final_blow_corp_name,
safeFinalBlowShipTypeId, final_blow_ship_type_id,
], ],
); );
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker. // Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
const attackerTicker = attackerIsNpc ? '' : safeFinalBlowAllianceTicker || safeFinalBlowCorpTicker || ''; const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
// For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page. // For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page.
const attackerLink = attackerIsNpc ? zkillLink('kill', safeKillmailId) : zkillLink('character', safeFinalBlowCharId); const attackerLink = attackerIsNpc ? zkillLink('kill', killmail_id) : zkillLink('character', final_blow_char_id);
return ( return (
<div <div
@@ -145,7 +121,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimShipUrl && ( {victimShipUrl && (
<div className="relative shrink-0 w-8 h-8 overflow-hidden"> <div className="relative shrink-0 w-8 h-8 overflow-hidden">
<a <a
href={zkillLink('kill', safeKillmailId)} href={zkillLink('kill', killmail_id)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block w-full h-full" className="block w-full h-full"
@@ -161,7 +137,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimPrimaryLogoUrl && ( {victimPrimaryLogoUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}> <WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a <a
href={zkillLink('kill', safeKillmailId)} href={zkillLink('kill', killmail_id)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="relative block shrink-0 w-8 h-8 overflow-hidden" className="relative block shrink-0 w-8 h-8 overflow-hidden"
@@ -177,12 +153,12 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div> </div>
<div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]"> <div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
<div className="truncate text-stone-200"> <div className="truncate text-stone-200">
{safeVictimCharName} {victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span> <span className="text-stone-400"> / {victimAffiliationTicker}</span>
</div> </div>
<div className="truncate text-stone-300 flex items-center gap-1"> <div className="truncate text-stone-300 flex items-center gap-1">
<span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]"> <span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
{safeVictimShipName} {victim_ship_name}
</span> </span>
{killValueFormatted && ( {killValueFormatted && (
<> <>
@@ -194,9 +170,9 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div> </div>
<div className="flex items-center ml-auto gap-2"> <div className="flex items-center ml-auto gap-2">
<div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]"> <div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]">
{!attackerIsNpc && (safeFinalBlowCharName || attackerTicker) && ( {!attackerIsNpc && (final_blow_char_name || attackerTicker) && (
<div className="truncate text-stone-200"> <div className="truncate text-stone-200">
{safeFinalBlowCharName} {final_blow_char_name}
{!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>} {!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
</div> </div>
)} )}

Some files were not shown because too many files have changed in this diff Show More