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
804 changed files with 151700 additions and 83441 deletions

View File

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

View File

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

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

@@ -1,7 +1,5 @@
export WEB_APP_URL="http://localhost:8000"
export RELEASE_COOKIE="PDpbnyo6mEI_0T4ZsHH_ESmi1vT1toQ8PTc0vbfg5FIT4Ih-Lh98mw=="
# Erlang node name for distributed Erlang (optional - defaults to wanderer@hostname)
# export RELEASE_NODE="wanderer@localhost"
export EVE_CLIENT_ID="<EVE_CLIENT_ID>"
export EVE_CLIENT_SECRET="<EVE_CLIENT_SECRET>"
export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
@@ -10,9 +8,4 @@ export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"
export WANDERER_CHARACTER_API_DISABLED="false"
export WANDERER_KILLS_SERVICE_ENABLED="true"
export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
export WANDERER_ZKILL_PRELOAD_DISABLED="false"

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

@@ -1,234 +0,0 @@
name: Build Develop
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:
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.set-commit-develop.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:
ssh-key: "${{ secrets.COMMIT_KEY }}"
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
- name: Set commit hash for develop
id: set-commit-develop
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
needs: build
runs-on: ubuntu-22.04
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
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- 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
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=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about develop 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_DEV }}
content: |
📣 New develop release available 🚀
**Commit**: `${{ github.sha }}`
**Status**: Development/Testing Release
Docker image: `wandererltd/community-edition:develop`
⚠️ This is an unstable development release for testing purposes.

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- main
- "releases/*"
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
@@ -18,10 +18,51 @@ permissions:
contents: write
jobs:
deploy-test:
name: 🚀 Deploy to test env (fly.io)
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && github.event_name == 'push') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: 👀 Read app name
uses: SebRollen/toml-action@v1.0.0
id: app_name
with:
file: "fly.toml"
field: "app"
- name: 🚀 Deploy Test
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
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:
name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
@@ -52,7 +93,6 @@ jobs:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ssh-key: "${{ secrets.COMMIT_KEY }}"
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
@@ -94,15 +134,9 @@ jobs:
git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com'
mix git_ops.release --force-patch --yes
git commit --allow-empty -m 'chore: [skip ci]'
git push --follow-tags
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Set commit hash for develop
id: set-commit-develop
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
needs: build
@@ -136,17 +170,17 @@ jobs:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: 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
@@ -260,25 +294,19 @@ jobs:
create-release:
name: 🏷 Create Release
runs-on: ubuntu-22.04
needs: [docker, merge]
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
needs: build
steps:
- 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: 🏷 Create Draft Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.get-latest-tag.outputs.tag }}
name: Release ${{ steps.get-latest-tag.outputs.tag }}
tag_name: ${{ needs.docker.outputs.release-tag }}
name: Release ${{ needs.docker.outputs.release-tag }}
body: |
## Info
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
@@ -289,11 +317,6 @@ jobs:
In order to promote this to prod, edit the draft and press **"Publish release"**.
draft: true
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:

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
*.key
.repomixignore
repomix*
/.idea/
/node_modules/
/assets/node_modules/
@@ -18,9 +17,6 @@ repomix*
/priv/static/*.js
/priv/static/*.css
# Dialyzer PLT files
/priv/plts/
.DS_Store
**/.DS_Store

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 be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
COPY priv priv
COPY lib lib
COPY assets assets
RUN mix assets.deploy
RUN mix compile
RUN mix assets.deploy
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
COPY rel rel
COPY rel rel
RUN mix release
# start a new build stage so that the final image will only contain

View File

@@ -30,10 +30,10 @@ format f:
mix format
test t:
MIX_ENV=test mix test
mix test
coverage cover co:
MIX_ENV=test mix test --cover
mix test --cover
unit-tests ut:
@echo "Running unit tests..."
@@ -45,3 +45,4 @@ versions v:
@cat .tool-versions
@cat Aptfile
@echo

View File

@@ -17,29 +17,5 @@ module.exports = {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "primereact/button",
"importNames": ["Button"],
"message": "Use WdButton instead Button"
}
]
}
],
"react/forbid-elements": [
"error",
{
"forbid": [
{
"element": "Button",
"message": "Use WdButton instead Button"
}
]
}
]
},
};

View File

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

View File

@@ -73,9 +73,7 @@ body > div:first-of-type {
}
.maps_bg {
/* OLD image */
/* background-image: url('../images/maps_bg.webp'); */
background-image: url('https://wanderer-industries.github.io/wanderer-assets/images/eve-screen-catalyst-expansion-bg.jpg');
background-image: url('../images/maps_bg.webp');
background-size: cover;
background-position: center;
width: 100%;

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

View File

@@ -1,31 +0,0 @@
import React, { createContext, useContext, useRef } from 'react';
import { Toast } from 'primereact/toast';
import type { ToastMessage } from 'primereact/toast';
interface ToastContextValue {
toastRef: React.RefObject<Toast>;
show: (message: ToastMessage | ToastMessage[]) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const toastRef = useRef<Toast>(null);
const show = (message: ToastMessage | ToastMessage[]) => {
toastRef.current?.show(message);
};
return (
<ToastContext.Provider value={{ toastRef, show }}>
<Toast ref={toastRef} position="top-right" />
{children}
</ToastContext.Provider>
);
};
export const useToast = (): ToastContextValue => {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within a ToastProvider');
return context;
};

View File

@@ -99,11 +99,6 @@
.p-dropdown-item {
padding: 0.25rem 0.5rem;
font-size: 14px;
width: 100%;
.p-dropdown-item-label {
width: 100%;
}
}
.p-dropdown-item-group {
@@ -185,106 +180,3 @@
.p-datatable .p-datatable-tbody > tr.p-highlight {
background: initial;
}
.suppress-menu-behaviour {
pointer-events: none;
.p-menuitem-content {
pointer-events: initial;
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
.p-autocomplete .p-autocomplete-multiple-container:not(.p-disabled).p-focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:hover {
border-color: #335c7e;
}
// --------------- TOAST
.p-toast .p-toast-message {
background-color: #1a1a1a;
color: #e0e0e0;
border-left: 4px solid transparent;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.p-toast .p-toast-message .p-toast-summary {
color: #ffffff;
font-weight: 600;
}
.p-toast .p-toast-message .p-toast-detail {
color: #c0c0c0;
font-size: 13px;
}
.p-toast .p-toast-icon-close {
color: #ffaa00;
transition: background 0.2s;
}
.p-toast .p-toast-icon-close:hover {
background: #333;
color: #fff;
}
.p-toast-message-success {
border-left-color: #f1c40f;
}
.p-toast-message-error {
border-left-color: #e74c3c;
}
.p-toast-message-info {
border-left-color: #3498db;
}
.p-toast-message-warn {
border-left-color: #e67e22;
}
.p-toast-message-success .p-toast-message-icon {
color: #f1c40f;
}
.p-toast-message-error .p-toast-message-icon {
color: #e74c3c;
}
.p-toast-message-info .p-toast-message-icon {
color: #3498db;
}
.p-toast-message-warn .p-toast-message-icon {
color: #e67e22;
}
.p-toast-message-success .p-toast-message-content {
border-left-color: #f1c40f;
}
.p-toast-message-error .p-toast-message-content {
border-left-color: #e74c3c;
}
.p-toast-message-info .p-toast-message-content {
border-left-color: #3498db;
}
.p-toast-message-warn .p-toast-message-content {
border-left-color: #e67e22;
}
.p-dialog-header-icon.p-dialog-header-close.p-link {
position: relative;
left: 6px;
}

View File

@@ -1,13 +1,18 @@
// import './tailwind.css';
@use 'primereact/resources/primereact.min.css';
@use 'primeicons/primeicons.css';
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
//@import "prime-fixes";
@import 'primereact/resources/primereact.min.css';
//@import 'primeflex/primeflex.css';
@import 'primeicons/primeicons.css';
//@import 'primereact/resources/primereact.css';
@use "fixes";
@use "prime-fixes";
@use "custom-scrollbar";
@use "tooltip";
@use "context-menu";
@import "fixes";
@import "prime-fixes";
@import "custom-scrollbar";
@import "tooltip";
@import "context-menu";
.fixedImportant {

View File

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

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 200px;
min-height: 300px;
.p-tabview {
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,3 +1,6 @@
@use "fix-dialog";
@use "fix-popup";
@use "fix-tabs";
@import "fix-dialog";
@import "fix-popup";
@import "fix-tabs";
//@import "fix-input";
//@import "theme";

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

View File

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

View File

@@ -1,4 +1,3 @@
export * from './useTagMenu';
export * from './useStatusMenu';
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,94 +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, WdButton } from '@/hooks/Mapper/components/ui-kit';
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 => (
<WdButton
outlined={system?.tag !== x}
severity="warning"
key={x}
value={x}
size="small"
className="p-[3px] justify-center"
onClick={() => system?.tag !== x && onSystemTag(x)}
>
{x}
</WdButton>
))}
<WdButton
disabled={!isSelectedTag}
icon="pi pi-ban"
size="small"
className="!p-0 !w-[initial] justify-center"
outlined
severity="help"
onClick={() => onSystemTag()}
></WdButton>
</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 { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
interface UseContextMenuSystemHandlersProps {
hubs: string[];
userHubs: string[];
systems: SolarSystemRawType[];
outCommand: OutCommandHandler;
}
export const useContextMenuSystemHandlers = ({
systems,
hubs,
userHubs,
outCommand,
}: UseContextMenuSystemHandlersProps) => {
export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const { deleteSystems } = useDeleteSystems();
const ref = useRef({ hubs, userHubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, userHubs, system, systems, outCommand, deleteSystems };
const ref = useRef({ hubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, system, systems, outCommand, deleteSystems };
const open = useCallback((ev: any, systemId: string) => {
setSystem(systemId);
@@ -79,37 +72,6 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onUserHubToggle = useCallback(() => {
const { userHubs, system, outCommand } = ref.current;
if (!system) {
return;
}
outCommand({
type: !userHubs.includes(system) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
data: {
system_id: system,
},
});
setSystem(undefined);
}, []);
// const onTogglePingRally = useCallback(() => {
// const { userHubs, system, outCommand } = ref.current;
// if (!system) {
// return;
// }
//
// outCommand({
// type: OutCommand.openPing,
// data: {
// solar_system_id: system,
// type: PingType.Rally,
// },
// });
// setSystem(undefined);
// }, []);
const onSystemTag = useCallback((tag?: string) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -142,6 +104,7 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -214,8 +177,6 @@ export const useContextMenuSystemHandlers = ({
onDeleteSystem,
onLockToggle,
onHubToggle,
onUserHubToggle,
// onTogglePingRally,
onSystemTag,
onSystemTemporaryName,
onSystemStatus,

View File

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

View File

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

View File

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

View File

@@ -2,60 +2,25 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuSystemMultipleProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteSystems(): void;
onCopySystems(): void;
}
export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps> = ({
contextMenuRef,
onDeleteSystems,
onCopySystems,
}) => {
const {
data: { options, userPermissions },
} = useMapRootState();
const items: MenuItem[] = useMemo(() => {
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
return [
{
label: 'Delete',
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
icon: PrimeIcons.TRASH,
command: onDeleteSystems,
},
{ separator: true },
{
label: 'Copy',
icon: PrimeIcons.COPY,
command: onCopySystems,
disabled: !allowCopy,
template: () => {
if (allowCopy) {
return <WdMenuItem icon="pi pi-copy">Copy</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Copy."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-copy">
Copy
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
];
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
}, [onDeleteSystems]);
return (
<>

View File

@@ -1,81 +1,40 @@
import { Node } from 'reactflow';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { encodeJsonToUriBase64 } from '@/hooks/Mapper/utils';
import { useToast } from '@/hooks/Mapper/ToastProvider.tsx';
export const useContextMenuSystemMultipleHandlers = () => {
const {
data: { pings, connections },
} = useMapRootState();
const { show } = useToast();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
const { deleteSystems } = useDeleteSystems();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const refVars = useRef({ systems, ping, connections, deleteSystems });
refVars.current = { systems, ping, connections, deleteSystems };
const handleSystemMultipleContext = useCallback<NodeSelectionMouseHandler>((ev, systems_) => {
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
setSystems(systems_);
ev.preventDefault();
ctxManager.next('ctxSysMult', contextMenuRef.current);
contextMenuRef.current?.show(ev);
}, []);
};
const onDeleteSystems = useCallback(() => {
const { systems, ping, deleteSystems } = refVars.current;
if (!systems) {
return;
}
const sysToDel = systems
.filter(x => !x.data.locked)
.filter(x => x.id !== ping?.solar_system_id)
.map(x => x.id);
const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
if (sysToDel.length === 0) {
return;
}
deleteSystems(sysToDel);
}, []);
const onCopySystems = useCallback(async () => {
const { systems, connections } = refVars.current;
if (!systems) {
return;
}
const connectionToCopy = connections.filter(
c => systems.filter(s => [c.target, c.source].includes(s.id)).length == 2,
);
await navigator.clipboard.writeText(
encodeJsonToUriBase64({ systems: systems.map(x => x.data), connections: connectionToCopy }),
);
show({
severity: 'success',
summary: 'Copied to clipboard',
detail: `Successfully copied to clipboard - [${systems.length}] systems and [${connectionToCopy.length}] connections`,
life: 3000,
});
}, [show]);
}, [deleteSystems, systems]);
return {
handleSystemMultipleContext,
contextMenuRef,
onDeleteSystems,
onCopySystems,
};
};

View File

@@ -1,10 +1,10 @@
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons.ts';
import classes from './FastSystemActions.module.scss';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import classes from './FastSystemActions.module.scss';
export interface FastSystemActionsProps {
systemId: string;
@@ -27,7 +27,7 @@ export const FastSystemActions = ({
ref.current = { systemId, systemName, regionName, isWH };
const handleOpenZKB = useCallback(
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}/`, '_blank'),
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
[],
);
@@ -59,21 +59,9 @@ export const FastSystemActions = ({
return (
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}>
<div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Anoikis' }}
source={ANOIK_ICON}
onClick={handleOpenAnoikis}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Dotlan' }}
source={DOTLAN_ICON}
onClick={handleOpenDotlan}
/>
<WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} />
<WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} />
<WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} />
</div>
<div className="flex gap-2 items-center pl-1">
@@ -81,14 +69,14 @@ export const FastSystemActions = ({
textSize={WdImageSize.off}
className={PrimeIcons.COPY}
onClick={copySystemNameToClipboard}
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }}
tooltip={{ content: 'Copy system name' }}
/>
{showEdit && (
<WdImgButton
textSize={WdImageSize.off}
className="pi pi-pen-to-square text-base"
onClick={onOpenSettings}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
tooltip={{ content: 'Edit system name and description' }}
/>
)}
</div>

View File

@@ -8,4 +8,6 @@ export type WaypointSetContextHandlerProps = {
destination: string;
};
export type WaypointSetContextHandler = (props: WaypointSetContextHandlerProps) => void;
export type NodeSelectionMouseHandler = (event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void;
export type NodeSelectionMouseHandler =
| ((event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void)
| undefined;

View File

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

View File

@@ -1,64 +0,0 @@
import { MapUserSettings, SettingsWrapper } 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}`);
}
}
/** Minimal check that an object matches SettingsWrapper<*> */
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
/** 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 (!isSettings((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<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 './useGetOwnOnlineCharacters';
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,26 +0,0 @@
import { useMemo } from 'react';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
export type UseLocalCounterProps = {
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
};
export const getLocalCharacters = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
return charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
};
export const useLocalCounter = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
const localCounterCharacters = useMemo(
() => getLocalCharacters({ charactersInSystem, userCharacters }),
[charactersInSystem, userCharacters],
);
return { localCounterCharacters };
};

View File

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

View File

@@ -1,10 +1,4 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import type { PanelPosition } from '@reactflow/core';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
Edge,
@@ -22,6 +16,8 @@ import ReactFlow, {
import 'reactflow/dist/style.css';
import classes from './Map.module.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import {
ContextMenuConnection,
ContextMenuRoot,
@@ -30,11 +26,25 @@ import {
useContextMenuRootHandlers,
} from './components';
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import { MapViewport, OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import type { Viewport } from '@reactflow/core/dist/esm/types';
import { usePrevious } from 'primereact/hooks';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
const getViewPortFromStore = () => {
const restored = localStorage.getItem(SESSION_KEY.viewPort);
if (!restored) {
return { ...DEFAULT_VIEW_PORT };
}
return JSON.parse(restored);
};
const initialNodes: Node<SolarSystemRawType>[] = [
// {
@@ -77,7 +87,6 @@ interface MapCompProps {
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
onChangeViewport?: (viewport: MapViewport) => void;
minimapClasses?: string;
isShowMinimap?: boolean;
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
@@ -86,10 +95,6 @@ interface MapCompProps {
isShowBackgroundPattern?: boolean;
isSoftBackground?: boolean;
theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
defaultViewport?: Viewport;
}
const MapComp = ({
@@ -107,28 +112,19 @@ const MapComp = ({
isSoftBackground,
theme,
onAddSystem,
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
onChangeViewport,
defaultViewport,
}: MapCompProps) => {
const { getNodes, setViewport } = useReactFlow();
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem, onCommand });
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
const refVars = useRef({ onChangeViewport });
refVars.current = { onChangeViewport };
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
@@ -184,10 +180,9 @@ const MapComp = ({
[onSelectionChange],
);
const handleMoveEnd: OnMoveEnd = useCallback((_, viewport) => {
// @ts-ignore
refVars.current.onChangeViewport?.(viewport);
}, []);
const handleMoveEnd: OnMoveEnd = (_, viewport) => {
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
};
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
@@ -211,23 +206,8 @@ const MapComp = ({
...x,
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
pings,
localShowShipName,
}));
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
const prevViewport = usePrevious(defaultViewport);
useEffect(() => {
if (defaultViewport == null) {
return;
}
if (prevViewport == null) {
return;
}
setViewport(defaultViewport);
}, [defaultViewport, prevViewport, setViewport]);
}, [showKSpaceBG, isThickConnections, update]);
return (
<>
@@ -243,7 +223,7 @@ const MapComp = ({
onConnect={onConnect}
// TODO we need save into session all of this
// and on any action do either
defaultViewport={defaultViewport}
defaultViewport={getViewPortFromStore()}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
connectionMode={connectionMode}
@@ -290,9 +270,7 @@ const MapComp = ({
// onlyRenderVisibleElements
selectionMode={SelectionMode.Partial}
>
{isShowMinimap && (
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
)}
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
</ReactFlow>
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>

View File

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

View File

@@ -1,4 +1,4 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.ConnectionTimeEOL {
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
@@ -8,10 +8,6 @@
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
}
.ConnectionBridge {
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
}
.ConnectionSave {
background-image: linear-gradient(207deg, transparent, var(--conn-save));
}
@@ -19,14 +15,3 @@
.SelectedItem {
background-color: var(--selected-item-bg);
}
.FastActions {
:global {
.p-menuitem-content {
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
}

View File

@@ -1,3 +1,10 @@
import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import classes from './ContextMenuConnection.module.scss';
import {
MASS_STATE_NAMES,
MASS_STATE_NAMES_ORDER,
@@ -6,25 +13,14 @@ import {
SHIP_SIZES_NAMES_SHORT,
SHIP_SIZES_SIZE,
} from '@/hooks/Mapper/components/map/constants.ts';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { ContextMenu } from 'primereact/contextmenu';
import { MenuItem } from 'primereact/menuitem';
import React, { RefObject, useMemo } from 'react';
import { Edge } from 'reactflow';
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
import classes from './ContextMenuConnection.module.scss';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
export interface ContextMenuConnectionProps {
contextMenuRef: RefObject<ContextMenu>;
onDeleteConnection(): void;
onChangeTimeState(lifetime: TimeStatus): void;
onChangeTimeState(): void;
onChangeMassState(state: MassState): void;
onChangeShipSizeStatus(state: ShipSizeStatus): void;
onChangeType(type: ConnectionType): void;
onToggleMassSave(isLocked: boolean): void;
onHide(): void;
edge?: Edge<SolarSystemConnection>;
@@ -36,7 +32,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
onChangeTimeState,
onChangeMassState,
onChangeShipSizeStatus,
onChangeType,
onToggleMassSave,
onHide,
edge,
@@ -46,106 +41,74 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
return [];
}
const sourceInfo = getSystemStaticInfo(edge.data?.source);
const targetInfo = getSystemStaticInfo(edge.data?.target);
const bothNullsec =
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
if (edge.data?.type === ConnectionType.bridge) {
return [
{
label: `Set as Wormhole`,
icon: 'pi hero-arrow-uturn-left',
command: () => onChangeType(ConnectionType.wormhole),
},
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
if (edge.data?.type === ConnectionType.gate) {
return [
{
label: 'Disconnect',
icon: PrimeIcons.TRASH,
command: onDeleteConnection,
},
];
}
const isWormhole = edge.data?.type !== ConnectionType.gate;
return [
{
className: clsx(classes.FastActions, '!h-[54px]'),
template: () => {
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
},
},
{
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
...(isWormhole
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
label: `EOL`,
className: clsx({
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
}),
icon: PrimeIcons.CLOCK,
command: onChangeTimeState,
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
...(bothNullsec
? [
{
label: `Set as Bridge`,
icon: 'pi hero-forward',
command: () => onChangeType(ConnectionType.bridge),
label: `Frigate`,
className: clsx({
[classes.ConnectionFrigate]: isFrigateSize,
}),
icon: PrimeIcons.CLOUD,
command: () =>
onChangeShipSizeStatus(
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
),
},
{
label: `Save mass`,
className: clsx({
[classes.ConnectionSave]: edge.data?.locked,
}),
icon: PrimeIcons.LOCK,
command: () => onToggleMassSave(!edge.data?.locked),
},
...(!isFrigateSize
? [
{
label: `Mass status`,
icon: PrimeIcons.CHART_PIE,
items: MASS_STATE_NAMES_ORDER.map(x => ({
label: MASS_STATE_NAMES[x],
className: clsx({
[classes.SelectedItem]: edge.data?.mass_status === x,
}),
command: () => onChangeMassState(x),
})),
},
]
: []),
{
label: `Ship Size`,
icon: PrimeIcons.CLOUD,
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
label: (
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
<div>{SHIP_SIZES_NAMES[x]}</div>
<div></div>
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
{SHIP_SIZES_SIZE[x]} t.
</div>
</div>
) as unknown as string, // TODO my lovely kostyl
className: clsx({
[classes.SelectedItem]: edge.data?.ship_size_type === x,
}),
command: () => onChangeShipSizeStatus(x),
})),
},
]
: []),
@@ -155,19 +118,11 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
command: onDeleteConnection,
},
];
}, [
edge,
onChangeTimeState,
onDeleteConnection,
onChangeType,
onChangeShipSizeStatus,
onToggleMassSave,
onChangeMassState,
]);
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
return (
<>
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" className="!w-[250px]" />
<ContextMenu model={items} ref={contextMenuRef} onHide={onHide} breakpoint="767px" />
</>
);
};

View File

@@ -1,12 +0,0 @@
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { WdLifetimeSelector, WdLifetimeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdLifetimeSelector.tsx';
export const LifetimeActionsWrapper = (props: WdLifetimeSelectorProps) => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
<div className="text-[12px] text-stone-500 font-semibold">Life time:</div>
<WdLifetimeSelector {...props} />
</LayoutEventBlocker>
);
};

View File

@@ -30,7 +30,7 @@ export const useContextMenuConnectionHandlers = () => {
setEdge(undefined);
};
const onChangeTimeState = (lifetime: TimeStatus) => {
const onChangeTimeState = () => {
if (!edge || !edge.data) {
return;
}
@@ -40,7 +40,7 @@ export const useContextMenuConnectionHandlers = () => {
data: {
source: edge.source,
target: edge.target,
value: lifetime,
value: edge.data.time_status === TimeStatus.default ? TimeStatus.eol : TimeStatus.default,
},
});
setEdge(undefined);

View File

@@ -2,70 +2,22 @@ import React, { RefObject, useMemo } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PrimeIcons } from 'primereact/api';
import { MenuItem } from 'primereact/menuitem';
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
export interface ContextMenuRootProps {
contextMenuRef: RefObject<ContextMenu>;
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
onAddSystem(): void;
onPasteSystemsAnsConnections(): void;
}
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
contextMenuRef,
onAddSystem,
onPasteSystemsAnsConnections,
pasteSystemsAndConnections,
}) => {
const {
data: { options, userPermissions },
} = useMapState();
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({ contextMenuRef, onAddSystem }) => {
const items: MenuItem[] = useMemo(() => {
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
return [
{
label: 'Add System',
icon: PrimeIcons.PLUS,
command: onAddSystem,
},
...(pasteSystemsAndConnections != null
? [
{
icon: 'pi pi-clipboard',
disabled: !allowPaste,
command: onPasteSystemsAnsConnections,
template: () => {
if (allowPaste) {
return (
<WdMenuItem icon="pi pi-clipboard">
Paste
</WdMenuItem>
);
}
return (
<MenuItemWithInfo
infoTitle="Action is blocked because you dont have permission to Paste."
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
tooltipWrapperClassName="flex"
>
<WdMenuItem disabled icon="pi pi-clipboard">
Paste
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
]
: []),
];
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
}, [onAddSystem]);
return (
<>

View File

@@ -1,76 +1,36 @@
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
import { recenterSystemsByBounds } from '@/hooks/Mapper/helpers/recenterSystems.ts';
import { OutCommand, OutCommandHandler, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { decodeUriBase64ToJson } from '@/hooks/Mapper/utils';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { ContextMenu } from 'primereact/contextmenu';
import React, { useCallback, useRef, useState } from 'react';
import { useReactFlow, XYPosition } from 'reactflow';
export type PasteSystemsAndConnections = {
systems: SolarSystemRawType[];
connections: SolarSystemConnection[];
};
import React, { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
type UseContextMenuRootHandlers = {
onAddSystem?: OnMapAddSystemCallback;
onCommand?: OutCommandHandler;
};
export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContextMenuRootHandlers = {}) => {
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
const rf = useReactFlow();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [position, setPosition] = useState<XYPosition | null>(null);
const [pasteSystemsAndConnections, setPasteSystemsAndConnections] = useState<PasteSystemsAndConnections>();
const handleRootContext = async (e: React.MouseEvent<HTMLDivElement>) => {
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
setPosition(rf.project({ x: e.clientX, y: e.clientY }));
e.preventDefault();
ctxManager.next('ctxRoot', contextMenuRef.current);
contextMenuRef.current?.show(e);
try {
const text = await navigator.clipboard.readText();
const result = decodeUriBase64ToJson(text);
setPasteSystemsAndConnections(result as PasteSystemsAndConnections);
} catch (err) {
setPasteSystemsAndConnections(undefined);
// do nothing
}
};
const ref = useRef({ onAddSystem, position, pasteSystemsAndConnections, onCommand });
ref.current = { onAddSystem, position, pasteSystemsAndConnections, onCommand };
const ref = useRef({ onAddSystem, position });
ref.current = { onAddSystem, position };
const onAddSystemCallback = useCallback(() => {
ref.current.onAddSystem?.({ coordinates: position });
}, [position]);
const onPasteSystemsAnsConnections = useCallback(async () => {
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
if (!position || !onCommand || !pasteSystemsAndConnections) {
return;
}
const { systems } = recenterSystemsByBounds(pasteSystemsAndConnections.systems);
await onCommand({
type: OutCommand.manualPasteSystemsAndConnections,
data: {
systems: systems.map(({ position: srcPos, ...rest }) => ({
position: { x: Math.round(srcPos.x + position.x), y: Math.round(srcPos.y + position.y) },
...rest,
})),
connections: pasteSystemsAndConnections.connections,
},
});
}, []);
return {
handleRootContext,
pasteSystemsAndConnections,
contextMenuRef,
onAddSystem: onAddSystemCallback,
onPasteSystemsAnsConnections,
};
};

View File

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

View File

@@ -1,87 +0,0 @@
import { useMemo } from 'react';
import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import classes from './LocalCounter.module.scss';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useLocalCharactersItemTemplate } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
hasUserCharacters: boolean;
showIcon?: boolean;
disableInteractive?: boolean;
className?: string;
contentClassName?: string;
}
export const LocalCounter = ({
className,
contentClassName,
localCounterCharacters,
hasUserCharacters,
showIcon = true,
disableInteractive,
}: LocalCounterProps) => {
const {
data: { localShowShipName },
} = useMapState();
const itemTemplate = useLocalCharactersItemTemplate(localShowShipName);
const theme = useTheme();
const pilotTooltipContent = useMemo(() => {
return (
<div
style={{
width: '100%',
minWidth: '300px',
overflow: 'hidden',
}}
>
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} autoSize={true} />
</div>
);
}, [localCounterCharacters, itemTemplate]);
if (localCounterCharacters.length === 0) {
return null;
}
return (
<div
className={clsx(
classes.TooltipActive,
{
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
},
className,
)}
>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={!disableInteractive}
smallPaddings
>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(
classes.localCounter,
{
[classes.hasUserCharacters]: hasUserCharacters,
},
contentClassName,
)}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>
</div>
</div>
</WdTooltipWrapper>
</div>
);
};

View File

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

View File

@@ -1,20 +1,10 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.EdgePathBack {
fill: none;
stroke: #80a5c5;
stroke-width: 3px;
&.time1 {
stroke: #f11ab2;
stroke-width: 4px;
}
&.time4 {
stroke: #a654e3;
stroke-width: 4px;
}
&.TimeCrit {
stroke: #f11ab2;
stroke-width: 4px;
@@ -39,13 +29,6 @@
&.Gate {
stroke: #9aff40;
}
&.Bridge {
stroke: #9aff40;
stroke-dasharray: 10 5;
stroke-linecap: round;
}
}
.EdgePathFront {

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
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 clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -9,7 +9,6 @@ import { PrimeIcons } from 'primereact/api';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
const MAP_TRANSLATES: Record<string, string> = {
[Position.Top]: 'translate(-48%, 0%)',
@@ -43,9 +42,7 @@ export const SHIP_SIZES_COLORS = {
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
const isWormhole = data?.type === ConnectionType.wormhole;
const isGate = data?.type === ConnectionType.gate;
const isBridge = data?.type === ConnectionType.bridge;
const isWormhole = data?.type !== ConnectionType.gate;
const {
data: { isThickConnections },
@@ -54,11 +51,13 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const [edgePath, labelX, labelY] = getBezierPath({
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,
sourceY: sy - offset.y,
sourcePosition: sourcePos,
@@ -68,7 +67,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
});
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
}, [isThickConnections, sourceNode, targetNode]);
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
if (!sourceNode || !targetNode || !data) {
return null;
@@ -80,11 +79,9 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
id={`back_${id}`}
className={clsx(classes.EdgePathBack, {
[classes.Tick]: isThickConnections,
[classes.time1]: isWormhole && data.time_status === TimeStatus._1h,
[classes.time4]: isWormhole && data.time_status === TimeStatus._4h,
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
[classes.Hovered]: hovered,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -98,8 +95,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.Gate]: !isWormhole,
})}
d={path}
markerEnd={markerEnd}
@@ -151,19 +147,6 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
</WdTooltipWrapper>
)}
{isBridge && (
<WdTooltipWrapper
content="Ansiblex Jump Bridge"
position={TooltipPosition.top}
className={clsx(
classes.LinkLabel,
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
)}
>
B
</WdTooltipWrapper>
)}
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
<WdTooltipWrapper
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}

View File

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

View File

@@ -5,8 +5,8 @@
}
.hoverTarget {
padding: 2px;
margin: -2px;
padding: 0.5rem;
margin: -0.5rem;
display: inline-block;
}

View File

@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { 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';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
hasUserCharacters: boolean;
showIcon?: boolean;
}
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const [settings] = useLocalCharacterWidgetSettings();
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
const theme = useTheme();
const pilotTooltipContent = useMemo(() => {
return (
<div
style={{
width: '100%',
minWidth: '300px',
overflow: 'hidden',
}}
>
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} autoSize={true} />
</div>
);
}, [localCounterCharacters, itemTemplate]);
if (localCounterCharacters.length === 0) {
return null;
}
return (
<div
className={clsx(classes.TooltipActive, {
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
})}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>
</div>
</div>
</WdTooltipWrapper>
</div>
);
};

View File

@@ -1,15 +1,10 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@keyframes move-stripes {
from {
background-position: 0 0;
}
to {
background-position: 30px 0;
}
}
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
.RootCustomNode {
display: flex;
@@ -26,19 +21,18 @@
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
box-shadow: 0 0 5px rgba(v.$dark-bg, 0.5);
border: 1px solid color.adjust(v.$pastel-blue, $lightness: -10%);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid darken($pastel-blue, 10%);
border-radius: 5px;
position: relative;
z-index: 3;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::after {
&::before {
content: '';
position: absolute;
top: 0;
@@ -54,7 +48,7 @@
}
&.Mataria {
&::after {
&::before {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -63,7 +57,7 @@
}
&.Caldaria {
&::after {
&::before {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -72,7 +66,7 @@
}
&.Amarria {
&::after {
&::before {
opacity: 0.45;
background-image: url('/images/amarr-180.png');
background-position-x: 0;
@@ -81,7 +75,7 @@
}
&.Gallente {
&::after {
&::before {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
background-position-x: 1px;
@@ -89,43 +83,11 @@
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -13px;
}
}
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
&.rally {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-color: v.$neon-color-1;
background: repeating-linear-gradient(
45deg,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
transparent 8px,
transparent 21px
);
background-size: 30px 30px;
animation: move-stripes 3s linear infinite;
}
}
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
@@ -146,7 +108,7 @@
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
}
}
@@ -347,13 +309,13 @@
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid v.$pastel-blue;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
pointer-events: auto;
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
}
&.HandleTop {

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -12,28 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
return (
<>
{nodeVars.visible && (
<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 && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -42,27 +40,21 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
)}
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
killsActivityType={localKillsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</KillsCounter>
)}
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -75,11 +67,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -97,11 +86,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<Tag
value={nodeVars.tag}
severity="warning"
className="py-0 px-[2px] text-[9px] [&_.p-tag-value]:leading-[1.3]"
></Tag>
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
)}
<div
@@ -141,26 +126,12 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center gap-0.5 justify-end">
<div className={clsx('flex items-center gap-0.5')}>
<div className="flex items-center gap-1 justify-end">
<div className={clsx('flex items-center gap-1')}>
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
)}
{nodeVars.description != null && nodeVars.description !== '' && (
<WdTooltipWrapper
className="h-[15px] transform -translate-y-[6%]"
position={TooltipPosition.top}
content={`System have description`}
>
<i
className={clsx(
'pi hero-chat-bubble-bottom-center-text w-[10px] h-[10px]',
'text-[8px] relative top-[1px]',
)}
/>
</WdTooltipWrapper>
)}
</div>
<LocalCounter
@@ -193,17 +164,6 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -1,4 +1,4 @@
@use './SolarSystemNodeDefault.module.scss';
@import './SolarSystemNodeDefault.module.scss';
/* ---------------------------------------------
Only override what's different from the base

View File

@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -12,26 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
return (
<>
{nodeVars.visible && (
<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 && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -40,27 +40,21 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</div>
)}
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && localKillsActivityType && (
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
size={TooltipSize.lg}
killsActivityType={localKillsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[localKillsActivityType])}
killsActivityType={nodeVars.killsActivityType}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</KillsCounter>
)}
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -74,10 +68,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -122,13 +113,23 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<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}
</div>
)}
{!nodeVars.isWormhole && !nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<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}
</div>
)}
@@ -173,17 +174,6 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -1,4 +1,4 @@
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.Signature {
position: relative;

View File

@@ -1,16 +1,15 @@
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
import classes from './UnsplashedSignature.module.scss';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import { useMemo } from 'react';
import clsx from 'clsx';
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TimeStatus } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
import clsx from 'clsx';
import { useMemo } from 'react';
import classes from './UnsplashedSignature.module.scss';
interface UnsplashedSignatureProps {
signature: SystemSignature;
@@ -36,7 +35,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
}, [customInfo]);
const isEOL = useMemo(() => {
return customInfo?.time_status === TimeStatus._1h;
return customInfo?.isEOL;
}, [customInfo]);
const whClassStyle = useMemo(() => {
@@ -59,7 +58,6 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
</InfoDrawer>
</div>
}
smallPaddings
>
<div className={clsx(classes.Box, whClassStyle)}>
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">

View File

@@ -16,7 +16,7 @@ export enum SOLAR_SYSTEM_CLASS_IDS {
thera = 12,
c13 = 13,
sentinel = 14,
barbican = 15,
baribican = 15,
vidette = 16,
conflux = 17,
redoubt = 18,
@@ -82,7 +82,7 @@ export const SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS = {
thera: SOLAR_SYSTEM_CLASS_GROUPS.thera,
c13: SOLAR_SYSTEM_CLASS_GROUPS.c13,
sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
barbican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
baribican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
@@ -217,7 +217,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 14,
effectPower: 2,
title: 'Class 14 (Sentinel Drifter)',
shortTitle: 'Sentinel MZ',
shortTitle: 'Sentinel',
},
{
id: 'barbican',
@@ -225,7 +225,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 15,
effectPower: 2,
title: 'Class 15 (Barbican Drifter)',
shortTitle: 'Liberated Barbican',
shortTitle: 'Barbican',
},
{
id: 'vidette',
@@ -233,7 +233,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 16,
effectPower: 2,
title: 'Class 16 (Vidette Drifter)',
shortTitle: 'Sanctified Vidette',
shortTitle: 'Vidette',
},
{
id: 'conflux',
@@ -241,7 +241,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 17,
effectPower: 2,
title: 'Class 17 (Conflux Drifter)',
shortTitle: 'Conflux Eyrie',
shortTitle: 'Conflux',
},
{
id: 'redoubt',
@@ -249,7 +249,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 18,
effectPower: 2,
title: 'Class 18 (Redoubt Drifter)',
shortTitle: 'Azdaja Redoubt',
shortTitle: 'Redoubt',
},
{
id: 'a1',
@@ -716,12 +716,11 @@ export const STATUS_CLASSES: Record<number, string> = {
[STATUSES.dangerous]: 'eve-system-status-dangerous',
};
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
export const TYPE_NAMES = {
[ConnectionType.wormhole]: 'Wormhole',
[ConnectionType.gate]: 'Gate',
[ConnectionType.bridge]: 'Jumpgate',
};
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];

View File

@@ -1,5 +0,0 @@
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
return targetPermission != null && permissions[targetPermission];
};

View File

@@ -4,4 +4,3 @@ export * from './getSystemClassStyles';
export * from './getShapeClass';
export * from './getBackgroundClass';
export * from './prepareUnsplashedChunks';
export * from './checkPermissions';

View File

@@ -15,12 +15,3 @@ export const isKnownSpace = (wormholeClassID: number) => {
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
return spaces.includes(wormholeClassID);
};
export const isNullsecSpace = (wormholeClassID: number) => {
switch (wormholeClassID) {
case SOLAR_SYSTEM_CLASS_IDS.ns:
return true;
}
return false;
};

View File

@@ -10,7 +10,7 @@ export const isWormholeSpace = (wormholeClassID: number) => {
case SOLAR_SYSTEM_CLASS_IDS.c6:
case SOLAR_SYSTEM_CLASS_IDS.c13:
case SOLAR_SYSTEM_CLASS_IDS.thera:
case SOLAR_SYSTEM_CLASS_IDS.barbican:
case SOLAR_SYSTEM_CLASS_IDS.baribican:
case SOLAR_SYSTEM_CLASS_IDS.vidette:
case SOLAR_SYSTEM_CLASS_IDS.conflux:
case SOLAR_SYSTEM_CLASS_IDS.redoubt:

View File

@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
export * from './useCommandsConnections';
export * from './useCommandsConnections';
export * from './useCenterSystem';
export * from './useSelectSystems';
export * from './useSelectSystem';
export * from './useMapCommands';

View File

@@ -1,18 +1,12 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandCenterSystem } from '@/hooks/Mapper/types';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SYSTEM_FOCUSED_LIFETIME } from '@/hooks/Mapper/constants.ts';
export const useCenterSystem = () => {
const rf = useReactFlow();
const { update } = useMapState();
const ref = useRef({ rf, update });
ref.current = { rf, update };
const highlightTimeout = useRef<number>();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandCenterSystem) => {
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId);
@@ -20,16 +14,5 @@ export const useCenterSystem = () => {
return;
}
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
ref.current.update({ systemHighlighted: systemId });
if (highlightTimeout.current !== undefined) {
clearTimeout(highlightTimeout.current);
}
highlightTimeout.current = setTimeout(() => {
highlightTimeout.current = undefined;
ref.current.update({ systemHighlighted: undefined });
}, SYSTEM_FOCUSED_LIFETIME);
}, []);
};

View File

@@ -1,4 +1,5 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -6,7 +7,6 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapState();
@@ -14,27 +14,8 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
ref.current.update(state => {
const existing = state.characters ?? [];
// Put updatedCharacters into a map keyed by ID
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
// 1. Update existing characters when possible
const merged = existing.map(character => {
const updated = updatedMap.get(character.eve_id);
if (updated) {
updatedMap.delete(character.eve_id); // Mark as processed
return { ...character, ...updated };
}
return character;
});
// 2. Any remaining items in updatedMap are NEW characters → add them
const newCharacters = Array.from(updatedMap.values());
return { characters: [...merged, ...newCharacters] };
});
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

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

View File

@@ -1,10 +1,8 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useEventBuffer } from '@/hooks/Mapper/hooks';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapInit = () => {
const rf = useReactFlow();
@@ -13,20 +11,6 @@ export const useMapInit = () => {
const ref = useRef({ rf, data, update });
ref.current = { update, data, rf };
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
const { rf } = ref.current;
rf.setNodes(systems.map(convertSystem2Node));
}, []);
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
const { rf } = ref.current;
rf.setEdges(connections.map(convertConnection2Edge));
}, []);
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
return useCallback(
({
systems,
@@ -38,10 +22,9 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
options,
user_permissions,
}: CommandInit) => {
const { update } = ref.current;
const { rf } = ref.current;
const updateData: Partial<MapData> = {};
@@ -65,14 +48,6 @@ export const useMapInit = () => {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) {
updateData.systems = systems;
}
@@ -88,13 +63,11 @@ export const useMapInit = () => {
update(updateData);
if (systems) {
handleUpdateSystems(systems);
// rf.setNodes(systems.map(convertSystem2Node));
rf.setNodes(systems.map(convertSystem2Node));
}
if (connections) {
handleUpdateConnections(connections);
// rf.setEdges(connections.map(convertConnection2Edge));
rf.setEdges(connections.map(convertConnection2Edge));
}
},
[],

View File

@@ -0,0 +1,21 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystem } from '@/hooks/Mapper/types';
export const useSelectSystem = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandSelectSystem) => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: node.id === systemId,
};
}),
);
}, []);
};

View File

@@ -1,31 +0,0 @@
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();
const ref = useRef({ rf, onSelectionChange });
ref.current = { rf, onSelectionChange };
return useCallback(({ systems, delay }: CommandSelectSystems) => {
const run = () => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: systems.includes(node.id),
};
}),
);
};
if (delay == null || delay === 0) {
run();
return;
}
setTimeout(run, delay);
}, []);
};

View File

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

View File

@@ -1,3 +1,4 @@
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
@@ -13,16 +14,12 @@ import {
CommandRemoveSystems,
Commands,
CommandSelectSystem,
CommandSelectSystems,
CommandUpdateConnection,
CommandUpdateSystems,
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import {
useCenterSystem,
useCommandsCharacters,
useCommandsConnections,
useMapAddSystems,
@@ -30,8 +27,10 @@ import {
useMapInit,
useMapRemoveSystems,
useMapUpdateSystems,
useSelectSystems,
useCenterSystem,
useSelectSystem,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
const mapInit = useMapInit();
@@ -39,7 +38,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const mapUpdateSystems = useMapUpdateSystems();
const removeSystems = useMapRemoveSystems(onSelectionChange);
const centerSystem = useCenterSystem();
const selectSystems = useSelectSystems(onSelectionChange);
const selectSystem = useSelectSystem();
const selectRef = useRef({ onSelectionChange });
selectRef.current = { onSelectionChange };
@@ -106,25 +105,45 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
setTimeout(() => {
const systemId = `${data}`;
selectRef.current.onSelectionChange({
systems: [systemId],
connections: [],
});
selectSystem(systemId as CommandSelectSystem);
}, 500);
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
// do nothing here
break;
case Commands.signaturesUpdated:
// do nothing here
break;
case Commands.linkSignatureToSystem:
// do nothing here
break;
case Commands.detailedKillsUpdated:
// do nothing here
break;
case Commands.characterActivityData:
break;
case Commands.trackingCharactersData:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
// do nothing
break;
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 { Commands } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface Kill {
solar_system_id: number | string;
@@ -10,78 +9,34 @@ interface Kill {
interface MapEvent {
name: Commands;
data?: unknown;
data?: any;
payload?: Kill[];
}
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null = null): { killsCount: number | null; killsActivityType: string | null } {
export function useNodeKillsCount(
systemId: number | string,
initialKillsCount: number | null
): number | null {
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(() => {
// Always prefer the calculated 1-hour count over initial count
// This ensures we properly expire old kills
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]);
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
const handleEvent = useCallback(
(event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(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 (!detailedKills[systemId] || detailedKills[systemId].length === 0) {
setKillsCount(killForSystem.kills);
}
}
return true;
const handleEvent = useCallback((event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(
kill => kill.solar_system_id.toString() === systemId.toString()
);
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
}
return false;
},
[systemId, detailedKills],
);
return true;
}
return false;
}, [systemId]);
useMapEventListener(handleEvent);
const killsActivityType = useMemo(() => {
return killsCount !== null && killsCount > 0 ? getActivityType(killsCount) : null;
}, [killsCount]);
return { killsCount, killsActivityType };
return killsCount;
}

View File

@@ -5,57 +5,43 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/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 { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
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;
description: string | null;
comments_count: number | null;
systemHighlighted: string | undefined;
function getActivityType(count: number): string {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
}
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
const localCounterCharacters = useMemo(() => {
return nodeVars.charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: nodeVars.userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
return { localCounterCharacters };
}
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props;
const {
id: solar_system_id,
@@ -66,19 +52,13 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
labels,
temporary_name,
linked_sig_eve_id: linkedSigEveId = '',
description,
comments_count,
} = data;
const {
storedSettings: { interfaceSettings },
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const systemStaticInfo = useMemo(() => {
return getSystemStaticInfo(solar_system_id)!;
}, [solar_system_id]);
const {
system_class,
security,
@@ -89,8 +69,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
region_id,
is_shattered,
solar_system_name,
constellation_name,
} = systemStaticInfo;
} = useMemo(() => {
return getSystemStaticInfo(parseInt(solar_system_id))!;
}, [solar_system_id]);
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
@@ -102,14 +83,13 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
characters,
wormholesData,
hubs,
kills,
userCharacters,
isConnecting,
hoverNodeId,
visibleNodes,
showKSpaceBG,
isThickConnections,
pings,
systemHighlighted,
},
outCommand,
} = useMapState();
@@ -139,6 +119,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isShowLinkedSigId,
});
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
[charactersInSystem, userCharacters],
@@ -147,47 +130,36 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
data: { system_id: solar_system_id },
data: { system_id: solar_system_id.toString() },
});
});
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SPACE_TO_CLASS[space] || null : null;
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name: solar_system_name || '',
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
});
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
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 = {
id,
selected,
visible,
isWormhole,
classTitleColor,
killsCount,
killsActivityType,
hasUserCharacters,
userCharacters,
showHandlers,
@@ -214,13 +186,47 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections,
classTitle: class_title,
temporaryName: computedTemporaryName,
regionName,
regionName: region_name,
solarSystemName: solar_system_name,
isRally,
description,
comments_count,
systemHighlighted,
};
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';
interface UseSystemNameParams {
isTempSystemNameEnabled: boolean;
temporary_name?: string | null;
solar_system_name: string;
isShowLinkedSigIdTempName: boolean;
linkedSigPrefix: string | null;
name?: string | null;
systemStaticInfo: SolarSystemStaticInfoRaw;
}
export const useSystemName = ({
export function useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name,
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
}: UseSystemNameParams) => {
const { solar_system_name = '' } = systemStaticInfo;
}: UseSystemNameParams) {
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
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 ?? '';
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
@@ -36,7 +32,6 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
@@ -44,13 +39,11 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
return { systemName, computedTemporaryName, customName };
};
}

View File

@@ -1,7 +1,7 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);

View File

@@ -10,5 +10,3 @@ export type OnMapSelectionChange = (event: {
}) => void;
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;
export type MapViewport = { zoom: 1; x: 0; y: 0 };

View File

@@ -1,5 +1,5 @@
@use './eve-common-variables';
@use './eve-common';
@import './eve-common-variables';
@import './eve-common';
.default-theme {
--rf-bg-color: #0C0A09;

View File

@@ -1,19 +1,18 @@
@use "sass:color";
$friendlyBase: #3bbd39;
$friendlyAlpha: #3bbd3952;
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
$friendlyDark5: color.adjust($friendlyBase, $lightness: -5%);
$friendlyDark20: darken($friendlyBase, 20%);
$friendlyDark30: darken($friendlyBase, 30%);
$friendlyDark5: darken($friendlyBase, 5%);
$lookingForBase: #43c2fd;
$lookingForAlpha: rgba(67, 176, 253, 0.48);
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
$lookingForDark15: darken($lookingForBase, 15%);
$homeBase: rgb(179, 253, 67);
$homeAlpha: rgba(186, 248, 48, 0.32);
$homeBackground: #a0fa5636;
$homeDark30: color.adjust($homeBase, $lightness: -30%);
$homeDark30: darken($homeBase, 30%);
:root {
--pastel-blue: #5a7d9a;
@@ -118,7 +117,6 @@ $homeDark30: color.adjust($homeBase, $lightness: -30%);
--conn-time-eol: #7452c3e3;
--conn-frigate: #325d88;
--conn-bridge: rgba(135, 185, 93, 0.85);
--conn-save: rgba(155, 102, 45, 0.85);
--selected-item-bg: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,4 +1,4 @@
@use './eve-common-variables';
@import './eve-common-variables';
.eve-wh-effect-color-pulsar {

View File

@@ -1,2 +1,2 @@
@use './default-theme.scss';
@use './pathfinder-theme.scss';
@import './default-theme.scss';
@import './pathfinder-theme.scss';

View File

@@ -1,11 +1,10 @@
@use "sass:color";
@use './eve-common-variables';
@use './eve-common';
@import './eve-common-variables';
@import './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
$homeBase: rgb(197, 253, 67);
$homeAlpha: rgba(197, 253, 67, 0.32);
$homeDark30: color.adjust($homeBase, $lightness: -30%);
$homeDark30: darken($homeBase, 30%);
.pathfinder-theme {
/* -- Override values from the default theme -- */

View File

@@ -1,8 +0,0 @@
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);

View File

@@ -1,48 +1,37 @@
import { Position, internalsSymbol, Node } from 'reactflow';
import { Position, internalsSymbol } from 'reactflow';
type Coords = [number, number];
type CoordsWithPosition = [number, number, Position];
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 {
// returns the position (top,right,bottom or right) passed node compared to
function getParams(nodeA, nodeB) {
const centerA = getNodeCenter(nodeA);
const centerB = getNodeCenter(nodeB);
const horizontalDiff = Math.abs(centerA.x - centerB.x);
const verticalDiff = Math.abs(centerA.y - centerB.y);
let position: Position;
if (
segmentsIntersect(
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 {
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
if (horizontalDiff > verticalDiff) {
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);
return [x, y, position];
}
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords {
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}`);
}
function getHandleCoordsByPosition(node, 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);
let offsetX = handle.width / 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) {
case Position.Left:
offsetX = 0;
@@ -58,20 +47,21 @@ function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords
break;
}
const x = node.positionAbsolute!.x + handle.x + offsetX;
const y = node.positionAbsolute!.y + handle.y + offsetY;
const x = node.positionAbsolute.x + handle.x + offsetX;
const y = node.positionAbsolute.y + handle.y + offsetY;
return [x, y];
}
function getNodeCenter(node: Node): { x: number; y: number } {
function getNodeCenter(node) {
return {
x: node.positionAbsolute!.x + node.width! / 2,
y: node.positionAbsolute!.y + node.height! / 2,
x: node.positionAbsolute.x + node.width / 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 [tx, ty, targetPos] = getParams(target, source);

View File

@@ -4,13 +4,10 @@ import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constant
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const MapInterface = () => {
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
const { windowsSettings, updateWidgetSettings } = useMapRootState();
const items = useMemo(() => {
if (Object.keys(windowsSettings).length === 0) {
return [];
}
return windowsSettings.windows
.map(x => {
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;

View File

@@ -1,15 +1,16 @@
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import classes from './AddSystemDialog.module.scss';
import clsx from 'clsx';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
@@ -33,7 +34,6 @@ export const AddSystemDialog = ({
data: { wormholesData },
} = useMapRootState();
// TODO fix it
const inputRef = useRef<any>();
const onShow = useCallback(() => {
inputRef.current?.focus();
@@ -62,7 +62,6 @@ export const AddSystemDialog = ({
},
});
// TODO fix it
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
const amatch = a.label.indexOf(query);
const bmatch = b.label.indexOf(query);
@@ -115,93 +114,90 @@ export const AddSystemDialog = ({
setVisible(false);
}}
>
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton
type="submit"
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
</form>
<div className="flex gap-2 justify-end">
<Button
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</Dialog>
);
};

View File

@@ -1,7 +1,7 @@
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useEffect, useRef, useState } from 'react';
import { CommentType } from '@/hooks/Mapper/types';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsProps {}
@@ -14,9 +14,7 @@ export const Comments = ({}: CommentsProps) => {
comments: { loadComments, comments, lastUpdateKey },
} = useMapRootState();
const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const [systemId] = selectedSystems;
const ref = useRef({ loadComments, systemId });
ref.current = { loadComments, systemId };

View File

@@ -1,5 +1,8 @@
.MarkdownTextViewer {
.MarkdownCommentRoot {
border-left-width: 3px;
@apply text-[12px] leading-[1.2] text-stone-300 break-words;
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
.h1 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
@@ -53,10 +56,6 @@
@apply font-bold text-green-400 break-words whitespace-normal;
}
strong {
font-weight: bold;
}
i, em {
@apply italic text-pink-400 break-words whitespace-normal;
}

View File

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

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