Compare commits

..

3 Commits

Author SHA1 Message Date
Dmitry Popov
1b4852d5b4 Merge pull request #517 from jackmurray/show-temp-name
Show system temp name when linking signatures
2025-10-10 00:00:41 +04:00
Jack
4bfe60b75c preferentially display the system's temporary name if it has one 2025-09-16 19:58:21 +00:00
Jack
6a44d10c56 enable display of custom name on the connection dropdown 2025-09-16 19:58:00 +00:00
476 changed files with 138603 additions and 46570 deletions

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>"
@@ -16,8 +14,3 @@ export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
# Promo codes for map subscriptions (optional)
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
# Codes are case-insensitive, discounts stack with period discounts
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- develop
env:
MIX_ENV: prod
@@ -21,7 +22,7 @@ jobs:
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
@@ -36,7 +37,7 @@ jobs:
elixir: ["1.17"]
node-version: ["18.x"]
outputs:
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
steps:
- name: Prepare
run: |
@@ -90,6 +91,7 @@ jobs:
- name: Generate Changelog & Update Tag Version
id: generate-changelog
if: github.ref == 'refs/heads/main'
run: |
git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com'
@@ -100,16 +102,15 @@ jobs:
- name: Set commit hash for develop
id: set-commit-develop
if: github.ref == 'refs/heads/develop'
run: |
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker:
name: 🛠 Build Docker Images
if: github.ref == 'refs/heads/develop'
needs: build
runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
@@ -136,17 +137,6 @@ jobs:
ref: ${{ needs.build.outputs.commit_hash }}
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: 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
@@ -195,24 +185,6 @@ jobs:
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
with:
stringToTruncate: |
📣 Wanderer new release available 🎉
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
@@ -243,9 +215,8 @@ jobs:
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
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
@@ -288,14 +259,3 @@ jobs:
## How to Promote?
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:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

187
.github/workflows/docker-arm.yml vendored Normal file
View File

@@ -0,0 +1,187 @@
name: Build Docker ARM Image
on:
push:
tags:
- '**'
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
REGISTRY_IMAGE: wandererltd/community-edition-arm
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
docker:
name: 🛠 Build Docker Images
runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
fail-fast: false
matrix:
platform:
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ steps.get-latest-tag.outputs.tag }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
build-args: |
MIX_ENV=prod
BUILD_METADATA=${{ steps.meta.outputs.json }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
with:
stringToTruncate: |
📣 Wanderer **ARM** release available 🎉
**Version**: :${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
- docker
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

View File

@@ -1,9 +1,9 @@
name: Build Develop
name: Build Docker Image
on:
push:
branches:
- develop
tags:
- '**'
env:
MIX_ENV: prod
@@ -18,85 +18,12 @@ 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
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
@@ -110,7 +37,6 @@ jobs:
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
@@ -120,9 +46,25 @@ jobs:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ steps.get-latest-tag.outputs.tag }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
@@ -171,6 +113,24 @@ jobs:
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
with:
stringToTruncate: |
📣 Wanderer new release available 🎉
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
@@ -201,8 +161,9 @@ jobs:
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' }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
- name: Create manifest list and push
working-directory: /tmp/digests
@@ -215,20 +176,12 @@ jobs:
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about develop release
name: 🏷 Notify about release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL_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.
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

View File

@@ -21,7 +21,7 @@ jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
@@ -35,17 +35,17 @@ jobs:
--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:
@@ -54,12 +54,12 @@ jobs:
_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: |
@@ -71,42 +71,42 @@ jobs:
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
@@ -115,16 +115,16 @@ jobs:
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)
@@ -132,26 +132,26 @@ jobs:
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
@@ -161,14 +161,14 @@ jobs:
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")
@@ -183,12 +183,12 @@ jobs:
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
@@ -198,24 +198,24 @@ jobs:
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
@@ -225,7 +225,7 @@ jobs:
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Create test results summary
id: summary
run: |
@@ -236,11 +236,11 @@ jobs:
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
@@ -252,7 +252,7 @@ jobs:
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
@@ -261,7 +261,7 @@ jobs:
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
@@ -271,11 +271,11 @@ jobs:
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` |
@@ -284,50 +284,50 @@ jobs:
| 📊 **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)
- **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.
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.

3
.gitignore vendored
View File

@@ -17,9 +17,6 @@ repomix*
/priv/static/images/
/priv/static/*.js
/priv/static/*.css
/priv/static/*-*.png
/priv/static/*-*.webp
/priv/static/*-*.webmanifest
# Dialyzer PLT files
/priv/plts/

File diff suppressed because it is too large Load Diff

View File

@@ -30,60 +30,10 @@ format f:
mix format
test t:
MIX_ENV=test mix test
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
test-parallel tp:
@echo "Running tests in 4 parallel partitions..."
@mkdir -p /tmp/wanderer_test_results
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
@for i in 1 2 3 4; do \
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
done; \
wait
@echo ""
@echo "========================================"
@echo " TEST RESULTS SUMMARY"
@echo "========================================"
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
total_tests=$$((total_tests + tests)); \
total_failures=$$((total_failures + failures)); \
total_excluded=$$((total_excluded + excluded)); \
if [ "$$exit_code" = "0" ]; then \
echo "Partition $$i: ✓ $$summary"; \
else \
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
fi; \
done; \
echo "========================================"; \
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
echo "========================================"; \
if [ "$$all_passed" = "true" ]; then \
echo "✓ All partitions passed!"; \
else \
echo "✗ Some partitions failed. Details below:"; \
echo ""; \
for i in 1 2 3 4; do \
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
if [ "$$exit_code" != "0" ]; then \
echo "======== PARTITION $$i FAILURES ========"; \
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
echo ""; \
fi; \
done; \
exit 1; \
fi
mix test
coverage cover co:
MIX_ENV=test mix test --cover
mix test --cover
unit-tests ut:
@echo "Running unit tests..."
@@ -95,3 +45,4 @@ versions v:
@cat .tool-versions
@cat Aptfile
@echo

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%;
@@ -1001,27 +999,3 @@ body > div:first-of-type {
.verticalTabsContainer .p-tabview-panel {
flex-grow: 1;
}
/* Blog post CTA links - only in main post content */
.post-content a {
display: inline-block;
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
color: white !important;
padding: 0.5rem 1.25rem;
border-radius: 0.5rem;
text-decoration: none !important;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
}
.post-content a:hover {
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
}
.post-content a:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
}

View File

@@ -9,7 +9,6 @@ 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';
const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -40,15 +39,13 @@ export default function MapRoot({ hooks }) {
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

@@ -284,7 +284,3 @@
border-left-color: #e67e22;
}
.p-dialog-header-icon.p-dialog-header-close.p-link {
position: relative;
left: 6px;
}

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 200px;
min-height: 400px;
.p-tabview {
width: 100%;

View File

@@ -51,8 +51,20 @@ export const Characters = ({ data }: CharactersProps) => {
['border-lime-600/70']: character.online,
},
)}
title={character.name}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(

View File

@@ -118,11 +118,7 @@ export const useContextMenuSystemItems = ({
});
if (isShowPingBtn) {
return (
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
);
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
}
return (
@@ -130,7 +126,7 @@ export const useContextMenuSystemItems = ({
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]">
<WdMenuItem disabled icon={iconClasses}>
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>

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

@@ -6,34 +6,27 @@ 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 },
data: { pings },
} = 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 ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
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;
}
@@ -48,34 +41,11 @@ export const useContextMenuSystemMultipleHandlers = () => {
}
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, ping]);
return {
handleSystemMultipleContext,
contextMenuRef,
onDeleteSystems,
onCopySystems,
};
};

View File

@@ -1,10 +1,10 @@
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useCallback, useRef } from 'react';
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'),
[],
);

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,4 +1,4 @@
import { MapUserSettings, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const REQUIRED_KEYS = [
'widgets',
@@ -19,8 +19,11 @@ export class MapUserSettingsParseError extends Error {
}
}
/** Minimal check that an object matches SettingsWrapper<*> */
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
@@ -49,8 +52,8 @@ export const parseMapUserSettings = (json: unknown): MapUserSettings => {
}
for (const key of REQUIRED_KEYS) {
if (!isSettings((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<T>`);
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
}
}

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,10 +1,11 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { SESSION_KEY } from '@/hooks/Mapper/constants.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,
@@ -32,9 +33,19 @@ import {
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
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';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
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 +88,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;
@@ -89,7 +99,6 @@ interface MapCompProps {
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
defaultViewport?: Viewport;
}
const MapComp = ({
@@ -110,25 +119,19 @@ const MapComp = ({
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 +187,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[]) => {
@@ -216,19 +218,6 @@ const MapComp = ({
}));
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
const prevViewport = usePrevious(defaultViewport);
useEffect(() => {
if (defaultViewport == null) {
return;
}
if (prevViewport == null) {
return;
}
setViewport(defaultViewport);
}, [defaultViewport, prevViewport, setViewport]);
return (
<>
<div
@@ -243,7 +232,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}
@@ -281,9 +270,9 @@ const MapComp = ({
deleteKeyCode={['']}
{...(isPanAndDrag
? {
selectionOnDrag: true,
panOnDrag: [2],
}
selectionOnDrag: true,
panOnDrag: [2],
}
: {})}
// TODO need create clear example with problem with that flag
// if system is not visible edge not drawing (and any render in Custom node is not happening)

View File

@@ -11,7 +11,6 @@ export type MapData = MapUnionTypes & {
isThickConnections: boolean;
linkedSigEveId: string;
localShowShipName: boolean;
systemHighlighted: string | undefined;
};
interface MapProviderProps {
@@ -45,7 +44,6 @@ const INITIAL_DATA: MapData = {
userHubs: [],
pings: [],
localShowShipName: false,
systemHighlighted: undefined,
};
export interface MapContextProps {

View File

@@ -2,77 +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;
onAutoLayout(): void;
}
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
contextMenuRef,
onAddSystem,
onPasteSystemsAnsConnections,
onAutoLayout,
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,
},
{
label: 'Auto Layout',
icon: PrimeIcons.REFRESH,
command: onAutoLayout,
},
...(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,93 +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 onAutoLayout = useCallback(async () => {
const { onCommand } = ref.current;
if (!onCommand) {
return;
}
console.log('Auto layouting systems');
await onCommand({
type: OutCommand.layoutSystems,
data: {
system_ids: [], // Layout all systems
},
});
}, []);
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,
onAutoLayout,
};
};

View File

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

View File

@@ -13,19 +13,9 @@ 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) => {
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
const {
data: { localShowShipName },
} = useMapState();
@@ -52,30 +42,22 @@ export const LocalCounter = ({
return (
<div
className={clsx(
classes.TooltipActive,
{
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
},
className,
)}
className={clsx(classes.TooltipActive, {
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={!disableInteractive}
interactive={true}
smallPaddings
>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(
classes.localCounter,
{
[classes.hasUserCharacters]: hasUserCharacters,
},
contentClassName,
)}
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
})}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>

View File

@@ -46,10 +46,6 @@
stroke-dasharray: 10 5;
stroke-linecap: round;
}
&.CrossList {
stroke: #ff0000;
}
}
.EdgePathFront {
@@ -97,10 +93,6 @@
stroke-width: 3px;
}
}
&.CrossList {
stroke: #ff0000;
}
}
.ClickPath {

View File

@@ -85,7 +85,6 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.Hovered]: hovered,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.CrossList]: data.is_cross_list,
})}
d={path}
markerEnd={markerEnd}
@@ -101,7 +100,6 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
[classes.Gate]: isGate,
[classes.Bridge]: isBridge,
[classes.CrossList]: data.is_cross_list,
})}
d={path}
markerEnd={markerEnd}

View File

@@ -1,6 +1,14 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);
@keyframes move-stripes {
from {
@@ -26,8 +34,8 @@
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 color.adjust($pastel-blue, $lightness: -10%);
border-radius: 5px;
position: relative;
z-index: 3;
@@ -99,7 +107,7 @@
}
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -113,11 +121,11 @@
bottom: 0;
z-index: -1;
border-color: v.$neon-color-1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
$neon-color-3 0px,
$neon-color-3 8px,
transparent 8px,
transparent 21px
);
@@ -146,7 +154,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 +355,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,
@@ -17,12 +17,10 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
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,
@@ -141,26 +139,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 +177,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

@@ -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,
@@ -16,7 +16,6 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
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>) => {
@@ -173,17 +172,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,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(() => {

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

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

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

@@ -17,15 +17,8 @@ export const useCommandsConnections = () => {
}, []);
const updateConnection = useCallback((value: CommandUpdateConnection) => {
const newEdge = convertConnection2Edge(value);
ref.current.rf.setEdges(eds => {
const exists = eds.find(e => e.id === newEdge.id);
if (exists) {
return eds.map(e => e.id === newEdge.id ? newEdge : e);
} else {
return [...eds, newEdge];
}
});
ref.current.rf.deleteElements({ edges: [value] });
ref.current.rf.addEdges([convertConnection2Edge(value)]);
}, []);
return { addConnections, removeConnections, updateConnection };

View File

@@ -38,8 +38,6 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
options,
user_permissions,
}: CommandInit) => {
const { update } = ref.current;
@@ -65,14 +63,6 @@ export const useMapInit = () => {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) {
updateData.systems = systems;
}

View File

@@ -62,7 +62,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
setTimeout(() => mapUpdateSystems(data as CommandUpdateSystems), 100);
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
@@ -89,7 +89,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
setTimeout(() => updateConnection(data as CommandUpdateConnection), 100);
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);

View File

@@ -5,7 +5,7 @@ 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, 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';
@@ -50,9 +50,27 @@ export interface SolarSystemNodeVars {
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
description: string | null;
comments_count: number | null;
systemHighlighted: string | undefined;
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
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 const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
@@ -66,13 +84,11 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
labels,
temporary_name,
linked_sig_eve_id: linkedSigEveId = '',
description,
comments_count,
} = data;
const {
storedSettings: { interfaceSettings },
data: { systemSignatures: mapSystemSignatures, pings },
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const systemStaticInfo = useMemo(() => {
@@ -108,7 +124,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
visibleNodes,
showKSpaceBG,
isThickConnections,
systemHighlighted,
pings,
},
outCommand,
} = useMapState();
@@ -153,7 +169,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
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,
@@ -216,9 +232,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
regionName,
solarSystemName: solar_system_name,
isRally,
description,
comments_count,
systemHighlighted,
};
return nodeVars;

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

@@ -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,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,3 +1,4 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx';
import {
InfoDrawer,
@@ -48,11 +49,7 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
<>
<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={

View File

@@ -1,9 +0,0 @@
.CERoot {
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-600/40;
}
}
}

View File

@@ -3,10 +3,9 @@ import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './CommentsEditor.module.scss';
export interface CommentsEditorProps {}
@@ -19,9 +18,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
outCommand,
} = useMapRootState();
const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const [systemId] = selectedSystems;
const ref = useRef({ outCommand, systemId, textVal });
ref.current = { outCommand, systemId, textVal };
@@ -51,7 +48,6 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
return (
<MarkdownEditor
className={classes.CERoot}
value={textVal}
onChange={setTextVal}
overlayContent={

View File

@@ -1,9 +1,9 @@
.CERoot {
@apply border border-stone-500/30 rounded-[2px];
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-950/70;
@apply bg-stone-600/40;
}
.cm-scroller {

View File

@@ -44,17 +44,9 @@ export interface MarkdownEditorProps {
overlayContent?: ReactNode;
value: string;
onChange: (value: string) => void;
height?: string;
className?: string;
}
export const MarkdownEditor = ({
value,
onChange,
overlayContent,
height = '70px',
className,
}: MarkdownEditorProps) => {
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => {
const [hasShift, setHasShift] = useState(false);
const refData = useRef({ onChange });
@@ -74,9 +66,9 @@ export const MarkdownEditor = ({
<div className={clsx(classes.MarkdownEditor, 'relative')}>
<CodeMirror
value={value}
height={height}
height="70px"
extensions={CODE_MIRROR_EXTENSIONS}
className={clsx(classes.CERoot, className)}
className={classes.CERoot}
theme={oneDark}
onChange={handleOnChange}
placeholder="Start typing..."

View File

@@ -121,7 +121,6 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
useEffect(() => {
if (!ping) {
setIsShow(false);
return;
}
@@ -162,26 +161,27 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
};
}, [interfaceSettings]);
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
if (!ping) {
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
// Only render Toast when there's a ping
return (
<>
{ping && (
<Toast
key={ping.id}
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<Toast
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full">
<div className="flex justify-between">
<div>
@@ -253,33 +253,28 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
{/*/>*/}
</div>
</section>
)}
></Toast>
)}
)}
></Toast>
{ping && (
<>
<WdButton
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<WdButton
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
)}
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
);
};

View File

@@ -3,18 +3,17 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import {
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -91,7 +90,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
if (k162TypeInfo) {
// Check if the k162Type matches our target system class
return k162TypeInfo.value.includes(targetSystemClassGroup);
return customInfo.k162Type === targetSystemClassGroup;
}
}
@@ -136,11 +135,6 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
[data, setVisible],
);
const { signatures } = useSystemSignaturesData({
systemId: `${data.solar_system_source}`,
settings: LINK_SIGNTATURE_SETTINGS,
});
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
@@ -158,12 +152,10 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
>
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
signatures={signatures}
hasUnsupportedLanguage={false}
settings={LINK_SIGNTATURE_SETTINGS}
hideLinkedSignatures
selectable
settings={LINK_SIGNTATURE_SETTINGS}
onSelect={handleSelect}
selectable={true}
filterSignature={filterSignature}
/>
</Dialog>

View File

@@ -8,8 +8,8 @@ import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
interface SystemSettingsDialog {
systemId: string;
@@ -125,7 +125,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label htmlFor="username">Custom name</label>
@@ -214,9 +214,13 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<div className="flex flex-col gap-1">
<label htmlFor="username">Description</label>
<div className="h-[200px]">
<MarkdownEditor value={description} onChange={e => setDescription(e)} height="180px" />
</div>
<InputTextarea
autoResize
rows={5}
cols={30}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
import { SystemInfoContent } from './SystemInfoContent';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
export const SystemInfo = () => {
const [visible, setVisible] = useState(false);
@@ -48,7 +48,7 @@ export const SystemInfo = () => {
</div>
<LayoutEventBlocker className="flex gap-1 items-center">
<a href={`https://zkillboard.com/system/${systemId}/`} rel="noreferrer" target="_blank">
<a href={`https://zkillboard.com/system/${systemId}`} rel="noreferrer" target="_blank">
<img src={ZKB_ICON} width="14" height="14" className="external-icon" />
</a>
<a href={`http://anoik.is/systems/${solarSystemName}`} rel="noreferrer" target="_blank">

View File

@@ -2,7 +2,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { useMemo } from 'react';
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
import { InfoDrawer, MarkdownTextViewer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemInfoContentProps {
@@ -51,7 +51,7 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
</div>
}
>
<MarkdownTextViewer>{description}</MarkdownTextViewer>
<div className="break-words">{description}</div>
</InfoDrawer>
)}
</div>

View File

@@ -1,16 +1,123 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useState } from 'react';
import { useSignatureUndo } from './hooks/useSignatureUndo';
import { useSystemSignaturesData } from './hooks/useSystemSignaturesData';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => {
const [showSettings, setShowSettings] = useState(false);
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const {
data: { selectedSystems },
@@ -20,6 +127,31 @@ export const SystemSignatures = () => {
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
settingsSignatures,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setVisible(false);
},
[settingsSignaturesUpdate],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
@@ -31,42 +163,7 @@ export const SystemSignatures = () => {
[settingsSignaturesUpdate],
);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings: settingsSignatures,
onLazyDeleteChange: handleLazyDeleteToggle,
});
const sigCount = useMemo(() => signatures.length, [signatures]);
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
const { countdown, handleUndo } = useSignatureUndo(systemId, settingsSignatures, deletedSignatures, outCommand);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (deletedSignatures.length > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
settingsSignaturesUpdate(newSettings);
setShowSettings(false);
},
[settingsSignaturesUpdate],
);
const openSettings = useCallback(() => setShowSettings(true), []);
const openSettings = useCallback(() => setVisible(true), []);
return (
<Widget
@@ -74,7 +171,7 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={deletedSignatures.length}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
@@ -90,21 +187,18 @@ export const SystemSignatures = () => {
) : (
<SystemSignaturesContent
systemId={systemId}
signatures={signatures}
selectedSignatures={selectedSignatures}
onSelectSignatures={setSelectedSignatures}
onDeleteSelected={handleDeleteSelected}
onSelectAll={handleSelectAll}
onPaste={handlePaste}
hasUnsupportedLanguage={hasUnsupportedLanguage}
settings={settingsSignatures}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
/>
)}
{showSettings && (
{visible && (
<SystemSignatureSettingsDialog
settings={settingsSignatures}
onCancel={() => setShowSettings(false)}
onCancel={() => setVisible(false)}
onSave={handleSettingsSave}
/>
)}

View File

@@ -33,39 +33,34 @@ import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
interface SystemSignaturesContentProps {
systemId: string;
signatures: ExtendedSystemSignature[];
selectedSignatures?: ExtendedSystemSignature[];
onSelectSignatures?: (s: ExtendedSystemSignature[]) => void;
onDeleteSelected?: () => Promise<void>;
onSelectAll?: () => void;
onPaste?: (clipboardString: string) => void;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
hasUnsupportedLanguage?: boolean;
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
}
export const SystemSignaturesContent = ({
systemId,
signatures,
selectedSignatures,
onSelectSignatures,
onDeleteSelected,
onSelectAll,
onPaste,
settings,
hideLinkedSignatures,
hasUnsupportedLanguage,
selectable,
onSelect,
onLazyDeleteChange,
onCountChange,
filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -84,18 +79,32 @@ export const SystemSignaturesContent = ({
const { clipboardContent, setClipboardContent } = useClipboard();
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings,
onCountChange,
onLazyDeleteChange,
onSignatureDeleted,
});
useEffect(() => {
if (selectable) return;
if (!clipboardContent?.text) return;
onPaste?.(clipboardContent.text);
handlePaste(clipboardContent.text);
setClipboardContent(null);
}, [selectable, clipboardContent, onPaste, setClipboardContent]);
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
useHotkey(true, ['a'], () => onSelectAll?.());
useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${SIGNATURE_WINDOW_ID}"]`);
@@ -108,7 +117,7 @@ export const SystemSignaturesContent = ({
event.stopPropagation();
// Delete key should always immediately delete, never show pending deletions
onDeleteSelected?.();
handleDeleteSelected();
});
const handleResize = useCallback(() => {
@@ -143,9 +152,9 @@ export const SystemSignaturesContent = ({
selectable
? onSelect?.(selectableSignatures[0])
: onSelectSignatures?.(selectableSignatures as ExtendedSystemSignature[]);
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
},
[onSelect, selectable, onSelectSignatures, deletedSignatures],
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
);
const {
@@ -168,6 +177,9 @@ export const SystemSignaturesContent = ({
);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
// Get the set of deleted signature IDs for quick lookup
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
if (filterSignature && !filterSignature(sig)) {
@@ -201,8 +213,24 @@ export const SystemSignaturesContent = ({
return settings[sig.kind] as boolean;
};
return signatures.filter(sig => shouldShowSignature(sig));
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
});
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
@@ -225,18 +253,20 @@ export const SystemSignaturesContent = ({
return getSignatureRowClass(
rowData as ExtendedSystemSignature,
refVars.current.selectedSignatures || [],
refVars.current.selectedSignatures,
refVars.current.settings[SETTINGS_KEYS.COLOR_BY_TYPE] as boolean,
);
}, []);
const handleSortSettings = useCallback((e: DataTableStateEvent) => {
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
});
}, []);
const handleSortSettings = useCallback(
(e: DataTableStateEvent) =>
refVars.current.settingsSignaturesUpdate({
...refVars.current.settingsSignatures,
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
}),
[],
);
return (
<div ref={tableRef} className="h-full">
@@ -257,7 +287,7 @@ export const SystemSignaturesContent = ({
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures || []}
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
@@ -306,8 +336,6 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
sortable
sortField="name"
/>
{showDescriptionColumn && (
<Column

View File

@@ -1,5 +1,5 @@
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
import clsx from 'clsx';
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
import { getRowBackgroundColor } from './getRowBackgroundColor';
import classes from './rowStyles.module.scss';
@@ -20,7 +20,7 @@ export function getSignatureRowClass(
return clsx([...baseCls, 'bg-violet-400/40 hover:bg-violet-300/40']);
}
if (row.deleted) {
if (row.pendingDeletion) {
return clsx([...baseCls, 'bg-red-400/40 hover:bg-red-400/50']);
}

View File

@@ -1,20 +1,24 @@
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
export interface UseSystemSignaturesDataProps {
systemId: string;
settings: SignatureSettingsType;
hideLinkedSignatures?: boolean;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
onLazyDeleteChange?: (value: boolean) => void;
deletionTiming?: number;
}
export interface UseFetchingParams {
systemId: string;
settings: SignatureSettingsType;
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
pendingDeletionMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>;
}
export interface UsePendingDeletionParams {

View File

@@ -0,0 +1,42 @@
import { useCallback, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
added: ExtendedSystemSignature[],
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand],
);
const clearPendingDeletions = useCallback(() => {
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
}, []);
return {
pendingDeletionMapRef,
processRemovedSignatures,
clearPendingDeletions,
};
}

View File

@@ -1,27 +1,21 @@
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback } from 'react';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useMemo } from 'react';
import { getDeletionTimeoutMs } from '../constants';
import { getActualSigs, prepareUpdatePayload } from '../helpers';
import { prepareUpdatePayload, getActualSigs, mergeLocalPending } from '../helpers';
import { UseFetchingParams } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSignatures }: UseFetchingParams) => {
export const useSignatureFetching = ({
systemId,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
}: UseFetchingParams) => {
const {
data: { characters },
outCommand,
} = useMapRootState();
const deleteTimeout = useMemo(() => {
const lazyDelete = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
if (!lazyDelete) {
return 0;
}
return getDeletionTimeoutMs(settings);
}, [settings]);
const handleGetSignatures = useCallback(async () => {
if (!systemId) {
setSignatures([]);
@@ -38,23 +32,24 @@ export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSig
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
})) as ExtendedSystemSignature[];
setSignatures(() => extended);
setSignatures(() => mergeLocalPending(pendingDeletionMapRef, extended));
}, [characters, systemId, outCommand]);
const handleUpdateSignatures = useCallback(
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
const actualSigs = getActualSigs(signaturesRef.current, newList, updateOnly, skipUpdateUntouched);
const { added, updated, removed } = getActualSigs(
signaturesRef.current,
newList,
updateOnly,
skipUpdateUntouched,
);
const { added, updated, removed } = actualSigs;
if (updated.length !== 0 || added.length !== 0 || removed.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: { ...prepareUpdatePayload(systemId, added, updated, removed), deleteTimeout },
});
}
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, deleteTimeout, outCommand, signaturesRef],
[systemId, outCommand, signaturesRef],
);
return {

View File

@@ -1,89 +0,0 @@
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import { ExtendedSystemSignature, OutCommandHandler } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useRef, useState } from 'react';
import { getDeletionTimeoutMs } from '../constants';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
export function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
deletedSignatures: ExtendedSystemSignature[],
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const intervalRef = useRef<number | null>(null);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (deletedSignatures.length === 0) {
setCountdown(0);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
// Ensure a minimum of 1 second for immediate deletion so the UI shows
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [deletedSignatures, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || deletedSignatures.length === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: deletedSignatures.map(s => s.eve_id) },
});
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, deletedSignatures, outCommand]);
return {
countdown,
handleUndo,
};
}

View File

@@ -1,29 +1,44 @@
import { useMapEventListener } from '@/hooks/Mapper/events';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
export const useSystemSignaturesData = ({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
onPendingChange,
});
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
systemId,
settings,
signaturesRef,
setSignatures,
pendingDeletionMapRef,
});
const handlePaste = useCallback(
@@ -52,14 +67,40 @@ export const useSystemSignaturesData = ({
setHasUnsupportedLanguage(false);
}
await handleUpdateSignatures(incomingSignatures, !lazyDeleteValue, false);
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
// Show pending deletions if lazy deletion is enabled
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
if (onSignatureDeleted && lazyDeleteValue) {
onSignatureDeleted(removed);
}
}
if (updated.length !== 0 || added.length !== 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed: [],
},
});
}
const keepLazy = settings[SETTINGS_KEYS.KEEP_LAZY_DELETE] as boolean;
if (lazyDeleteValue && !keepLazy) {
onLazyDeleteChange?.(false);
}
},
[settings, handleUpdateSignatures, onLazyDeleteChange],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
);
const handleDeleteSelected = useCallback(async () => {
@@ -68,15 +109,23 @@ export const useSystemSignaturesData = ({
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
setSelectedSignatures([]);
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
}, [signatures]);
const undoPending = useCallback(() => {
clearPendingDeletions();
}, [clearPendingDeletions]);
useMapEventListener(event => {
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
handleGetSignatures();
@@ -87,13 +136,18 @@ export const useSystemSignaturesData = ({
useEffect(() => {
if (!systemId) {
setSignatures([]);
undoPending();
return;
}
handleGetSignatures();
}, [systemId]);
useEffect(() => {
onCountChange?.(signatures.length);
}, [signatures]);
return {
signatures,
signatures: signatures.filter(sig => !sig.deleted),
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,

View File

@@ -1,14 +1,14 @@
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { PrimeIcons } from 'primereact/api';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
import clsx from 'clsx';
import { renderName } from './renderName.tsx';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
export const renderInfoColumn = (row: SystemSignature) => {
if (!row.group || row.group === SignatureGroup.Wormhole) {
@@ -18,9 +18,7 @@ export const renderInfoColumn = (row: SystemSignature) => {
return (
<div className="flex justify-start items-center gap-[4px]">
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
{customInfo.time_status === TimeStatus._1h && (
{customInfo.isEOL && (
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
</WdTooltipWrapper>

View File

@@ -30,14 +30,10 @@ export const SystemStructures: React.FC = () => {
const processClipboard = useCallback(
(text: string) => {
if (!systemId) {
console.warn('Cannot update structures: no system selected');
return;
}
const updated = processSnippetText(text, structures);
handleUpdateStructures(updated);
},
[systemId, structures, handleUpdateStructures],
[structures, handleUpdateStructures],
);
const handlePaste = useCallback(

View File

@@ -30,6 +30,9 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
const { outCommand } = useMapRootState();
const [prevQuery, setPrevQuery] = useState('');
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
useEffect(() => {
if (structure) {
setEditData(structure);
@@ -43,24 +46,34 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const query = e.query.trim();
if (!query) {
const newQuery = e.query.trim();
if (!newQuery) {
setOwnerSuggestions([]);
return;
}
// If user typed more text but we have partial match in prevResults
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
setOwnerSuggestions(filtered);
return;
}
try {
// TODO fix it
const { results = [] } = await outCommand({
type: OutCommand.getCorporationNames,
data: { search: query },
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
console.error('Failed to fetch owners:', err);
setOwnerSuggestions([]);
}
},
[outCommand],
[prevQuery, prevResults, outCommand],
);
const handleChange = (field: keyof StructureItem, val: string | Date) => {
@@ -109,6 +122,7 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// fetch corporation ticker if we have an ownerId
if (editData.ownerId) {
try {
// TODO fix it
const { ticker } = await outCommand({
type: OutCommand.getCorporationTicker,
data: { corp_id: editData.ownerId },

View File

@@ -56,11 +56,6 @@ export function useSystemStructures({ systemId, outCommand }: UseSystemStructure
const handleUpdateStructures = useCallback(
async (newList: StructureItem[]) => {
if (!systemId) {
console.warn('Cannot update structures: systemId is undefined');
return;
}
const { added, updated, removed } = getActualStructures(structures, newList);
const sanitizedAdded = added.map(sanitizeIds);

View File

@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useStableValue } from '@/hooks/Mapper/hooks';
interface UseSystemKillsProps {
systemId?: string;
@@ -30,8 +30,9 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
update,
storedSettings: { settingsKills },
} = useMapRootState();
const { excludedSystems } = settingsKills;
const excludedSystems = useStableValue(settingsKills.excludedSystems ?? []);
const effectiveSinceHours = sinceHours;
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
@@ -75,13 +76,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
eventType = OutCommand.getSystemsKills;
requestData = {
system_ids: effectiveSystemIds,
since_hours: sinceHours,
since_hours: effectiveSinceHours,
};
} else if (systemId) {
eventType = OutCommand.getSystemKills;
requestData = {
system_id: systemId,
since_hours: sinceHours,
since_hours: effectiveSinceHours,
};
} else {
setIsLoading(false);
@@ -109,7 +110,16 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
setIsLoading(false);
}
},
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
[showAllVisible, systemId, outCommand, effectiveSystemIds, effectiveSinceHours, mergeKillsIntoGlobal],
);
const debouncedFetchKills = useMemo(
() =>
debounce(fetchKills, 500, {
leading: true,
trailing: false,
}),
[fetchKills],
);
const finalKills = useMemo(() => {
@@ -131,22 +141,27 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
useEffect(() => {
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
didFallbackFetch.current = true;
debouncedFetchKills.cancel();
fetchKills(true);
}
}, [systemId, showAllVisible, fetchKills]);
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
useEffect(() => {
if (effectiveSystemIds.length === 0) return;
if (showAllVisible || systemId) {
// Cancel any pending debounced fetch
debouncedFetchKills.cancel();
// Fetch kills immediately
fetchKills();
return;
return () => debouncedFetchKills.cancel();
}
}, [showAllVisible, systemId, effectiveSystemIds, fetchKills]);
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills, fetchKills]);
const refetch = useCallback(() => {
debouncedFetchKills.cancel();
fetchKills();
}, [fetchKills]);
}, [debouncedFetchKills, fetchKills]);
return {
kills: finalKills,

View File

@@ -9,14 +9,12 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
import { WormholeSignaturesDialog } from '@/hooks/Mapper/components/mapRootContent/components/WormholeSignaturesDialog';
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
import { TopSearch } from '@/hooks/Mapper/components/mapRootContent/components/TopSearch';
export interface MapRootContentProps {}
@@ -35,7 +33,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false);
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
const [showWormholeList, setShowWormholeList] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = isReady ? <MapInterface /> : null;
@@ -43,7 +40,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
const handleShowWormholesReference = useCallback(() => setShowWormholeList(true), []);
useMapEventListener(event => {
if (event.name === Commands.showTracking) {
@@ -68,7 +64,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
additionalContent={<PingsInterface hasLeftOffset />}
/>
</div>
@@ -77,13 +72,11 @@ export const MapRootContent = ({}: MapRootContentProps) => {
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<div className="flex items-center ml-1">
<TopSearch />
<PingsInterface />
<MapContextMenu
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
/>
</div>
</Topbar>
@@ -98,7 +91,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)}
<WormholeSignaturesDialog visible={showWormholeList} onHide={() => setShowWormholeList(false)} />
{hasOldSettings && <OldSettingsDialog />}
</Layout>

View File

@@ -1,7 +1,4 @@
import { Dialog } from 'primereact/dialog';
import { Menu } from 'primereact/menu';
import { MenuItem } from 'primereact/menuitem';
import { useState, useCallback, useRef, useMemo } from 'react';
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
interface CharacterActivityProps {
@@ -9,69 +6,17 @@ interface CharacterActivityProps {
onHide: () => void;
}
const periodOptions = [
{ value: 30, label: '30 Days' },
{ value: 365, label: '1 Year' },
{ value: null, label: 'All Time' },
];
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
const [selectedPeriod, setSelectedPeriod] = useState<number | null>(30);
const menuRef = useRef<Menu>(null);
const handlePeriodChange = useCallback((days: number | null) => {
setSelectedPeriod(days);
}, []);
const menuItems: MenuItem[] = useMemo(
() => [
{
label: 'Period',
items: periodOptions.map(option => ({
label: option.label,
icon: selectedPeriod === option.value ? 'pi pi-check' : undefined,
command: () => handlePeriodChange(option.value),
})),
},
],
[selectedPeriod, handlePeriodChange],
);
const selectedPeriodLabel = useMemo(
() => periodOptions.find(opt => opt.value === selectedPeriod)?.label || 'All Time',
[selectedPeriod],
);
const headerIcons = (
<>
<button
type="button"
className="p-dialog-header-icon p-link"
onClick={e => menuRef.current?.toggle(e)}
aria-label="Filter options"
>
<span className="pi pi-bars" />
</button>
<Menu model={menuItems} popup ref={menuRef} />
</>
);
return (
<Dialog
header={
<div className="flex items-center gap-2">
<span>Character Activity</span>
<span className="text-xs text-stone-400">({selectedPeriodLabel})</span>
</div>
}
header="Character Activity"
visible={visible}
className="w-[550px] max-h-[90vh]"
onHide={onHide}
dismissableMask
contentClassName="p-0 h-full flex flex-col"
icons={headerIcons}
>
<CharacterActivityContent selectedPeriod={selectedPeriod} />
<CharacterActivityContent />
</Dialog>
);
};

View File

@@ -7,28 +7,16 @@ import {
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo, useEffect } from 'react';
import { useCharacterActivityHandlers } from '@/hooks/Mapper/components/mapRootContent/hooks/useCharacterActivityHandlers';
import { useMemo } from 'react';
interface CharacterActivityContentProps {
selectedPeriod: number | null;
}
export const CharacterActivityContent = ({ selectedPeriod }: CharacterActivityContentProps) => {
export const CharacterActivityContent = () => {
const {
data: { characterActivityData },
} = useMapRootState();
const { handleShowActivity } = useCharacterActivityHandlers();
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
// Reload activity data when period changes
useEffect(() => {
handleShowActivity(selectedPeriod);
}, [selectedPeriod, handleShowActivity]);
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-full w-full">

View File

@@ -3,7 +3,7 @@
}
.SidebarOnTheMap {
width: 500px;
width: 400px;
padding: 0 !important;
:global {

View File

@@ -5,7 +5,6 @@ import {
ConnectionType,
OutCommand,
Passage,
PassageWithSourceTarget,
SolarSystemConnection,
} from '@/hooks/Mapper/types';
import clsx from 'clsx';
@@ -20,7 +19,7 @@ import { PassageCard } from './PassageCard';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) => {
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border', {
@@ -36,7 +35,7 @@ const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTem
};
export interface ConnectionPassagesContentProps {
passages: PassageWithSourceTarget[];
passages: Passage[];
}
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
@@ -114,20 +113,6 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
[outCommand],
);
const preparedPassages = useMemo(() => {
if (!cnInfo) {
return [];
}
return passages
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
.map<PassageWithSourceTarget>(x => ({
...x,
source: x.from ? cnInfo.target : cnInfo.source,
target: x.from ? cnInfo.source : cnInfo.target,
}));
}, [cnInfo, passages]);
useEffect(() => {
if (!selectedConnection) {
return;
@@ -160,14 +145,12 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
<InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center">
<SystemView
showCustomName
systemId={cnInfo.source}
className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion
/>
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView
showCustomName
systemId={cnInfo.target}
className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion
@@ -201,7 +184,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
{/* separator */}
<div className="w-full h-px bg-neutral-800 px-0.5"></div>
<ConnectionPassages passages={preparedPassages} />
<ConnectionPassages passages={passages} />
</div>
</Sidebar>
);

View File

@@ -35,10 +35,6 @@
&.ThreeColumns {
grid-template-columns: auto 1fr auto;
}
&.FourColumns {
grid-template-columns: auto auto 1fr auto;
}
}
.CardBorderLeftIsOwn {

View File

@@ -1,19 +1,17 @@
import clsx from 'clsx';
import classes from './PassageCard.module.scss';
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
import { SystemView, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { Passage } from '@/hooks/Mapper/types';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { useCallback, useMemo } from 'react';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
import { useMemo } from 'react';
type PassageCardType = {
// compact?: boolean;
showShipName?: boolean;
// showSystem?: boolean;
// useSystemsCache?: boolean;
} & PassageWithSourceTarget;
} & Passage;
const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
@@ -27,7 +25,7 @@ export const getShipName = (name: string) => {
});
};
export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => {
const isOwn = false;
const insertedAt = useMemo(() => {
@@ -35,46 +33,11 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
return date.toLocaleString();
}, [inserted_at]);
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
<div className="flex flex-col justify-between px-2 py-1 gap-1">
{/*here icon and other*/}
<div className={clsx(classes.CharRow, classes.FourColumns)}>
<WdTooltipWrapper
position={TooltipPosition.top}
content={
<div className="flex justify-between gap-2 items-center">
<SystemView
showCustomName
systemId={source}
className="select-none text-center !text-[12px]"
hideRegion
/>
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView
showCustomName
systemId={target}
className="select-none text-center !text-[12px]"
hideRegion
/>
</div>
}
>
<div
className={clsx(
'transition-all transform ease-in duration-200',
'pi text-stone-500 text-[15px] w-[35px] h-[33px] !flex items-center justify-center border rounded-[6px]',
{
['pi-angle-double-right !text-orange-400 border-orange-400 hover:bg-orange-400/30']: from,
['pi-angle-double-left !text-stone-500/70 border-stone-500/70 hover:bg-stone-500/30']: !from,
},
)}
/>
</WdTooltipWrapper>
<div className={clsx(classes.CharRow, classes.ThreeColumns)}>
{/*portrait*/}
<span
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
@@ -86,7 +49,7 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
{/*here name and ship name*/}
<div className="grid gap-1 justify-between grid-cols-[max-content_1fr]">
{/*char name*/}
<div className="grid gap-1 grid-cols-[auto_1px_1fr_auto]">
<div className="grid gap-1 grid-cols-[auto_1px_1fr]">
<span
className={clsx(classes.MaxWidth, 'text-ellipsis overflow-hidden whitespace-nowrap', {
[classes.CardBorderLeftIsOwn]: isOwn,
@@ -99,21 +62,6 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
<div className="h-3 border-r border-neutral-500 my-0.5"></div>
{char.alliance_ticker && <span className="text-neutral-400">{char.alliance_ticker}</span>}
{!char.alliance_ticker && <span className="text-neutral-400">{char.corporation_ticker}</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[2px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div>
{/*ship name*/}

View File

@@ -12,15 +12,9 @@ export interface MapContextMenuProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
}
export const MapContextMenu = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
onShowWormholesReference,
}: MapContextMenuProps) => {
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
const {
outCommand,
storedSettings: { setInterfaceSettings },
@@ -58,12 +52,6 @@ export const MapContextMenu = ({
command: onShowOnTheMap,
visible: canTrackCharacters,
},
{
label: 'Wormholes Ref.',
icon: 'pi pi-bullseye',
command: onShowWormholesReference,
visible: canTrackCharacters,
},
{ separator: true, visible: true },
{
label: 'Settings',

View File

@@ -62,7 +62,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
header="Map user settings"
visible
draggable={false}
className="w-[600px] h-[400px]"
style={{ width: '600px' }}
onShow={handleShow}
onHide={handleHide}
>

View File

@@ -10,14 +10,9 @@ import { useCallback } from 'react';
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const {
storedSettings: { resetSettings },
} = useMapRootState();
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const renderSettingsList = useCallback(
@@ -27,7 +22,7 @@ export const CommonSettings = () => {
[renderSettingItem],
);
const handleResetSettings = useCallback(() => resetSettings(), [resetSettings]);
const handleResetSettings = () => {};
return (
<div className="flex flex-col h-full gap-1">

View File

@@ -5,8 +5,6 @@ import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
export const ImportExport = () => {
const {
@@ -24,9 +22,8 @@ export const ImportExport = () => {
}
try {
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(applyMigrations(parsed) || createDefaultStoredSettings())) {
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
@@ -62,9 +59,8 @@ export const ImportExport = () => {
try {
const text = await loadTextFile();
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(applyMigrations(parsed) || createDefaultStoredSettings())) {
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',

View File

@@ -1,13 +1,13 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { OutCommand } from '@/hooks/Mapper/types';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { callToastSuccess } from '@/hooks/Mapper/helpers';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
export const ServerSettings = () => {
@@ -29,16 +29,15 @@ export const ServerSettings = () => {
}
if (res?.default_settings == null) {
applySettings(createDefaultStoredSettings());
applySettings(createDefaultWidgetSettings());
return;
}
try {
//INFO: INSTEAD CHECK WE WILL TRY TO APPLY MIGRATION
applySettings(applyMigrations(JSON.parse(res.default_settings)) || createDefaultStoredSettings());
applySettings(parseMapUserSettings(res.default_settings));
callToastSuccess(toast.current, 'Settings synchronized successfully');
} catch (error) {
applySettings(createDefaultStoredSettings());
applySettings(createDefaultWidgetSettings());
}
}, [applySettings, outCommand]);

View File

@@ -1,5 +1,14 @@
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { saveTextFile } from '@/hooks/Mapper/utils';
import { ConfirmPopup } from 'primereact/confirmpopup';
@@ -9,7 +18,10 @@ import { useCallback, useRef } from 'react';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return lsSettings ? JSON.parse(lsSettings) : defaultValues;
return {
version: -1,
settings: lsSettings ? JSON.parse(lsSettings) : defaultValues,
};
};
export const OldSettingsDialog = () => {
@@ -32,15 +44,13 @@ export const OldSettingsDialog = () => {
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_6');
const out: MapUserSettings = {
version: 0,
migratedFromOld: false,
killsWidget: createSettings(widgetKills, {}),
localWidget: createSettings(widgetLocal, {}),
widgets: createSettings(widgetsOld, {}),
routes: createSettings(widgetRoutes, {}),
onTheMap: createSettings(onTheMapOld, {}),
signaturesWidget: createSettings(signatures, {}),
interface: createSettings(interfaceSettings, {}),
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createSettings(widgetLocal, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createSettings(widgetsOld, getDefaultWidgetProps()),
routes: createSettings(widgetRoutes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createSettings(onTheMapOld, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createSettings(signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createSettings(interfaceSettings, STORED_INTERFACE_DEFAULT_VALUES),
};
if (asFile) {

View File

@@ -7,14 +7,12 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { TopSearch } from '@/hooks/Mapper/components/mapRootContent/components/TopSearch';
// import { DebugComponent } from '@/hooks/Mapper/components/mapRootContent/components/RightBar/DebugComponent.tsx';
interface RightBarProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
additionalContent?: ReactNode;
}
@@ -22,7 +20,6 @@ export const RightBar = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
onShowWormholesReference,
additionalContent,
}: RightBarProps) => {
const {
@@ -51,7 +48,7 @@ export const RightBar = ({
classes.RightBarRoot,
'w-full h-full',
'text-gray-200 shadow-lg border-l border-zinc-800 border-opacity-70 bg-opacity-70 bg-neutral-900',
'flex flex-col items-center justify-between pt-1',
'flex flex-col items-center justify-between',
)}
>
<div className="flex flex-col gap-2 items-center mt-1">
@@ -68,41 +65,15 @@ export const RightBar = ({
</button>
</WdTooltipWrapper>
<div className="flex flex-col gap-1">
<TopSearch
customBtn={open => (
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={open}
>
<i className="pi pi-search"></i>
</button>
</WdTooltipWrapper>
)}
/>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Wormholes Reference" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowWormholesReference}
>
<i className="pi pi-bullseye"></i>
</button>
</WdTooltipWrapper>
</div>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
</>
)}
{additionalContent}

View File

@@ -1,15 +1,15 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect } from 'react';
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import {
SignatureGroupContent,
SignatureGroupSelect,
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components';
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
linked_system: string;
@@ -119,7 +119,6 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
added: [],
updated: [out],
removed: [],
deleteTimeout: 0,
},
});

View File

@@ -13,26 +13,6 @@ export const renderK162Type = (option: K162Type) => {
return renderNoValue();
}
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
const arr = whClassName.split('_');
return (
<div className="flex gap-1 items-center">
{arr.map(x => (
<WHClassView
key={x}
classNameWh="!text-[11px] !font-bold"
hideWhClassName
hideTooltip
whClassName={x}
noOffset
useShortTitle
/>
))}
</div>
);
}
return (
<WHClassView
classNameWh="!text-[11px] !font-bold"

View File

@@ -20,7 +20,7 @@ const renderLinkedSystemItem = (option: { value: string }) => {
return (
<div className="flex gap-2 items-center">
<SystemView systemId={value} className={classes.SystemView} />
<SystemView systemId={value} className={classes.SystemView} showCustomName={true} />
</div>
);
};
@@ -37,7 +37,7 @@ const renderLinkedSystemValue = (option: { value: string }) => {
return (
<div className="flex gap-2 items-center">
<SystemView systemId={option.value} className={classes.SystemView} />
<SystemView systemId={option.value} className={classes.SystemView} showCustomName={true} />
</div>
);
};

View File

@@ -1,170 +0,0 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
:root {
--rf-has-user-characters: #ffc75d;
}
.Sidebar {
:global {
.p-sidebar-header {
padding-left: 14px;
padding-right: 14px;
}
.p-sidebar-content {
padding-left: 0px;
padding-right: 0px;
}
}
}
.Content {
position: relative;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: 50% 50%;
z-index: -1;
background-repeat: no-repeat;
border-radius: 3px;
}
}
&.Mataria {
&::after {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
//background-position-x: 52px;
//background-position-y: -83px;
background-position-x: 331px;
background-position-y: -77px;
transform: scale(-1);
}
}
&.Caldaria {
&::after {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
//background-position-x: 41px;
//background-position-y: -45px;
background-position-x: 360px;
background-position-y: -16px;
background-size: 30%;
transform: rotate(180deg);
}
}
&.Amarria {
&::after {
opacity: 0.45;
background-image: url('/images/amarr-small.png');
//background-position-x: 23px;
//background-position-y: -74px;
background-position-x: -1px;
background-position-y: -75px;
background-origin: border-box;
background-size: auto;
transform: scale(1, -1);
}
}
&.Gallente {
&::after {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
//background-position-x: 1px;
//background-position-y: -55px;
background-position-x: 294px;
background-position-y: -53px;
transform: scale(-1);
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -88px;
}
}
&.selected {
border-color: v.$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 {
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
}
&.eve-system-status-friendly {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-friendly-dark30), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-friendly-dark5);
}
}
&.eve-system-status-lookingFor {
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: v.$pastel-pink;
}
}
&.eve-system-status-warning {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-warning), transparent);
}
&.eve-system-status-dangerous {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-dangerous), transparent);
}
&.eve-system-status-target {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-target), transparent);
}
}

View File

@@ -1,347 +0,0 @@
import classes from './TopSearch.module.scss';
import { Sidebar } from 'primereact/sidebar';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import { Commands, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import {
SystemViewStandalone,
TooltipPosition,
WdImageSize,
WdImgButton,
WdTooltipWrapper,
WHClassView,
WHEffectView,
} from '@/hooks/Mapper/components/ui-kit';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { STATUS_CLASSES } from '@/hooks/Mapper/components/map/constants.ts';
import { REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants.ts';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { getLocalCharacters } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
import { PrimeIcons } from 'primereact/api';
type CompiledSystem = {
dynamic: SolarSystemRawType;
static: SolarSystemStaticInfoRaw | undefined;
};
const useItemTemplate = () => {
const {
data: { wormholesData, characters, userCharacters, hubs },
} = useMapRootState();
return useCallback(
(item: CompiledSystem, options: VirtualScrollerTemplateOptions) => {
if (!item.static) {
return null;
}
const {
security,
system_class,
class_title,
effect_power,
region_name,
solar_system_name,
solar_system_id,
effect_name,
statics,
region_id,
} = item.static;
const onlineCharactersInSystem = characters.filter(
c => c.location?.solar_system_id === solar_system_id && c.online,
);
const hasOnlineUserCharacters = onlineCharactersInSystem.some(x => userCharacters.includes(x.eve_id));
const onlineCharacters = getLocalCharacters({ charactersInSystem: onlineCharactersInSystem, userCharacters });
const offlineCharactersInSystem = characters.filter(
c => c.location?.solar_system_id === solar_system_id && !c.online,
);
const hasOfflineUserCharacters = offlineCharactersInSystem.some(x => userCharacters.includes(x.eve_id));
const offlineCharacters = getLocalCharacters({ charactersInSystem: offlineCharactersInSystem, userCharacters });
const handleSelect = () => {
emitMapEvent({
name: Commands.centerSystem,
data: solar_system_id.toString(),
});
};
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
const regionClass = SPACE_TO_CLASS[REGIONS_MAP[region_id]] || null;
const showTempName = item.dynamic.temporary_name != null;
const showCustomName = item.dynamic.name != null && item.dynamic.name !== solar_system_name;
return (
<div
className={clsx(
'w-full box-border px-3.5 py-1 h-[48px] cursor-pointer',
'bg-transparent hover:bg-stone-800/30 transition-all !duration-250 ease-in-out',
{
'surface-hover': options.odd,
['border-b border-gray-600 border-opacity-20']: !options.last,
},
classes.Content,
regionClass && classes[regionClass],
item.dynamic.status !== undefined && classes[STATUS_CLASSES[item.dynamic.status]],
)}
onClick={handleSelect}
>
<div className={clsx('w-full')}>
<div className={clsx('grid grid-cols-[1fr_auto] gap-1.5 w-full')}>
<div className="flex [&>*]:!text-[13px] gap-1.5">
<SystemViewStandalone
className="!text-[13px]"
security={security}
system_class={system_class}
solar_system_id={parseInt(item.dynamic.id)}
class_title={class_title}
solar_system_name={`${solar_system_name}`}
region_name={region_name}
nameClassName="font-semibold"
/>
{(showTempName || showCustomName) && (
<div className="grid grid-cols-[auto_1fr] gap-1.5 text-stone-400 text-[12px]">
{showTempName && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{item.dynamic.temporary_name}
</span>
)}
{showCustomName && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{item.dynamic.name}</span>
)}
</div>
)}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
</div>
<div>
{isWH && (
<div className="flex gap-1 grow justify-between !text-[13px]">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
</div>
)}
</div>
</div>
<div className="flex gap-1.5 text-[13px] pl-[2px] items-center h-[20px]">
<LocalCounter
disableInteractive
className="[&_span]:!text-[12px] [&_i]:!text-[13px]"
hasUserCharacters={hasOnlineUserCharacters}
localCounterCharacters={onlineCharacters}
/>
<LocalCounter
disableInteractive
className="[&_span]:!text-[12px] [&_i]:!text-[13px] text-stone-[400]"
contentClassName={clsx('!text-stone-500', { ['!text-yellow-600']: hasOfflineUserCharacters })}
hasUserCharacters={hasOfflineUserCharacters}
localCounterCharacters={offlineCharacters}
/>
{item.dynamic.locked && <i className={clsx(PrimeIcons.LOCK, 'text-[11px] relative top-[1px]')} />}
{hubs.includes(solar_system_id.toString()) && (
<i className={clsx(PrimeIcons.MAP_MARKER, 'text-[11px] relative top-[1px]')} />
)}
{item.dynamic.comments_count != null && item.dynamic.comments_count > 0 && (
<WdTooltipWrapper
position={TooltipPosition.top}
content={`[${item.dynamic.comments_count}] Comments in System - click to system to see it in Comments Widget`}
>
<i className={clsx(PrimeIcons.COMMENT, 'text-[11px] relative top-[1px]')} />
</WdTooltipWrapper>
)}
{item.dynamic.description != null && item.dynamic.description !== '' && (
<WdTooltipWrapper
position={TooltipPosition.top}
content={`System have description - click to system to see it in Info Widget`}
>
<i
className={clsx(
'pi hero-chat-bubble-bottom-center-text w-[14px] h-[14px]',
'text-[8px] relative top-[-1px]',
)}
/>
</WdTooltipWrapper>
)}
{/*kek*/}
</div>
</div>
</div>
);
},
[characters, hubs, userCharacters, wormholesData],
);
};
export interface TopSearchSidebarProps {
show: boolean;
onHide: () => void;
}
export const TopSearchSidebar = ({ show, onHide }: TopSearchSidebarProps) => {
const [searchVal, setSearchVal] = useState('');
// eslint-disable-next-line
const inputRef = useRef<any>();
const {
data: { systems },
} = useMapRootState();
const itemTemplate = useItemTemplate();
const systemsCompiled = useMemo<CompiledSystem[]>(() => {
return systems.map(x => ({
dynamic: x,
static: getSystemStaticInfo(x.id),
}));
}, [systems]);
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
const filtered = useMemo(() => {
let out = systemsCompiled;
out = out.sort((a, b) => {
// 1. Status > 0 — always on top and ASC
if (a.dynamic.status > 0 && b.dynamic.status > 0) return a.dynamic.status - b.dynamic.status;
if (a.dynamic.status > 0) return -1;
if (b.dynamic.status > 0) return 1;
// 2. IF status = 0, J priority
const aStartsWithJ = a.dynamic.name?.startsWith('J') ?? false;
const bStartsWithJ = b.dynamic.name?.startsWith('J') ?? false;
if (aStartsWithJ && !bStartsWithJ) return -1;
if (!aStartsWithJ && bStartsWithJ) return 1;
// 3. IF both starts with J or not - sort by name
const nameA = a.dynamic.name ?? '';
const nameB = b.dynamic.name ?? '';
return nameA.localeCompare(nameB);
});
const normalized = searchVal.toLowerCase();
if (searchVal !== '') {
out = out.filter(x => {
if (x.static?.solar_system_name.toLowerCase().includes(normalized)) {
return true;
}
if (x.dynamic.name?.toLowerCase().includes(normalized)) {
return true;
}
if (x.dynamic.temporary_name?.toLowerCase().includes(normalized)) {
return true;
}
return false;
});
}
return out;
}, [searchVal, systemsCompiled]);
return (
<Sidebar
className={clsx(classes.Sidebar, 'bg-neutral-900 !p-[0px] w-[500px]')}
visible={show}
position="right"
onShow={onShow}
onHide={onHide}
modal={false}
header={`Search [${filtered.length}]`}
icons={<></>}
>
<div className={clsx('grid grid-rows-[auto_1fr] gap-y-[8px] h-full')}>
<div className={'flex justify-between items-center gap-2 px-2 pt-1'}>
<IconField className="w-full">
{searchVal.length > 0 && (
<WdImgButton
className="pi pi-trash"
textSize={WdImageSize.large}
tooltip={{
content: 'Clear',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={() => setSearchVal('')}
/>
)}
<InputText
id="label"
className="w-full"
aria-describedby="label"
ref={inputRef}
autoComplete="off"
value={searchVal}
placeholder="Type To Search"
onChange={e => setSearchVal(e.target.value)}
/>
</IconField>
</div>
<VirtualScroller
items={filtered}
itemSize={48}
itemTemplate={itemTemplate}
className={clsx(
classes.VirtualScroller,
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
'[&>div]:w-full',
)}
autoSize={false}
/>
</div>
</Sidebar>
);
};
interface TopSearchProps {
customBtn?: (open: () => void) => React.ReactNode;
}
export const TopSearch = ({ customBtn }: TopSearchProps) => {
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
return (
<>
{customBtn != null && customBtn(() => setOpenAddSystem(true))}
{customBtn == null && (
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent px-2 relative left-1"
type="button"
onClick={() => setOpenAddSystem(true)}
>
<i className="pi pi-search text-lg"></i>
</button>
)}
<TopSearchSidebar show={openAddSystem} onHide={() => setOpenAddSystem(false)} />
</>
);
};

View File

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

View File

@@ -1,13 +1,13 @@
import { Column } from 'primereact/column';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { DataTable } from 'primereact/datatable';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TrackingCharacter } from '@/hooks/Mapper/types';
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
export const TrackingCharactersList = () => {
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
const { trackingCharacters, main, following, updateTracking } = useTracking();
const { trackingCharacters, updateTracking } = useTracking();
const refVars = useRef({ trackingCharacters });
refVars.current = { trackingCharacters };
@@ -20,31 +20,9 @@ export const TrackingCharactersList = () => {
[updateTracking],
);
const items = useMemo(() => {
let out = trackingCharacters;
out = out.sort((a, b) => {
const aId = a.character.eve_id;
const bId = b.character.eve_id;
// 1. main always first
if (aId === main && bId !== main) return -1;
if (bId === main && aId !== main) return 1;
// 2. following after main
if (aId === following && bId !== following) return -1;
if (bId === following && aId !== following) return 1;
// 3. sort by name
return a.character.name.localeCompare(b.character.name);
});
return out;
}, [trackingCharacters, main, following]);
return (
<DataTable
value={items}
value={trackingCharacters}
size="small"
selectionMode={null}
selection={selected}

View File

@@ -1,9 +1,8 @@
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { createContext, useCallback, useContext, useRef, useState } from 'react';
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
type DiffTrackingInfo = { characterId: string; tracked: boolean };
@@ -123,14 +122,6 @@ export const TrackingProvider = ({ children }: WithChildren) => {
[outCommand],
);
// Listen for refresh_tracking_data event (triggered when ACL members change)
useMapEventListener(event => {
if (event.name === Commands.refreshTrackingData) {
loadTracking();
return true;
}
});
return (
<TrackingContext.Provider
value={{

View File

@@ -1,170 +0,0 @@
import { useMemo, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WormholeDataRaw } from '@/hooks/Mapper/types';
import { RespawnTag, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import clsx from 'clsx';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
const renderSpawns = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.src.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) {
return (
<span
key={s}
className="px-[4px] py-[1px] rounded bg-stone-800 text-stone-300 text-xs border border-stone-700"
>
{s}
</span>
);
}
const cls = WORMHOLE_CLASS_STYLES[String(info.wormholeClassID)] || '';
const label = `${info.shortName}`;
return (
<span
key={s}
className={clsx(cls, 'px-[4px] py-[1px] rounded text-xs border border-stone-700 bg-stone-900/40')}
>
{label}
</span>
);
})}
</div>
);
const renderName = (w: WormholeDataRaw) => (
<div className="flex items-center gap-2">
<WHClassView
whClassName={w.name}
noOffset
useShortTitle
classNameWh="overflow-hidden text-ellipsis whitespace-nowrap"
/>
</div>
);
const renderRespawn = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.respawn.map(r => (
<RespawnTag key={r} value={r} />
))}
</div>
);
export interface WormholeSignaturesDialogProps {
visible: boolean;
onHide: () => void;
}
export const WormholeSignaturesDialog = ({ visible, onHide }: WormholeSignaturesDialogProps) => {
const {
data: { wormholes },
} = useMapRootState();
const [filter, setFilter] = useState('');
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
if (!q) return wormholes;
return wormholes.filter(w => {
const destInfo = WORMHOLES_ADDITIONAL_INFO[w.dest];
const spawnsLabels = w.src
.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) return s;
return `${info.title} ${info.shortName}`.trim();
})
.join(' ');
return [
w.name,
destInfo?.title,
destInfo?.shortName,
spawnsLabels,
String(w.total_mass),
String(w.max_mass_per_jump),
w.lifetime,
w.respawn.join(','),
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(q);
});
}, [wormholes, filter]);
return (
<Dialog
header="Wormholes Reference"
visible={visible}
draggable={false}
resizable={false}
className="w-[950px] h-[600px]"
onHide={onHide}
contentClassName="!p-0 flex flex-col h-full"
>
<div className="p-3 flex items-center justify-between gap-2 border-b border-stone-800">
<div className="font-semibold text-sm text-stone-200">Reference list of all wormhole types</div>
<IconField iconPosition="right">
<InputIcon
className={clsx('pi pi-times', {
['cursor-pointer text-stone-400 hover:text-stone-200']: filter,
['text-stone-700 opacity-50 cursor-default']: !filter,
})}
onClick={() => filter && setFilter('')}
role="button"
aria-label="Clear search"
aria-disabled={!filter}
title={filter ? 'Clear' : 'Nothing to clear'}
/>
<InputText className="w-64" placeholder="Search" value={filter} onChange={e => setFilter(e.target.value)} />
</IconField>
</div>
<div className="flex-1 p-3 overflow-x-hidden">
<DataTable value={filtered} size="small" scrollable scrollHeight="flex" stripedRows>
<Column header="Type" body={renderName} className="w-[160px]" bodyClassName="whitespace-normal break-words" />
<Column header="Spawns In" body={renderSpawns} bodyClassName="whitespace-normal break-words text-[13px]" />
<Column
field="lifetime"
header="Lifetime"
className="w-[90px]"
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Total Mass"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.total_mass)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Max/jump"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.max_mass_per_jump)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Respawn"
className="w-[150px]"
body={renderRespawn}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
</DataTable>
</div>
</Dialog>
);
};

View File

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

View File

@@ -23,17 +23,17 @@ export const useCharacterActivityHandlers = () => {
/**
* Handle showing the character activity dialog
*/
const handleShowActivity = useCallback((days?: number | null) => {
const handleShowActivity = useCallback(() => {
// Update local state to show the dialog
update(state => ({
...state,
showCharacterActivity: true,
}));
// Send the command to the server with optional days parameter
// Send the command to the server
outCommand({
type: OutCommand.showActivity,
data: days !== undefined ? { days } : {},
data: {},
});
}, [outCommand, update]);

View File

@@ -1,36 +1,36 @@
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import {
SystemCustomLabelDialog,
SystemLinkSignatureDialog,
SystemSettingsDialog,
} from '@/hooks/Mapper/components/mapInterface/components';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import isEqual from 'lodash.isequal';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Node, useReactFlow, Viewport, XYPosition } from 'reactflow';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Node, useReactFlow, XYPosition } from 'reactflow';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { SystemPingDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemPingDialog';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import { MINIMAP_PLACEMENT_MAP } from '@/hooks/Mapper/constants.ts';
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import type { PanelPosition } from '@reactflow/core';
import { useHotkey } from '../../hooks/useHotkey';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemPingDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemPingDialog';
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MINIMAP_PLACEMENT_MAP } from '@/hooks/Mapper/constants.ts';
import type { PanelPosition } from '@reactflow/core';
import { MINI_MAP_PLACEMENT_OFFSETS } from './constants.ts';
// TODO: INFO - this component needs for abstract work with Map instance
@@ -48,7 +48,7 @@ export const MapWrapper = () => {
linkSignatureToSystem,
systemSignatures,
},
storedSettings: { interfaceSettings, settingsLocal, mapSettings, mapSettingsUpdate },
storedSettings: { interfaceSettings, settingsLocal },
} = useMapRootState();
const {
@@ -83,17 +83,8 @@ export const MapWrapper = () => {
systems,
systemSignatures,
deleteSystems,
mapSettingsUpdate,
});
ref.current = {
selectedConnections,
selectedSystems,
systemContextProps,
systems,
systemSignatures,
deleteSystems,
mapSettingsUpdate,
};
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, systemSignatures, deleteSystems };
useMapEventListener(event => {
runCommand(event);
@@ -106,7 +97,7 @@ export const MapWrapper = () => {
runCommand({
name: Commands.selectSystems,
data: { systems: selectedSystems, delay: 200 } as CommandSelectSystems,
data: { systems: selectedSystems } as CommandSelectSystems,
});
}
});
@@ -130,10 +121,6 @@ export const MapWrapper = () => {
[update],
);
const handleChangeViewport = useCallback((viewport: Viewport) => {
ref.current.mapSettingsUpdate({ viewport });
}, []);
const handleCommand: OutCommandHandler = useCallback(
event => {
switch (event.type) {
@@ -272,7 +259,6 @@ export const MapWrapper = () => {
onConnectionInfoClick={handleConnectionDbClick}
onSystemContextMenu={handleSystemContextMenu}
onSelectionContextMenu={handleSystemMultipleContext}
onChangeViewport={handleChangeViewport}
minimapClasses={minimapClasses}
isShowMinimap={showMinimap}
showKSpaceBG={isShowKSpace}
@@ -284,7 +270,6 @@ export const MapWrapper = () => {
onAddSystem={onAddSystem}
minimapPlacement={minimapPosition}
localShowShipName={settingsLocal.showShipName}
defaultViewport={mapSettings.viewport}
/>
{openSettings != null && (

View File

@@ -3,7 +3,6 @@ import {
WdEveEntityPortrait,
WdEveEntityPortraitSize,
WdEveEntityPortraitType,
WdImgButton,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
@@ -15,8 +14,6 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import clsx from 'clsx';
import { useCallback } from 'react';
import classes from './CharacterCard.module.scss';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
export type CharacterCardProps = {
compact?: boolean;
@@ -69,9 +66,6 @@ export const CharacterCard = ({
const shipType = char.ship?.ship_type_info?.name;
const locationShown = showSystem && char.location?.solar_system_id;
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
// INFO: Simple mode show only name and icon of ally/corp. By default it compact view
if (simpleMode) {
return (
@@ -250,24 +244,7 @@ export const CharacterCard = ({
{char.name}
</span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[6px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
className="min-w-[16px]"
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div>
{locationShown ? (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
<SystemView

View File

@@ -2,16 +2,10 @@ import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import classes from './MarkdownTextViewer.module.scss';
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
type MarkdownTextViewerProps = { children: string };
export const MarkdownTextViewer = ({ children }: MarkdownTextViewerProps) => {
return (
<div className={classes.MarkdownTextViewer}>
<Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>
</div>
);
return <Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>;
};

View File

@@ -4,17 +4,8 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import clsx from 'clsx';
type MenuItemWithInfoProps = {
infoTitle: ReactNode;
infoClass?: string;
tooltipWrapperClassName?: string;
} & WithChildren;
export const MenuItemWithInfo = ({
children,
infoClass,
infoTitle,
tooltipWrapperClassName,
}: MenuItemWithInfoProps) => {
type MenuItemWithInfoProps = { infoTitle: ReactNode; infoClass?: string } & WithChildren;
export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWithInfoProps) => {
return (
<div className="flex justify-between w-full h-full items-center">
{children}
@@ -22,7 +13,6 @@ export const MenuItemWithInfo = ({
content={infoTitle}
position={TooltipPosition.top}
className="!opacity-100 !pointer-events-auto"
wrapperClassName={tooltipWrapperClassName}
>
<div className={clsx('pi text-orange-400', infoClass)} />
</WdTooltipWrapper>

View File

@@ -1,20 +0,0 @@
import { Respawn } from '@/hooks/Mapper/types';
import clsx from 'clsx';
export const WORMHOLE_SPAWN_CLASSES_BG = {
[Respawn.static]: 'bg-lime-400/80 text-stone-950',
[Respawn.wandering]: 'bg-stone-800',
[Respawn.reverse]: 'bg-blue-400 text-stone-950',
};
type RespawnTagProps = { value: string };
export const RespawnTag = ({ value }: RespawnTagProps) => (
<span
className={clsx(
'px-[6px] py-[0px] rounded text-stone-300 text-[12px] font-[500] border border-stone-700',
WORMHOLE_SPAWN_CLASSES_BG[value as Respawn],
)}
>
{value}
</span>
);

View File

@@ -42,5 +42,5 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
return <SystemViewStandalone {...rest} {...systemInfo} />;
}
return <SystemViewStandalone customName={mapSystemInfo.name ?? undefined} {...rest} {...systemInfo} />;
return <SystemViewStandalone customName={mapSystemInfo.temporary_name ?? mapSystemInfo.name ?? undefined} {...rest} {...systemInfo} />;
};

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