mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 04:51:00 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 920ddbf065 | |||
| a9786b2e70 | |||
| d6ace14e70 | |||
| 2cbfd21539 | |||
| f7c070e45b | |||
| 06ed9734c6 | |||
| d93d4c6cd0 | |||
| faba2dee5d | |||
| ca3976fe62 | |||
| 7fea37eb5d | |||
| 68379dcf3a | |||
| 56f276af1b | |||
| f024d21cf5 | |||
| 67bc28cff4 | |||
| d7fa9dd570 | |||
| 428b058cbc | |||
| 0b5e318538 | |||
| 568507ab98 | |||
| 198f53b96f | |||
| 9babb94e07 | |||
| ef6a7b2583 | |||
| d446d711ba | |||
| d1af76f444 | |||
| 914d914b74 | |||
| 8f57224700 | |||
| 1e047eba91 |
@@ -0,0 +1,41 @@
|
||||
# pwap/web Docker build context (root) — exclude everything not needed
|
||||
# for `web/` build. Other monorepo subprojects stay out of the image.
|
||||
|
||||
# Other monorepo dirs (we only need web/ + shared/)
|
||||
exchange/
|
||||
mobile/
|
||||
pwap-mobile/
|
||||
docs/
|
||||
res/
|
||||
|
||||
# All node_modules everywhere
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# Git, GitHub
|
||||
.git/
|
||||
.github/
|
||||
|
||||
# Env files (built-in vars are passed as build-args from CI)
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
|
||||
# Editor / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Cache
|
||||
**/.eslintcache
|
||||
**/coverage/
|
||||
|
||||
# Already-built artifacts (we rebuild fresh inside container)
|
||||
web/dist/
|
||||
shared/**/dist/
|
||||
@@ -0,0 +1,58 @@
|
||||
name: CodeQL (SAST)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Every Sunday at 02:00 UTC — catches CVEs disclosed during the week
|
||||
- cron: '0 2 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }}
|
||||
runs-on: pwap-runner
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [javascript-typescript]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# OWASP top-10 + injection + auth flaws + prototype pollution
|
||||
queries: security-extended,security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
# GitHub Advanced Security dashboard upload requires paid plan.
|
||||
# SARIF saved as a downloadable artifact instead.
|
||||
upload: false
|
||||
output: /tmp/codeql-results
|
||||
|
||||
- name: Upload SARIF as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: codeql-sarif-${{ matrix.language }}
|
||||
path: /tmp/codeql-results/*.sarif
|
||||
retention-days: 7
|
||||
@@ -6,14 +6,25 @@ on:
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rollback_to:
|
||||
description: 'Rollback to git SHA (skips build, redeploys old image). Empty = normal deploy.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write # version bump commit
|
||||
packages: write # GHCR push
|
||||
|
||||
env:
|
||||
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: pezkuwichain/pwap-web
|
||||
|
||||
jobs:
|
||||
# ========================================
|
||||
@@ -21,7 +32,7 @@ jobs:
|
||||
# ========================================
|
||||
web:
|
||||
name: Web App
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: pwap-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -75,13 +86,164 @@ jobs:
|
||||
path: web/dist/
|
||||
|
||||
# ========================================
|
||||
# DEPLOY WEB APP TO VPS
|
||||
# BUILD & PUSH DOCKER IMAGE TO GHCR
|
||||
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
|
||||
# Tagged with git SHA so any commit can be redeployed by SHA.
|
||||
# ========================================
|
||||
deploy:
|
||||
name: Deploy Web
|
||||
runs-on: ubuntu-latest
|
||||
build-image:
|
||||
name: Build & Push Image
|
||||
runs-on: pwap-runner
|
||||
needs: [web, telegram-gate]
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # cosign keyless signing via Sigstore OIDC
|
||||
outputs:
|
||||
image_sha: ${{ steps.meta.outputs.image_sha }}
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract image metadata
|
||||
id: meta
|
||||
run: |
|
||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "image_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./
|
||||
file: ./web/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
|
||||
${{ steps.meta.outputs.image }}:latest
|
||||
build-args: |
|
||||
VITE_NETWORK=MAINNET
|
||||
VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||
VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||
VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||
VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||
VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:cache
|
||||
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
||||
provenance: false
|
||||
|
||||
- name: Sign image with cosign (keyless, Sigstore Fulcio)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE_DIGEST="${{ steps.meta.outputs.image }}@${{ steps.build.outputs.digest }}"
|
||||
# cosign needs its own registry auth — docker/login-action only writes
|
||||
# ~/.docker/config.json which cosign on self-hosted runner can't read
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
echo "Signing $IMAGE_DIGEST"
|
||||
cosign sign --yes "$IMAGE_DIGEST"
|
||||
echo "✅ Image signed (transparency log: rekor.sigstore.dev)"
|
||||
|
||||
# ========================================
|
||||
# TELEGRAM CEO APPROVAL GATE
|
||||
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
||||
# writes the gate file to /tmp/pexsec-gates/<sha> when CEO clicks
|
||||
# Approve/Cancel in Telegram. 30-minute timeout = deploy cancelled.
|
||||
# ========================================
|
||||
telegram-gate:
|
||||
name: Telegram deploy approval
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
|
||||
steps:
|
||||
- name: Send approval request and wait
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
SHA: ${{ github.sha }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
MESSAGE: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
SHORT="${SHA:0:7}"
|
||||
GATE_DIR="/tmp/pexsec-gates"
|
||||
mkdir -p "$GATE_DIR" 2>/dev/null || true
|
||||
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||
|
||||
# Strip Markdown special chars to prevent Telegram parse errors
|
||||
SAFE_MSG=$(echo "${MESSAGE}" | head -1 | tr -d '_*`[]()#|{}!' | cut -c1-120)
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"chat_id\": \"${CEO_CHAT_ID}\",
|
||||
\"parse_mode\": \"Markdown\",
|
||||
\"text\": \"🚀 *pwap/web Deploy Approval*\\n\\n\`${SHORT}\` — ${ACTOR}\\n\\n_${SAFE_MSG}_\\n\\nTargets: app.pezkuwichain.io + pex.mom\",
|
||||
\"reply_markup\": {
|
||||
\"inline_keyboard\": [[
|
||||
{\"text\": \"✅ Approve\", \"callback_data\": \"deploy_approve:${SHORT}\"},
|
||||
{\"text\": \"❌ Cancel\", \"callback_data\": \"deploy_cancel:${SHORT}\"}
|
||||
]]
|
||||
}
|
||||
}"
|
||||
|
||||
echo "Waiting for Telegram approval (max 30 min)..."
|
||||
TIMEOUT=1800
|
||||
ELAPSED=0
|
||||
while [ $ELAPSED -lt $TIMEOUT ]; do
|
||||
if [ -f "$GATE_DIR/$SHORT" ]; then
|
||||
DECISION=$(cat "$GATE_DIR/$SHORT")
|
||||
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||
if [ "$DECISION" = "approved" ]; then
|
||||
echo "Deploy approved."
|
||||
exit 0
|
||||
else
|
||||
echo "Deploy cancelled."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
sleep 10
|
||||
ELAPSED=$((ELAPSED + 10))
|
||||
done
|
||||
echo "No approval received within 30 minutes — deploy cancelled."
|
||||
exit 1
|
||||
|
||||
# ========================================
|
||||
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
||||
# ========================================
|
||||
bump-version:
|
||||
name: Bump Version
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit, telegram-gate, build-image]
|
||||
# Skip on rollback (workflow_dispatch with rollback_to set)
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||
outputs:
|
||||
new_version: ${{ steps.bump.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -101,61 +263,413 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version
|
||||
id: bump
|
||||
working-directory: ./web
|
||||
run: |
|
||||
npm version patch --no-git-tag-version
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "NEW_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
cd ..
|
||||
git add web/package.json
|
||||
git commit -m "chore(web): bump version to $VERSION [skip ci]" || echo "No version change"
|
||||
git push || echo "Nothing to push"
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: dist/
|
||||
# ========================================
|
||||
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
||||
# Pulls SHA-tagged image from GHCR, extracts /dist, scp to VPS.
|
||||
# Health check + auto-rollback to .deploy-tag-prev on failure.
|
||||
# ========================================
|
||||
deploy-app:
|
||||
name: Deploy app.pezkuwichain.io
|
||||
runs-on: pwap-runner
|
||||
needs: [telegram-gate, bump-version, build-image]
|
||||
if: |
|
||||
always() &&
|
||||
needs.telegram-gate.result == 'success' &&
|
||||
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
DOMAIN: app.pezkuwichain.io
|
||||
TARGET_PATH: /var/www/subdomains/app
|
||||
|
||||
- name: Deploy to VPS
|
||||
steps:
|
||||
- name: Determine image SHA
|
||||
id: sha
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||
echo "Rolling back to: ${{ github.event.inputs.rollback_to }}"
|
||||
else
|
||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Capture currently-live SHA (for auto-rollback)
|
||||
id: prev
|
||||
run: |
|
||||
# /.deploy-sha is written into every deploy; read what's live now
|
||||
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||
echo "Previous live SHA: ${PREV:-unknown}"
|
||||
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign (for verify)
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Verify image signature (cosign keyless)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
echo "Verifying signature for $IMAGE"
|
||||
cosign verify "$IMAGE" \
|
||||
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
> /dev/null
|
||||
echo "✅ Signature valid — image was built by trusted CI"
|
||||
|
||||
- name: Extract /dist from image
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
mkdir -p dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
# Stamp this build's SHA into dist so future deploys can read PREV
|
||||
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||
ls -la dist/ | head -10
|
||||
|
||||
- name: Deploy to DEV VPS
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/subdomains/app'
|
||||
strip_components: 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
- name: Health check (60s window)
|
||||
id: healthcheck
|
||||
run: |
|
||||
echo "✅ Deployed web app v${{ env.NEW_VERSION }} to app.pezkuwichain.io"
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||
exit 1
|
||||
|
||||
# ── Automatic rollback: pull PREV SHA image, redeploy, recheck ──
|
||||
- name: Auto-rollback to previous SHA
|
||||
id: rollback
|
||||
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||
run: |
|
||||
PREV="${{ steps.prev.outputs.prev }}"
|
||||
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
rm -rf dist && mkdir dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "$PREV" > dist/.deploy-sha
|
||||
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SCP rollback artifact
|
||||
if: steps.rollback.outcome == 'success'
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/subdomains/app'
|
||||
strip_components: 1
|
||||
|
||||
- name: Re-health-check after rollback
|
||||
if: steps.rollback.outcome == 'success'
|
||||
id: healthcheck_rb
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Rollback also failed!"
|
||||
exit 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||
|
||||
- name: Notify failure (Telegram)
|
||||
if: failure()
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||
run: |
|
||||
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||
elif [ -z "$PREV_SHA" ]; then
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||
else
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||
|
||||
# ========================================
|
||||
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
||||
# ========================================
|
||||
deploy-pex:
|
||||
name: Deploy pex.mom
|
||||
runs-on: pwap-runner
|
||||
needs: [telegram-gate, bump-version, build-image]
|
||||
if: |
|
||||
always() &&
|
||||
needs.telegram-gate.result == 'success' &&
|
||||
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
DOMAIN: pex.mom
|
||||
TARGET_PATH: /var/www/pex.mom
|
||||
|
||||
steps:
|
||||
- name: Determine image SHA
|
||||
id: sha
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Capture currently-live SHA (for auto-rollback)
|
||||
id: prev
|
||||
run: |
|
||||
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||
echo "Previous live SHA: ${PREV:-unknown}"
|
||||
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign (for verify)
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Verify image signature (cosign keyless)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
echo "Verifying signature for $IMAGE"
|
||||
cosign verify "$IMAGE" \
|
||||
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
> /dev/null
|
||||
echo "✅ Signature valid — image was built by trusted CI"
|
||||
|
||||
- name: Extract /dist from image
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
mkdir -p dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||
|
||||
- name: Deploy to VPS3
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_PEX_HOST }}
|
||||
username: ${{ secrets.VPS_PEX_USER }}
|
||||
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/pex.mom'
|
||||
strip_components: 1
|
||||
|
||||
- name: Health check (60s window)
|
||||
id: healthcheck
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||
exit 1
|
||||
|
||||
- name: Auto-rollback to previous SHA
|
||||
id: rollback
|
||||
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||
run: |
|
||||
PREV="${{ steps.prev.outputs.prev }}"
|
||||
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
rm -rf dist && mkdir dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "$PREV" > dist/.deploy-sha
|
||||
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SCP rollback artifact
|
||||
if: steps.rollback.outcome == 'success'
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_PEX_HOST }}
|
||||
username: ${{ secrets.VPS_PEX_USER }}
|
||||
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/pex.mom'
|
||||
strip_components: 1
|
||||
|
||||
- name: Re-health-check after rollback
|
||||
if: steps.rollback.outcome == 'success'
|
||||
id: healthcheck_rb
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Rollback also failed!"
|
||||
exit 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||
|
||||
- name: Notify failure (Telegram)
|
||||
if: failure()
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||
run: |
|
||||
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||
elif [ -z "$PREV_SHA" ]; then
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||
else
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||
|
||||
# ========================================
|
||||
# SECURITY CHECKS (BLOCKING)
|
||||
# npm audit (high + critical) + TruffleHog secret scan
|
||||
# ========================================
|
||||
security-audit:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: pwap-runner
|
||||
needs: [web]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Web - npm audit (critical only)
|
||||
- name: Web — npm audit (high + critical)
|
||||
working-directory: ./web
|
||||
run: |
|
||||
npm install
|
||||
npm audit --audit-level=critical
|
||||
npm audit --audit-level=high
|
||||
|
||||
- name: TruffleHog Secret Scan
|
||||
- name: TruffleHog — PR diff (verified secrets only)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
extra_args: --only-verified
|
||||
|
||||
- name: TruffleHog — full repo scan (verified secrets only)
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
extra_args: --only-verified
|
||||
|
||||
# ========================================
|
||||
# CI GATE — explicit merge-block
|
||||
# All required checks must succeed (or be skipped, e.g. for rollback path).
|
||||
# Branch protection on main should require this job's success.
|
||||
# ========================================
|
||||
ci-gate:
|
||||
name: CI Gate ✅
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Verify all required jobs succeeded or were intentionally skipped
|
||||
run: |
|
||||
results='${{ toJSON(needs) }}'
|
||||
echo "$results" | python3 -c "
|
||||
import json, sys
|
||||
needs = json.load(sys.stdin)
|
||||
failed = [name for name, job in needs.items() if job['result'] not in ('success', 'skipped')]
|
||||
if failed:
|
||||
print('❌ Required jobs failed: ' + ', '.join(failed))
|
||||
sys.exit(1)
|
||||
print('✅ All required CI jobs passed or skipped')
|
||||
"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Internal resources (never commit)
|
||||
res/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
-2002
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ pwap/
|
||||
|
||||
**Status:** ✅ Production Ready
|
||||
|
||||
The primary web interface for Pezkuwi blockchain at [pezkuwichain.app](https://pezkuwichain.app)
|
||||
The primary web interface for Pezkuwi blockchain at [app.pezkuwichain.io](https://app.pezkuwichain.io)
|
||||
|
||||
**Tech Stack:**
|
||||
- React 18 + TypeScript
|
||||
@@ -166,9 +166,10 @@ RTL support for CKB, AR, FA.
|
||||
|
||||
## Links
|
||||
|
||||
- **Website:** https://pezkuwichain.app
|
||||
- **SDK UI:** https://pezkuwichain.app/sdk
|
||||
- **Documentation:** https://docs.pezkuwichain.app
|
||||
- **Website:** https://app.pezkuwichain.io
|
||||
- **Website (alt):** https://pex.mom
|
||||
- **Exchange:** https://pex.network
|
||||
- **Documentation:** https://docs.pezkuwichain.io
|
||||
|
||||
## License
|
||||
|
||||
|
||||
-1
Submodule exchange deleted from bb3bc812ed
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 677 KiB |
@@ -1,31 +0,0 @@
|
||||
# PWAP WSL Dev Environment Setup
|
||||
# Run in PowerShell as Administrator
|
||||
|
||||
Write-Host "=== PWAP Dev Setup ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. Fix .wslconfig - enable mirrored networking
|
||||
$wslconfig = @"
|
||||
[wsl2]
|
||||
memory=48GB
|
||||
swap=16GB
|
||||
networkingMode=mirrored
|
||||
"@
|
||||
Set-Content -Path "$env:USERPROFILE\.wslconfig" -Value $wslconfig
|
||||
Write-Host "[OK] .wslconfig updated (mirrored networking)" -ForegroundColor Green
|
||||
|
||||
# 2. Restart ADB on default port
|
||||
$adbPath = "C:\Users\satos\Desktop\platform-tools\adb.exe"
|
||||
& $adbPath kill-server 2>$null
|
||||
Start-Sleep -Seconds 1
|
||||
& $adbPath start-server
|
||||
Write-Host "[OK] ADB server restarted on port 5037" -ForegroundColor Green
|
||||
& $adbPath devices
|
||||
|
||||
# 3. Shutdown WSL so new config takes effect
|
||||
Write-Host "`nShutting down WSL..." -ForegroundColor Yellow
|
||||
wsl --shutdown
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host "[OK] WSL shutdown complete" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n=== Done! ===" -ForegroundColor Cyan
|
||||
Write-Host "Now open WSL again and run: cd pwap && claude" -ForegroundColor White
|
||||
@@ -0,0 +1,53 @@
|
||||
# pwap/web — Static SPA build for distribution.
|
||||
# Build context is the pwap repo ROOT (not web/) because vite aliases like
|
||||
# @pezkuwi/utils, @shared/* resolve to ../shared/* — both web/ and shared/
|
||||
# must be in the build context.
|
||||
# Stage 1: build with Node. Stage 2: pure dist/ in busybox (smallest possible
|
||||
# attacker surface — no shell, no package manager, no node runtime).
|
||||
# Tag the resulting image with the git SHA in CI so rollback is just
|
||||
# "pull pwap-web:<old-sha>".
|
||||
|
||||
# ─── Stage 1: Build ────────────────────────────────────────────
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /build/web
|
||||
|
||||
# Copy package files first to leverage Docker layer cache when only src changes
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy shared/ first (less frequently changed), then web/ source
|
||||
COPY shared/ /build/shared/
|
||||
COPY web/ /build/web/
|
||||
|
||||
# Build args for environment-specific values (passed from CI)
|
||||
ARG VITE_NETWORK=MAINNET
|
||||
ARG VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||
ARG VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||
ARG VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||
ARG VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||
ARG VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||
ARG VITE_SUPABASE_URL
|
||||
ARG VITE_SUPABASE_ANON_KEY
|
||||
|
||||
ENV VITE_NETWORK=$VITE_NETWORK
|
||||
ENV VITE_WS_ENDPOINT=$VITE_WS_ENDPOINT
|
||||
ENV VITE_WS_ENDPOINT_FALLBACK_1=$VITE_WS_ENDPOINT_FALLBACK_1
|
||||
ENV VITE_ASSET_HUB_ENDPOINT=$VITE_ASSET_HUB_ENDPOINT
|
||||
ENV VITE_PEOPLE_CHAIN_ENDPOINT=$VITE_PEOPLE_CHAIN_ENDPOINT
|
||||
ENV VITE_WALLETCONNECT_PROJECT_ID=$VITE_WALLETCONNECT_PROJECT_ID
|
||||
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
|
||||
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ─── Stage 2: Distribution image ───────────────────────────────
|
||||
# busybox:musl gives us a tiny base (~1.5MB) with a shell for `cp` operations
|
||||
# during deploy extraction, but no npm/curl/wget/ssh — minimal attack surface
|
||||
# if the image were ever exposed.
|
||||
FROM busybox:musl
|
||||
WORKDIR /dist
|
||||
COPY --from=builder /build/web/dist /dist
|
||||
LABEL org.opencontainers.image.source="https://github.com/pezkuwichain/pwap"
|
||||
LABEL org.opencontainers.image.description="pwap/web static SPA — Pezkuwi wallet/exchange frontend"
|
||||
LABEL org.opencontainers.image.licenses="proprietary"
|
||||
CMD ["sh", "-c", "echo 'pwap-web image — extract /dist via: docker create + docker cp'; sleep infinity"]
|
||||
Generated
+297
-268
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -120,7 +120,8 @@
|
||||
"@pezkuwi/x-textdecoder": "^14.0.25",
|
||||
"@pezkuwi/x-textencoder": "^14.0.25",
|
||||
"@pezkuwi/x-ws": "^14.0.25",
|
||||
"@pezkuwi/networks": "^14.0.25"
|
||||
"@pezkuwi/networks": "^14.0.25",
|
||||
"elliptic": "^6.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
@@ -147,6 +148,7 @@
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||
"vitest": "^4.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ function ReferralHandler() {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const endpoint = import.meta.env.VITE_WS_ENDPOINT || 'ws://127.0.0.1:9944';
|
||||
const endpoint = import.meta.env.VITE_WS_ENDPOINT || 'wss://rpc.pezkuwichain.io';
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { Pez20Badge } from './Pez20Badge';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||
@@ -811,6 +812,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||
{t('balance.pezBalance')}
|
||||
</CardTitle>
|
||||
<Pez20Badge className="flex-shrink-0" />
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -853,6 +855,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
{t('balance.usdtBalance')}
|
||||
</CardTitle>
|
||||
<Pez20Badge className="flex-shrink-0" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -8,6 +8,12 @@ import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiEmoji, getCi
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
import { getKycStatus } from '@pezkuwi/lib/kyc';
|
||||
import LandingPageDesktop from './landing/LandingPageDesktop';
|
||||
import './landing/landing.css';
|
||||
import HeroSection from './HeroSection';
|
||||
import { NetworkStats } from './NetworkStats';
|
||||
import TrustScoreCalculator from './TrustScoreCalculator';
|
||||
import ChainSpecs from './ChainSpecs';
|
||||
import RewardDistribution from './RewardDistribution';
|
||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||
import NotificationBell from './notifications/NotificationBell';
|
||||
import ProposalWizard from './proposals/ProposalWizard';
|
||||
@@ -22,7 +28,7 @@ import {
|
||||
ExternalLink, FileEdit, Users2, MessageSquare, Wifi, WifiOff,
|
||||
Wallet, DollarSign, PiggyBank, History, Key, TrendingUp,
|
||||
ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, Users,
|
||||
Droplet, Mail, Coins, Menu, X, ChevronDown,
|
||||
Droplet, Coins, Menu, X, ChevronDown,
|
||||
} from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useWebSocket } from '@/contexts/WebSocketContext';
|
||||
@@ -56,11 +62,18 @@ interface AppItem {
|
||||
title: string; icon: string; imgIcon?: string;
|
||||
route: string; href?: string; comingSoon?: boolean; requiresAuth?: boolean;
|
||||
}
|
||||
interface AppSection { titleKey: string; emoji: string; borderColor: string; apps: AppItem[]; }
|
||||
interface AppSection {
|
||||
titleKey: string; emoji: string;
|
||||
headerBg: string; cardBg: string; iconBg: string;
|
||||
apps: AppItem[];
|
||||
}
|
||||
|
||||
const APP_SECTIONS: AppSection[] = [
|
||||
{
|
||||
titleKey: 'mobile.section.finance', emoji: '💰', borderColor: 'border-l-green-500',
|
||||
titleKey: 'mobile.section.finance', emoji: '💰',
|
||||
headerBg: 'bg-gradient-to-r from-green-900/80 to-green-800/60',
|
||||
cardBg: 'bg-green-950/40 border border-green-700/30',
|
||||
iconBg: 'hover:bg-green-900/40',
|
||||
apps: [
|
||||
{ title: 'mobile.app.wallet', icon: '👛', route: '/wallet' },
|
||||
{ title: 'mobile.app.bank', icon: '🏦', route: '/finance/bank' },
|
||||
@@ -73,7 +86,10 @@ const APP_SECTIONS: AppSection[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
titleKey: 'mobile.section.governance', emoji: '🏛️', borderColor: 'border-l-red-500',
|
||||
titleKey: 'mobile.section.governance', emoji: '🏛️',
|
||||
headerBg: 'bg-gradient-to-r from-red-900/80 to-red-800/60',
|
||||
cardBg: 'bg-red-950/40 border border-red-700/30',
|
||||
iconBg: 'hover:bg-red-900/40',
|
||||
apps: [
|
||||
{ title: 'mobile.app.president', icon: '👑', route: '/elections', requiresAuth: true },
|
||||
{ title: 'mobile.app.assembly', icon: '🏛️', route: '/governance/assembly' },
|
||||
@@ -86,7 +102,10 @@ const APP_SECTIONS: AppSection[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
titleKey: 'mobile.section.social', emoji: '💬', borderColor: 'border-l-blue-500',
|
||||
titleKey: 'mobile.section.social', emoji: '💬',
|
||||
headerBg: 'bg-gradient-to-r from-blue-900/80 to-blue-800/60',
|
||||
cardBg: 'bg-blue-950/40 border border-blue-700/30',
|
||||
iconBg: 'hover:bg-blue-900/40',
|
||||
apps: [
|
||||
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
||||
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
||||
@@ -99,7 +118,10 @@ const APP_SECTIONS: AppSection[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
titleKey: 'mobile.section.education', emoji: '📚', borderColor: 'border-l-yellow-500',
|
||||
titleKey: 'mobile.section.education', emoji: '📚',
|
||||
headerBg: 'bg-gradient-to-r from-amber-900/80 to-amber-800/60',
|
||||
cardBg: 'bg-amber-950/40 border border-amber-700/30',
|
||||
iconBg: 'hover:bg-amber-900/40',
|
||||
apps: [
|
||||
{ title: 'mobile.app.university', icon: '🎓', route: '/education/university' },
|
||||
{ title: 'mobile.app.perwerde', icon: '📖', route: '/education', requiresAuth: true },
|
||||
@@ -278,7 +300,6 @@ const AppLayout: React.FC = () => {
|
||||
onClick={(e) => { e.stopPropagation(); setOpenMenu(openMenu === 'hdr-trading' ? null : 'hdr-trading'); }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-gray-900/70 border border-red-500/30 text-sm text-gray-300 hover:text-white transition-colors whitespace-nowrap"
|
||||
>
|
||||
<ArrowRightLeft className="w-3.5 h-3.5 text-red-400" />
|
||||
{t('nav.trading')}
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -477,7 +498,8 @@ const AppLayout: React.FC = () => {
|
||||
<div className="pt-20 min-h-screen bg-gray-950"><P2PDashboard /></div>
|
||||
) : (
|
||||
/* ── LOGGED-IN DESKTOP HOME ── */
|
||||
<div className="pt-20 pb-20 min-h-screen bg-gray-950">
|
||||
<>
|
||||
<div className="pt-20 bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||
|
||||
{/* Greeting row */}
|
||||
@@ -534,12 +556,17 @@ const AppLayout: React.FC = () => {
|
||||
{/* Section cards — 2-column grid */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-5">
|
||||
{APP_SECTIONS.map((section, sectionIdx) => (
|
||||
<div key={section.titleKey} className="bg-gray-900/60 rounded-2xl border border-gray-800/60 overflow-hidden">
|
||||
<div className={`flex items-center px-5 py-3 border-l-4 ${section.borderColor} bg-gray-900/40`}>
|
||||
<h3 className="text-sm font-bold text-white tracking-wide">
|
||||
{t(section.titleKey)} {section.emoji}
|
||||
<div key={section.titleKey} className={`${section.cardBg} rounded-2xl overflow-hidden shadow-lg`}>
|
||||
{/* Colored header */}
|
||||
<div className={`flex items-center justify-between px-5 py-3.5 ${section.headerBg}`}>
|
||||
<h3 className="text-sm font-bold text-white tracking-wide drop-shadow">
|
||||
{section.emoji} {t(section.titleKey)}
|
||||
</h3>
|
||||
<span className="text-[11px] text-white/60 font-medium">
|
||||
{section.apps.length + (sectionIdx === 1 ? govExtras.length : 0)} apps
|
||||
</span>
|
||||
</div>
|
||||
{/* App icon grid */}
|
||||
<div className="grid grid-cols-4 gap-1 px-3 py-3">
|
||||
{section.apps.map((app) => {
|
||||
const needsLogin = app.requiresAuth && !user;
|
||||
@@ -548,7 +575,7 @@ const AppLayout: React.FC = () => {
|
||||
key={app.title}
|
||||
onClick={getAppClickHandler(app)}
|
||||
className={`flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl transition-all active:scale-95
|
||||
${app.comingSoon ? 'opacity-50' : 'hover:bg-gray-800/60'}`}
|
||||
${app.comingSoon ? 'opacity-40 cursor-default' : section.iconBg}`}
|
||||
>
|
||||
<div className="relative">
|
||||
{app.imgIcon ? (
|
||||
@@ -559,7 +586,7 @@ const AppLayout: React.FC = () => {
|
||||
{app.comingSoon && <span className="absolute -top-1 -right-2 text-[10px]">🔒</span>}
|
||||
{needsLogin && !app.comingSoon && <span className="absolute -top-1 -right-2 text-[10px]">🔑</span>}
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-300 font-medium text-center leading-tight">
|
||||
<span className="text-[11px] text-gray-200 font-medium text-center leading-tight">
|
||||
{t(app.title)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -570,10 +597,10 @@ const AppLayout: React.FC = () => {
|
||||
<button
|
||||
key={ex.title}
|
||||
onClick={ex.onAction}
|
||||
className="flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl transition-all active:scale-95 hover:bg-gray-800/60"
|
||||
className={`flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl transition-all active:scale-95 ${section.iconBg}`}
|
||||
>
|
||||
<span className="text-3xl">{ex.icon}</span>
|
||||
<span className="text-[11px] text-gray-300 font-medium text-center leading-tight">
|
||||
<span className="text-[11px] text-gray-200 font-medium text-center leading-tight">
|
||||
{t(ex.title)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -584,6 +611,12 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HeroSection />
|
||||
<NetworkStats key="network-stats-live" />
|
||||
<div id="trust-calculator"><TrustScoreCalculator /></div>
|
||||
<div id="chain-specs"><ChainSpecs /></div>
|
||||
<div id="rewards"><RewardDistribution /></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Back-to-home button */}
|
||||
@@ -599,9 +632,9 @@ const AppLayout: React.FC = () => {
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* ── BOTTOM TAB BAR (mirrors MobileHomeLayout) ── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-40 bg-gray-950/95 backdrop-blur-md border-t border-gray-800">
|
||||
<div className="flex items-center justify-around h-16 max-w-md mx-auto">
|
||||
{/* ── BOTTOM TAB BAR ── */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-gray-950/95 backdrop-blur-md border-t border-gray-800">
|
||||
<div className="flex items-center justify-around h-16">
|
||||
<BottomTabBtn icon="🏠" label={t('mobile.home', 'Home')} active={currentTab === 'home'} onClick={() => navigate('/')} />
|
||||
<BottomTabBtn icon="🏛️" label={t('mobile.citizen', 'Citizen')} active={currentTab === 'citizen'} onClick={() => navigate('/be-citizen')} accent />
|
||||
<BottomTabBtn icon="👥" label={t('mobile.referral', 'Referral')} active={currentTab === 'referral'} onClick={() => navigate('/dashboard')} />
|
||||
@@ -609,44 +642,63 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* ── FOOTER ── */}
|
||||
<footer className="bg-gray-950 border-t border-gray-800 py-12 pb-28">
|
||||
<div className="max-w-full mx-auto px-4">
|
||||
<div className="mb-8 space-y-2 text-sm text-gray-400 text-center">
|
||||
<p className="flex items-center justify-center gap-2"><Mail className="w-4 h-4" />info@pezkuwichain.io</p>
|
||||
<p className="flex items-center justify-center gap-2"><Mail className="w-4 h-4" />info@pezkuwichain.app</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 text-left">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">PezkuwiChain</h3>
|
||||
<p className="text-gray-400 text-sm">{t('footer.platform')}</p>
|
||||
<footer className="lp-footer pb-20">
|
||||
<div className="lp-container">
|
||||
<div className="lp-foot-grid">
|
||||
<div className="lp-foot-brand">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||
<div className="lp-logo-mark" style={{ width: 28, height: 28 }} />
|
||||
<h4>PezkuwiChain</h4>
|
||||
</div>
|
||||
<p>{t('landing.footer.desc')}</p>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<span className="lp-status-dot" />
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--fg-2)' }}>{t('landing.footer.mainnet')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4">{t('footer.about')}</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><a href="https://raw.githubusercontent.com/pezkuwichain/DKSweb/main/public/Whitepaper.pdf" download="Pezkuwi_Whitepaper.pdf" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.whitepaper')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="https://github.com/pezkuwichain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.github')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<div className="lp-foot-col">
|
||||
<h5>{t('landing.footer.network')}</h5>
|
||||
<ul>
|
||||
<li><a href="/network">{t('landing.footer.explorer')}</a></li>
|
||||
<li><a href="/telemetry">{t('landing.footer.telemetry')}</a></li>
|
||||
<li><a href="/network">{t('landing.footer.validators')}</a></li>
|
||||
<li><a href="/faucet">{t('landing.footer.faucet')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4">{t('footer.developers')}</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><a href="/api" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.api')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="/developers" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.sdk')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<div className="lp-foot-col">
|
||||
<h5>{t('landing.footer.use')}</h5>
|
||||
<ul>
|
||||
<li><a href="/wallet">{t('landing.footer.wallet')}</a></li>
|
||||
<li><a href="/p2p">{t('landing.footer.trade')}</a></li>
|
||||
<li><a href="/">{t('landing.footer.vote')}</a></li>
|
||||
<li><a href="/grants">{t('landing.footer.grants')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-4">{t('footer.community')}</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.discord')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="https://x.com/PezkuwiChain" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.twitter')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.telegram')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="https://www.youtube.com/@SatoshiQazi" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.youtube')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<li><a href="https://facebook.com/profile.php?id=61582484611719" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">{t('footer.facebook')}<ExternalLink className="w-3 h-3 ml-1" /></a></li>
|
||||
<div className="lp-foot-col">
|
||||
<h5>{t('landing.footer.build')}</h5>
|
||||
<ul>
|
||||
<li><a href="/docs">{t('landing.footer.docs')}</a></li>
|
||||
<li><a href="/api">{t('landing.footer.api')}</a></li>
|
||||
<li><a href="/developers">{t('landing.footer.sdk')}</a></li>
|
||||
<li><a href="https://github.com/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.github')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="lp-foot-col">
|
||||
<h5>{t('landing.footer.community')}</h5>
|
||||
<ul>
|
||||
<li><a href="/forum">{t('landing.footer.forum')}</a></li>
|
||||
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.discord')}</a></li>
|
||||
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer">{t('landing.footer.telegram')}</a></li>
|
||||
<li><a href="https://x.com/PezkuwiChain" target="_blank" rel="noopener noreferrer">{t('landing.footer.twitter')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t border-gray-800 text-center">
|
||||
<p className="text-gray-400 text-sm">{t('footer.copyright')}</p>
|
||||
<div className="lp-foot-bottom">
|
||||
<span>{t('landing.footer.copyright')}</span>
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
||||
<span className="lp-foot-flag" title="Kurdish flag" />
|
||||
<span className="lp-foot-kurdish-text">{t('landing.footer.builtBy')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -685,7 +737,7 @@ function DesktopScoreCard({ icon, label, value, sub, color, onClick, actionLabel
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex-shrink-0 w-36 bg-gray-900/80 rounded-xl border border-gray-800/60 border-l-4 ${color} p-3 space-y-1
|
||||
className={`flex-shrink-0 w-36 bg-gray-800/70 rounded-xl border border-gray-700/50 border-l-4 ${color} p-3 space-y-1
|
||||
${onClick ? 'cursor-pointer hover:bg-gray-800/60 transition-colors' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Small pill marking a token as a Pezkuwi token-standard asset.
|
||||
* PEZ-20 = fungible standard (pallet-assets on Asset Hub), PEZ-721 = NFT standard.
|
||||
* See docs.pezkuwichain.io → Token Standards.
|
||||
*/
|
||||
export const Pez20Badge: React.FC<{ standard?: 'PEZ-20' | 'PEZ-721'; className?: string }> = ({
|
||||
standard = 'PEZ-20',
|
||||
className = '',
|
||||
}) => (
|
||||
<a
|
||||
href="https://docs.pezkuwichain.io/token-standards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`${standard} token standard on Pezkuwi Asset Hub`}
|
||||
className={
|
||||
'inline-flex items-center rounded-full border border-blue-500/40 bg-blue-500/10 ' +
|
||||
'px-2 py-0.5 text-[10px] font-semibold tracking-wide text-blue-300 ' +
|
||||
'hover:bg-blue-500/20 transition-colors no-underline ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
{standard}
|
||||
</a>
|
||||
);
|
||||
|
||||
export default Pez20Badge;
|
||||
@@ -15,6 +15,8 @@ interface ChainStats {
|
||||
validators: number;
|
||||
nominators: number;
|
||||
collators: number;
|
||||
collatorsAH: number;
|
||||
collatorsPeople: number;
|
||||
activeProposals: number;
|
||||
totalVoters: number;
|
||||
citizenCount: number;
|
||||
@@ -325,6 +327,7 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const [stats, setStats] = useState<ChainStats>({
|
||||
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
||||
peers: 0, validators: 0, nominators: 0, collators: 0,
|
||||
collatorsAH: 0, collatorsPeople: 0,
|
||||
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
||||
tokensStakedPct: '—',
|
||||
});
|
||||
@@ -417,12 +420,7 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const validators = sessionVals.length;
|
||||
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nomCount = await (api.query.staking as any).counterForNominators?.();
|
||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||
} catch {}
|
||||
// Nominators/staking migrated to Asset Hub — counted in the Asset Hub effect below.
|
||||
})();
|
||||
}, [api, isApiReady]);
|
||||
|
||||
@@ -448,10 +446,18 @@ const LandingPageDesktop: React.FC = () => {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Nominators live on Asset Hub after the staking migration (AHM).
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const collCount = await (assetHubApi.query.collatorSelection as any)?.candidates?.();
|
||||
if (collCount != null) setStats(prev => ({ ...prev, collators: collCount.length }));
|
||||
const nomCount = await (assetHubApi.query.staking as any)?.counterForNominators?.();
|
||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||
} catch {}
|
||||
|
||||
// Collators are the invulnerable set (not staking candidates, which are empty).
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inv = await (assetHubApi.query.collatorSelection as any)?.invulnerables?.();
|
||||
if (inv != null) setStats(prev => ({ ...prev, collatorsAH: inv.length, collators: inv.length + prev.collatorsPeople }));
|
||||
} catch {}
|
||||
})();
|
||||
}, [assetHubApi, isAssetHubReady]);
|
||||
@@ -465,6 +471,13 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
||||
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
||||
} catch {}
|
||||
|
||||
// People Chain also runs invulnerable collators — add them to the total.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inv = await (peopleApi.query.collatorSelection as any)?.invulnerables?.();
|
||||
if (inv != null) setStats(prev => ({ ...prev, collatorsPeople: inv.length, collators: prev.collatorsAH + inv.length }));
|
||||
} catch {}
|
||||
})();
|
||||
}, [peopleApi, isPeopleReady]);
|
||||
|
||||
|
||||
@@ -165,9 +165,14 @@ export const WalletConnectModal: React.FC<WalletConnectModalProps> = ({ isOpen,
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('walletModal.wcOpenApp', 'Open pezWallet')}
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
{t('walletModal.wcInstallHint', "Don't have pezWallet? It will be available on Play Store soon.")}
|
||||
</p>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-gray-500 text-center hover:text-purple-400 transition-colors"
|
||||
>
|
||||
{t('walletModal.wcInstallHint', "Don't have pezWallet? Download it on Play Store.")}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
// Desktop: QR code
|
||||
|
||||
@@ -40,6 +40,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
const isMobile = useIsMobile();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showWCModal, setShowWCModal] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
@@ -58,7 +59,12 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
await connectWallet();
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
await connectWallet();
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAccount = (account: typeof accounts[0]) => {
|
||||
@@ -169,6 +175,32 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generic Error - any error not caught above */}
|
||||
{error && !error.includes('authorize') && !error.includes('not found') && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-300">{error}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
disabled={isConnecting}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingExtension', 'Approve in extension...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
{t('walletModal.tryAgain')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connected State */}
|
||||
{selectedAccount && !error && (
|
||||
<div className="space-y-4">
|
||||
@@ -351,13 +383,18 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
onClick={handleConnect}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
size="sm"
|
||||
disabled={isApiInitializing}
|
||||
disabled={isApiInitializing || isConnecting}
|
||||
>
|
||||
{isApiInitializing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingBlockchain', 'Connecting to blockchain...')}
|
||||
</>
|
||||
) : isConnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingExtension', 'Approve in extension...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
@@ -405,9 +442,15 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex items-center justify-center gap-1 text-xs text-gray-400">
|
||||
{t('walletModal.mobileComingSoon')}
|
||||
</div>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1 text-xs text-gray-400 hover:text-purple-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t('walletModal.mobilePlayStore', 'Download on Play Store')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,14 @@ if (typeof window !== 'undefined' && !import.meta.env.DEV) {
|
||||
import React, { createContext, useContext, useEffect, useState, useRef, ReactNode } from 'react';
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
|
||||
|
||||
const web3EnableWithTimeout = (origin: string, timeoutMs = 20_000): Promise<Awaited<ReturnType<typeof web3Enable>>> =>
|
||||
Promise.race([
|
||||
web3Enable(origin),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Extension connection timed out. Please check if the extension popup is blocked by your browser, then click Authorize and try again.')), timeoutMs)
|
||||
)
|
||||
]);
|
||||
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
|
||||
import { getCurrentNetworkConfig } from '../../../shared/blockchain/endpoints';
|
||||
@@ -371,7 +379,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
|
||||
try {
|
||||
// Enable extension (works for both desktop extension and pezWallet DApps browser)
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
const extensions = await web3EnableWithTimeout('PezkuwiChain');
|
||||
if (extensions.length === 0) return;
|
||||
|
||||
// Get accounts
|
||||
@@ -449,7 +457,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
// Desktop / pezWallet DApps browser: Try extension (injected provider)
|
||||
const hasExtension = !!(window as unknown as { injectedWeb3?: Record<string, unknown> }).injectedWeb3;
|
||||
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
const extensions = await web3EnableWithTimeout('PezkuwiChain');
|
||||
|
||||
if (extensions.length === 0) {
|
||||
if (hasExtension) {
|
||||
@@ -490,7 +498,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('❌ Wallet connection failed:', err);
|
||||
}
|
||||
setError('Failed to connect wallet');
|
||||
setError(err instanceof Error ? err.message : 'Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+194
-3
@@ -1840,14 +1840,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'محفظة الجوال',
|
||||
'walletModal.mobileDesc': 'للجوال — امسح رمز QR بتطبيق pezWallet',
|
||||
'walletModal.mobileConnect': 'اتصل بـ pezWallet',
|
||||
'walletModal.mobileComingSoon': 'قريباً على Play Store',
|
||||
'walletModal.mobileComingSoon': 'تنزيل من Play Store',
|
||||
'walletModal.mobilePlayStore': 'تنزيل من Play Store',
|
||||
'walletModal.connectingExtension': 'وافق في الإضافة...',
|
||||
'walletModal.or': 'أو',
|
||||
'walletModal.connectWC': 'اتصل عبر pezWallet (الجوال)',
|
||||
'walletModal.wcScanQR': 'امسح بـ pezWallet للاتصال',
|
||||
'walletModal.wcOpenWallet': 'اتصل بتطبيق pezWallet',
|
||||
'walletModal.wcWaitingMobile': 'وافق على الاتصال في pezWallet',
|
||||
'walletModal.wcOpenApp': 'افتح pezWallet',
|
||||
'walletModal.wcInstallHint': 'ليس لديك pezWallet؟ سيكون متاحاً قريباً على Play Store.',
|
||||
'walletModal.wcInstallHint': 'ليس لديك pezWallet؟ حمّله من Play Store.',
|
||||
'walletModal.wcGenerating': 'جاري إنشاء رمز QR...',
|
||||
'walletModal.wcWaiting': 'في انتظار اتصال المحفظة...',
|
||||
'walletModal.wcConnected': 'تم الاتصال!',
|
||||
@@ -2892,7 +2894,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'الشبكة غير متصلة',
|
||||
'networkStats.disconnectedDesc': 'تأكد من تشغيل عقدة المدقق على ws://127.0.0.1:9944',
|
||||
'networkStats.disconnectedDesc': 'تعذر الاتصال بالشبكة. يرجى المحاولة لاحقاً.',
|
||||
'networkStats.connecting': 'جاري الاتصال بالشبكة...',
|
||||
'networkStats.title': 'حالة الشبكة',
|
||||
'networkStats.connected': 'متصل',
|
||||
@@ -3938,4 +3940,193 @@ export default {
|
||||
'help.feature.community': 'التواصل مع المجتمع',
|
||||
'help.whatskurd.title': 'مراسلة WhatsKURD',
|
||||
'help.whatskurd.desc': 'تواصل معنا عبر نظام المراسلة على البلوك تشين',
|
||||
|
||||
// Landing page
|
||||
'landing.nav.network': 'الشبكة',
|
||||
'landing.nav.governance': 'الحوكمة',
|
||||
'landing.nav.trading': 'التداول',
|
||||
'landing.nav.citizens': 'المواطنون',
|
||||
'landing.nav.docs': 'التوثيق',
|
||||
'landing.nav.mainnet': 'الشبكة الرئيسية',
|
||||
'landing.nav.connectWallet': 'ربط المحفظة',
|
||||
'landing.nav.login': 'تسجيل الدخول',
|
||||
'landing.nav.peers': 'الأعضاء',
|
||||
'landing.hero.badge': 'الدولة الرقمية الكردية · الآن على الشبكة الرئيسية',
|
||||
'landing.hero.badgeVersion': 'v1.0',
|
||||
'landing.hero.h1part1': 'سلسلة كتل سيادية',
|
||||
'landing.hero.h1part2': 'لأمة',
|
||||
'landing.hero.h1accent': 'بلا حدود',
|
||||
'landing.hero.h1part3': '.',
|
||||
'landing.hero.sub': 'PezkuwiChain بلوك تشين عام ومفتوح — بُني أولاً للأمة الكردية، ولكل الشعوب عديمة الجنسية والأمم الثقافية حول العالم.',
|
||||
'landing.hero.cta.explore': 'استكشف الشبكة ←',
|
||||
'landing.hero.cta.whitepaper': 'اقرأ الورقة البيضاء',
|
||||
'landing.hero.sun.name': 'روج · شمس كردستان',
|
||||
'landing.hero.sun.rays': '٢١ شعاعاً · ↻ منذ ١٩٣٢',
|
||||
'landing.statbar.network': 'الشبكة',
|
||||
'landing.statbar.connected': 'متصل',
|
||||
'landing.statbar.latestBlock': 'آخر كتلة',
|
||||
'landing.statbar.finalized': 'مكتمل',
|
||||
'landing.statbar.finalizedMeta': '~كتلتان خلف',
|
||||
'landing.statbar.validators': 'المحققون',
|
||||
'landing.statbar.validatorsMeta': 'يتحققون من الكتل',
|
||||
'landing.statbar.collators': 'المجمّعون',
|
||||
'landing.statbar.collatorsMeta': 'ينتجون الكتل',
|
||||
'landing.statbar.nominators': 'المرشِّحون',
|
||||
'landing.statbar.nominatorsMeta': 'يراهنون على المحققين',
|
||||
'landing.ticker.block': 'كتلة',
|
||||
'landing.ticker.validators': 'محققون',
|
||||
'landing.ticker.nominators': 'مرشِّحون',
|
||||
'landing.ticker.citizens': 'مواطنون',
|
||||
'landing.ticker.staked': 'مراهَن به',
|
||||
'landing.ticker.proposals': 'مقترحات',
|
||||
'landing.ticker.peers': 'أعضاء',
|
||||
'landing.network.eyebrow': '// ٠١ · الشبكة الحية',
|
||||
'landing.network.h2': 'أرقام',
|
||||
'landing.network.h2em': 'تحكم نفسها.',
|
||||
'landing.network.p': 'كل مقياس أدناه مُجلَب مباشرة من سلسلة التتابع — ما يراه المحققون، تراه أنت.',
|
||||
'landing.network.activeProposals': 'المقترحات النشطة',
|
||||
'landing.network.proposalsMeta': '↗ الحوكمة حية',
|
||||
'landing.network.totalVoters': 'إجمالي الناخبين',
|
||||
'landing.network.votersMeta': 'التصويت بالاقتناع',
|
||||
'landing.network.tokensStaked': 'الرموز المراهَن بها',
|
||||
'landing.network.stakedMeta': 'من إجمالي العرض',
|
||||
'landing.network.citizens': 'مواطنون حول العالم',
|
||||
'landing.network.citizensMeta': 'في ١٨٧ دولة',
|
||||
'landing.features.eyebrow': '// ٠٢ · ما يمكنك فعله',
|
||||
'landing.features.h2': 'سلسلة واحدة، كل',
|
||||
'landing.features.h2em': 'أداة مدنية.',
|
||||
'landing.features.p': 'طبقة Layer 1 مستقلة — مشتقة أصلاً من Polkadot، والآن قاعدة كود سيادية كاملة.',
|
||||
'landing.features.01.eyebrow': '٠١ · الحوكمة',
|
||||
'landing.features.01.h3': 'صوّت على القواعد التي تحكم الشبكة والأمة.',
|
||||
'landing.features.01.p': 'استفتاءات بأسلوب OpenGov مع التصويت بالاقتناع، والتفويض، وفترة قرار ٧ أيام.',
|
||||
'landing.features.01.link': 'افتح لوحة الحوكمة ←',
|
||||
'landing.features.02.eyebrow': '٠٢ · المواطنة',
|
||||
'landing.features.02.h3': 'هويتك، موقّعة من الدولة.',
|
||||
'landing.features.02.p': 'NFT روحي على سلسلة الشعب. درجة الثقة، السمات الموثقة، وزن التصويت.',
|
||||
'landing.features.02.link': 'كن مواطناً ←',
|
||||
'landing.features.03.eyebrow': '٠٣ · الاقتصاد',
|
||||
'landing.features.03.h3': 'تداول وأقرض وموّل الأمة بـ HEZ.',
|
||||
'landing.features.03.p': 'مجمعات DEX على Asset Hub، وP2P مدعوم بالضمانة، وخزينة تمول المنح.',
|
||||
'landing.features.03.link': 'افتح الاقتصاد ←',
|
||||
'landing.features.04.eyebrow': '٠٤ · المحققون',
|
||||
'landing.features.04.p': 'Nominated Proof-of-Stake يؤمّن كل كتلة. راهن بـ HEZ لدعم محقق.',
|
||||
'landing.features.04.h3suffix': 'عقدة. ١٢ أمة. صفر توقف منذ التكوين.',
|
||||
'landing.features.04.link': 'شغّل عقدة ←',
|
||||
'landing.arch.eyebrow': '// ٠٣ · الهندسة المعمارية',
|
||||
'landing.arch.h2': 'ثلاث سلاسل،',
|
||||
'landing.arch.h2em': 'أمة واحدة.',
|
||||
'landing.arch.p': 'ثلاث سلاسل مستقلة، مملوكة بالكامل من قبل الشبكة.',
|
||||
'landing.arch.rc.tag': 'Pezkuwi RC · سلسلة التتابع',
|
||||
'landing.arch.rc.h4': 'Pezkuwi Relay',
|
||||
'landing.arch.rc.p': 'الحوكمة، واختيار المحقق، والتنسيق عبر السلاسل.',
|
||||
'landing.arch.ah.tag': 'Pezkuwi AH · Asset Hub',
|
||||
'landing.arch.ah.h4': 'HezDex على Asset Hub',
|
||||
'landing.arch.ah.p': 'إصدار HEZ وPEZ، مجمعات السيولة، المقايضات على السلسلة، الرهان.',
|
||||
'landing.arch.people.tag': 'Pezkuwi People · سلسلة الشعب',
|
||||
'landing.arch.people.h4': 'هوية Tîkî',
|
||||
'landing.arch.people.p': 'NFT المواطنة، KYC، درجة الثقة، الرسم الاجتماعي.',
|
||||
'landing.arch.stats.block': 'كتلة',
|
||||
'landing.arch.stats.time': 'الوقت',
|
||||
'landing.arch.stats.validators': 'محققون',
|
||||
'landing.arch.stats.staked': 'مراهَن به',
|
||||
'landing.arch.stats.collators': 'مجمّعون',
|
||||
'landing.arch.stats.nominators': 'مرشِّحون',
|
||||
'landing.arch.stats.citizens': 'مواطنون',
|
||||
'landing.arch.stats.countries': 'دول',
|
||||
'landing.tok.eyebrow': '// ٠٤ · الاقتصاد الرمزي',
|
||||
'landing.tok.h2': 'رمزان.',
|
||||
'landing.tok.h2em': 'دوران.',
|
||||
'landing.tok.p': 'HEZ وحدة المنفعة المستقرة للشبكة. PEZ رمز مكافأة المواطن.',
|
||||
'landing.tok.tabHez': 'HEZ · المنفعة',
|
||||
'landing.tok.tabPez': 'PEZ · المواطن',
|
||||
'landing.tok.total': 'الإجمالي',
|
||||
'landing.ref.eyebrow': '// كن مرجعاً',
|
||||
'landing.ref.h2': 'لا تدعو فقط —',
|
||||
'landing.ref.h2em': 'تُرشِّح.',
|
||||
'landing.ref.p': 'كل صديق توافق عليه يصبح مواطناً في PezkuwiChain ويمنحك +١٠ درجات ثقة.',
|
||||
'landing.ref.step1.label': 'الخطوة ١',
|
||||
'landing.ref.step1.title': 'شارك رابطك',
|
||||
'landing.ref.step1.desc': 'أرسل رابط إحالتك الشخصي من محفظة Pezkuwi إلى شخص تثق به.',
|
||||
'landing.ref.step2.label': 'الخطوة ٢',
|
||||
'landing.ref.step2.title': 'يتقدمون بطلب',
|
||||
'landing.ref.step2.desc': 'يقدم صديقك طلب مواطنة على PezkuwiChain وينتظر موافقتك.',
|
||||
'landing.ref.step3.label': 'الخطوة ٣',
|
||||
'landing.ref.step3.title': 'توافق — ثم يوقّعون',
|
||||
'landing.ref.step3.desc': 'كن مرجعهم، ثم يوقّعون معاملة المواطنة. تنمو درجة ثقتك بـ +١٠.',
|
||||
'landing.cta.eyebrow': '// انضم إلى الشبكة',
|
||||
'landing.cta.h2': 'المواطنة',
|
||||
'landing.cta.h2em': 'على بُعد توقيع واحد.',
|
||||
'landing.cta.p': 'اربط محفظتك، واسكُب NFT مواطنتك، وصوّت على المقترحات.',
|
||||
'landing.cta.become': 'كن مواطناً ←',
|
||||
'landing.cta.validator': 'شغّل محققاً',
|
||||
'landing.cta.services': 'الخدمات المتاحة',
|
||||
'landing.pallets.eyebrow': '// ٠٢ب · منظومة المحفظة',
|
||||
'landing.pallets.h2': 'كل أداة،',
|
||||
'landing.pallets.h2em': 'محفظة واحدة.',
|
||||
'landing.pallets.p': 'ستة عشر طبقاً منظمة في أربعة أعمدة — المالية، الحوكمة، الاجتماعي، والتعليم.',
|
||||
'landing.pallets.finance': 'المالية',
|
||||
'landing.pallets.financeCount': '٨ وحدات',
|
||||
'landing.pallets.governance': 'الحوكمة',
|
||||
'landing.pallets.governanceCount': '٨ وحدات',
|
||||
'landing.pallets.social': 'الاجتماعي',
|
||||
'landing.pallets.socialCount': '٥ مباشرة · ٣ قريباً',
|
||||
'landing.pallets.education': 'التعليم',
|
||||
'landing.pallets.educationCount': '٤ مباشرة · ٤ قريباً',
|
||||
'landing.pallets.comingSoon': 'قريباً',
|
||||
'landing.pallets.wallet': 'المحفظة',
|
||||
'landing.pallets.bank': 'البنك',
|
||||
'landing.pallets.exchange': 'البورصة',
|
||||
'landing.pallets.dex': 'Pez-DEX',
|
||||
'landing.pallets.p2p': 'P2P',
|
||||
'landing.pallets.b2b': 'B2B',
|
||||
'landing.pallets.zekat': 'الضريبة/الزكاة',
|
||||
'landing.pallets.launchpad': 'منصة الإطلاق',
|
||||
'landing.pallets.president': 'الرئيس',
|
||||
'landing.pallets.assembly': 'البرلمان',
|
||||
'landing.pallets.vote': 'التصويت',
|
||||
'landing.pallets.validators': 'المحققون',
|
||||
'landing.pallets.justice': 'العدالة',
|
||||
'landing.pallets.proposals': 'المقترحات',
|
||||
'landing.pallets.polls': 'الاستطلاعات',
|
||||
'landing.pallets.identity': 'الهوية',
|
||||
'landing.pallets.whatskurd': 'whatsKURD',
|
||||
'landing.pallets.forum': 'المنتدى',
|
||||
'landing.pallets.kurdmedia': 'KurdMedia',
|
||||
'landing.pallets.events': 'الفعاليات',
|
||||
'landing.pallets.help': 'المساعدة',
|
||||
'landing.pallets.music': 'الموسيقى',
|
||||
'landing.pallets.rewshenbir': 'روشنبير',
|
||||
'landing.pallets.referral': 'الإحالة',
|
||||
'landing.pallets.university': 'الجامعة',
|
||||
'landing.pallets.perwerde': 'پەروەردە',
|
||||
'landing.pallets.certificates': 'الشهادات',
|
||||
'landing.pallets.research': 'البحث',
|
||||
'landing.pallets.library': 'المكتبة',
|
||||
'landing.pallets.tutor': 'المعلم',
|
||||
'landing.pallets.labs': 'المختبرات',
|
||||
'landing.pallets.languages': 'اللغات',
|
||||
'landing.footer.desc': 'طبقة Layer 1 عامة ومفتوحة لسيادة رقمية للأمم عديمة الجنسية والأمم الثقافية — الكرد أولاً، كل الشعوب تالياً.',
|
||||
'landing.footer.mainnet': 'الشبكة الرئيسية · كتلة كل ٦ ثواني',
|
||||
'landing.footer.network': 'الشبكة',
|
||||
'landing.footer.use': 'الاستخدام',
|
||||
'landing.footer.build': 'البناء',
|
||||
'landing.footer.community': 'المجتمع',
|
||||
'landing.footer.explorer': 'المستكشف',
|
||||
'landing.footer.telemetry': 'القياس عن بُعد',
|
||||
'landing.footer.validators': 'المحققون',
|
||||
'landing.footer.faucet': 'الصنبور',
|
||||
'landing.footer.wallet': 'المحفظة',
|
||||
'landing.footer.trade': 'التداول',
|
||||
'landing.footer.vote': 'التصويت',
|
||||
'landing.footer.grants': 'المنح',
|
||||
'landing.footer.docs': 'التوثيق',
|
||||
'landing.footer.api': 'مرجع API',
|
||||
'landing.footer.sdk': 'SDK',
|
||||
'landing.footer.github': 'GitHub',
|
||||
'landing.footer.forum': 'المنتدى',
|
||||
'landing.footer.discord': 'Discord',
|
||||
'landing.footer.telegram': 'Telegram',
|
||||
'landing.footer.twitter': 'X / Twitter',
|
||||
'landing.footer.copyright': '© ٢٠٢٦ PezkuwiChain · مفتوح المصدر تحت MIT',
|
||||
'landing.footer.builtBy': 'بناه الشعب الرقمي الكردي لكل أمة وشعب عديم الجنسية',
|
||||
};
|
||||
|
||||
+194
-3
@@ -1830,14 +1830,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'جزدانی مۆبایل',
|
||||
'walletModal.mobileDesc': 'بۆ مۆبایل — بە pezWallet QR کۆد بخوێنەوە',
|
||||
'walletModal.mobileConnect': 'بە pezWallet پەیوەندی بکە',
|
||||
'walletModal.mobileComingSoon': 'بەم زووانە لە Play Store',
|
||||
'walletModal.mobileComingSoon': 'دابەزاندن لە Play Store',
|
||||
'walletModal.mobilePlayStore': 'دابەزاندن لە Play Store',
|
||||
'walletModal.connectingExtension': 'لە پێوەکراوەکەدا پەسەندی بکە...',
|
||||
'walletModal.or': 'یان',
|
||||
'walletModal.connectWC': 'بە pezWallet پەیوەندی بکە (مۆبایل)',
|
||||
'walletModal.wcScanQR': 'بۆ پەیوەندیکردن بە pezWallet سکان بکە',
|
||||
'walletModal.wcOpenWallet': 'بە pezWallet پەیوەندی بکە',
|
||||
'walletModal.wcWaitingMobile': 'لە pezWallet پەیوەندییەکە پشتڕاست بکەوە',
|
||||
'walletModal.wcOpenApp': 'pezWallet بکەوە',
|
||||
'walletModal.wcInstallHint': 'pezWallet نییە؟ بەم زووانە لە Play Store بەردەست دەبێت.',
|
||||
'walletModal.wcInstallHint': 'pezWallet نییە؟ لە Play Store دابەزێنە.',
|
||||
'walletModal.wcGenerating': 'QR کۆد دروستدەکرێت...',
|
||||
'walletModal.wcWaiting': 'چاوەڕوانی پەیوەندیکردنی جزدان...',
|
||||
'walletModal.wcConnected': 'پەیوەندی کرا!',
|
||||
@@ -2882,7 +2884,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'تۆڕ پچڕاوە',
|
||||
'networkStats.disconnectedDesc': 'دڵنیابەرەوە کە گرێی پشتڕاستکەرەوەکەت لە ws://127.0.0.1:9944 کاردەکات',
|
||||
'networkStats.disconnectedDesc': 'پەیوەندی بە تۆڕەکە نەکرا. تکایە دواتر هەوڵبدەرەوە.',
|
||||
'networkStats.connecting': 'پەیوەستبوون بە تۆڕ...',
|
||||
'networkStats.title': 'بارودۆخی تۆڕ',
|
||||
'networkStats.connected': 'پەیوەستە',
|
||||
@@ -3928,4 +3930,193 @@ export default {
|
||||
'help.feature.community': 'پەیوەندی کۆمەڵگە',
|
||||
'help.whatskurd.title': 'WhatsKURD پەیامگێڕ',
|
||||
'help.whatskurd.desc': 'لەڕێگەی سیستەمی پەیامگێڕی بلۆکچێیندا پەیوەندیمان پێوە بکە',
|
||||
|
||||
// Landing page
|
||||
'landing.nav.network': 'تۆڕ',
|
||||
'landing.nav.governance': 'حوکمڕانی',
|
||||
'landing.nav.trading': 'بازرگانی',
|
||||
'landing.nav.citizens': 'هاووڵاتیان',
|
||||
'landing.nav.docs': 'بەڵگەنامەکان',
|
||||
'landing.nav.mainnet': 'تۆڕی سەرەکی',
|
||||
'landing.nav.connectWallet': 'پووچەڵکە پەیوەندبکە',
|
||||
'landing.nav.login': 'چوونەژوورەوە',
|
||||
'landing.nav.peers': 'ئەندام',
|
||||
'landing.hero.badge': 'دەوڵەتی دیجیتاڵی کوردستان · ئێستا لە تۆڕی سەرەکیدا',
|
||||
'landing.hero.badgeVersion': 'v1.0',
|
||||
'landing.hero.h1part1': 'زنجیرەیەکی سەربەخۆ',
|
||||
'landing.hero.h1part2': 'بۆ نەتەوەیەکی',
|
||||
'landing.hero.h1accent': 'بێسنوور',
|
||||
'landing.hero.h1part3': '.',
|
||||
'landing.hero.sub': 'PezkuwiChain بلۆکچێینێکی گشتی و بێ-مۆڵەتەیی — یەکەم جار بۆ نەتەوەی کوردی دروستکراوە، و بۆ هەر خەڵکی بێ-دەوڵەت و نەتەوەی فەرهەنگی لەسەرتاسەری جیهان.',
|
||||
'landing.hero.cta.explore': 'تۆڕەکە بگەڕێ ←',
|
||||
'landing.hero.cta.whitepaper': 'کاغەزی سپی بخوێنەرەوە',
|
||||
'landing.hero.sun.name': 'ڕۆژ · خۆری کوردستان',
|
||||
'landing.hero.sun.rays': '٢١ تیژ · ↻ لە ١٩٣٢ەوە',
|
||||
'landing.statbar.network': 'تۆڕ',
|
||||
'landing.statbar.connected': 'پەیوەندیکراو',
|
||||
'landing.statbar.latestBlock': 'دوایین بلۆک',
|
||||
'landing.statbar.finalized': 'کۆتایی پێهاتوو',
|
||||
'landing.statbar.finalizedMeta': '~٢ بلۆک دواکەوتوو',
|
||||
'landing.statbar.validators': 'دڵنیاکەرەوەکان',
|
||||
'landing.statbar.validatorsMeta': 'بلۆکەکان دڵنیا دەکەنەوە',
|
||||
'landing.statbar.collators': 'کۆکەرەوەکان',
|
||||
'landing.statbar.collatorsMeta': 'بلۆکەکان بەرهەم دەهێنن',
|
||||
'landing.statbar.nominators': 'نامزدکەرەوەکان',
|
||||
'landing.statbar.nominatorsMeta': 'بۆ دڵنیاکەرەوەکان ستاک دەکەن',
|
||||
'landing.ticker.block': 'بلۆک',
|
||||
'landing.ticker.validators': 'دڵنیاکەرەوەکان',
|
||||
'landing.ticker.nominators': 'نامزدکەرەوەکان',
|
||||
'landing.ticker.citizens': 'هاووڵاتیان',
|
||||
'landing.ticker.staked': 'ستاک کراو',
|
||||
'landing.ticker.proposals': 'پێشنیارەکان',
|
||||
'landing.ticker.peers': 'ئەندامان',
|
||||
'landing.network.eyebrow': '// ٠١ · تۆڕی زیندوو',
|
||||
'landing.network.h2': 'ژمارەیەک کە',
|
||||
'landing.network.h2em': 'خۆی حوکمی دەکات.',
|
||||
'landing.network.p': 'هەر ئامارێک لەژێرەوە ڕاستەوخۆ لە زنجیرەی پەیوەندەوە وەردەگیرێت.',
|
||||
'landing.network.activeProposals': 'پێشنیارە چالاکەکان',
|
||||
'landing.network.proposalsMeta': '↗ حوکمڕانی زیندووە',
|
||||
'landing.network.totalVoters': 'کۆی دەنگدەران',
|
||||
'landing.network.votersMeta': 'دەنگدانی باوەڕ',
|
||||
'landing.network.tokensStaked': 'تۆکنی ستاک کراو',
|
||||
'landing.network.stakedMeta': 'لە کۆی دابینکراو',
|
||||
'landing.network.citizens': 'هاووڵاتیان لە جیهاندا',
|
||||
'landing.network.citizensMeta': 'لە ١٨٧ وڵاتدا',
|
||||
'landing.features.eyebrow': '// ٠٢ · ئەوەی دەتوانی بکەیت',
|
||||
'landing.features.h2': 'یەک زنجیرە، هەموو',
|
||||
'landing.features.h2em': 'ئامرازی شارستانی.',
|
||||
'landing.features.p': 'Layer 1ـێکی سەربەخۆ — لە سەرەتاوە فۆرکی Polkadot بوو، ئێستا کۆدبەیسێکی تەواو سەربەخۆیە.',
|
||||
'landing.features.01.eyebrow': '٠١ · حوکمڕانی',
|
||||
'landing.features.01.h3': 'لەسەر ئەو یاسانە دەنگ بدە کە تۆڕ و نەتەوەت بەڕێدەبات.',
|
||||
'landing.features.01.p': 'ڕێفراندۆمی شێوازی OpenGov بە دەنگدانی باوەڕ، نوێنەری، و ماوەی بڕیار ٧ ڕۆژ.',
|
||||
'landing.features.01.link': 'داشبۆردی حوکمڕانی بکەرەوە ←',
|
||||
'landing.features.02.eyebrow': '٠٢ · هاووڵاتیبوون',
|
||||
'landing.features.02.h3': 'ناسنامەت، واژووکراوی دەوڵەت.',
|
||||
'landing.features.02.p': 'NFT سۆڵبەندی لە زنجیرەی خەڵکدا. پوانەی باوەڕ، تایبەتمەندییە دڵنیاکراوەکان، ئیقرار دەنگدان.',
|
||||
'landing.features.02.link': 'بە هاووڵاتی بە ←',
|
||||
'landing.features.03.eyebrow': '٠٣ · ئابووری',
|
||||
'landing.features.03.h3': 'بازرگانی، قەرز، و فینانسکردنی نەتەوە بە HEZ.',
|
||||
'landing.features.03.p': 'گۆڵی DEX لە Asset Hub، P2P پاراستووی بە ئەمانەت، و خەزینە.',
|
||||
'landing.features.03.link': 'ئابووری بکەرەوە ←',
|
||||
'landing.features.04.eyebrow': '٠٤ · دڵنیاکەرەوەکان',
|
||||
'landing.features.04.p': 'Nominated Proof-of-Stake هەموو بلۆکێک دڵنیا دەکاتەوە. HEZ ستاک بکە بۆ پشتگیری دڵنیاکەرەوەیەک.',
|
||||
'landing.features.04.h3suffix': 'گرێ. ١٢ نەتەوە. هیچ وەستانێک لە دواوە نیە.',
|
||||
'landing.features.04.link': 'گرێ بخستەکارەوە ←',
|
||||
'landing.arch.eyebrow': '// ٠٣ · ئەندازیاری',
|
||||
'landing.arch.h2': 'سێ زنجیرە،',
|
||||
'landing.arch.h2em': 'یەک نەتەوە.',
|
||||
'landing.arch.p': 'سێ زنجیرەی سەربەخۆ، بە تەواوی خاوەنداری تۆڕەکەیە.',
|
||||
'landing.arch.rc.tag': 'Pezkuwi RC · زنجیرەی پەیوەند',
|
||||
'landing.arch.rc.h4': 'Pezkuwi Relay',
|
||||
'landing.arch.rc.p': 'حوکمڕانی، دیاریکردنی دڵنیاکەرەوە، و هاوئاهەنگیی نێوان زنجیرەکان.',
|
||||
'landing.arch.ah.tag': 'Pezkuwi AH · Asset Hub',
|
||||
'landing.arch.ah.h4': 'HezDex لە Asset Hub',
|
||||
'landing.arch.ah.p': 'دەرکردنی HEZ و PEZ، گۆڵی ئاوی، گۆڕینی لەسەر زنجیرە، ستاک.',
|
||||
'landing.arch.people.tag': 'Pezkuwi People · زنجیرەی خەڵک',
|
||||
'landing.arch.people.h4': 'ناسنامەی Tîkî',
|
||||
'landing.arch.people.p': 'NFT هاووڵاتیبوون، KYC، پوانەی باوەڕ، گرافی کۆمەڵایەتی.',
|
||||
'landing.arch.stats.block': 'بلۆک',
|
||||
'landing.arch.stats.time': 'کات',
|
||||
'landing.arch.stats.validators': 'دڵنیاکەرەوەکان',
|
||||
'landing.arch.stats.staked': 'ستاک کراو',
|
||||
'landing.arch.stats.collators': 'کۆکەرەوەکان',
|
||||
'landing.arch.stats.nominators': 'نامزدکەرەوەکان',
|
||||
'landing.arch.stats.citizens': 'هاووڵاتیان',
|
||||
'landing.arch.stats.countries': 'وڵاتان',
|
||||
'landing.tok.eyebrow': '// ٠٤ · تۆکنۆمیک',
|
||||
'landing.tok.h2': 'دوو تۆکن.',
|
||||
'landing.tok.h2em': 'دوو ڕۆڵ.',
|
||||
'landing.tok.p': 'HEZ یەکەی بەکارهێنانی ئیستەبلی تۆڕەکەیە. PEZ تۆکنی خەڵاتی هاووڵاتیانە.',
|
||||
'landing.tok.tabHez': 'HEZ · بەکارهێنان',
|
||||
'landing.tok.tabPez': 'PEZ · هاووڵاتی',
|
||||
'landing.tok.total': 'کۆی',
|
||||
'landing.ref.eyebrow': '// بە ئامادەکاری بە',
|
||||
'landing.ref.h2': 'تەنها بانگهێشت ناکەی —',
|
||||
'landing.ref.h2em': 'ئامادەکاری دەکەی.',
|
||||
'landing.ref.p': 'هەر هاوڕێیەک کە ئەمڕۆیی دەکەیت بە هاووڵاتی PezkuwiChain دەبێت و +١٠ پوانەی باوەڕت بۆ دەبات.',
|
||||
'landing.ref.step1.label': 'هەنگاوی ١',
|
||||
'landing.ref.step1.title': 'بەستەرەکەت هاوبەش بکە',
|
||||
'landing.ref.step1.desc': 'بەستەری ئامادەکارییەکەت لە جزدانی Pezkuwi بنێرە بۆ کەسێکی متمانەپێکراو.',
|
||||
'landing.ref.step2.label': 'هەنگاوی ٢',
|
||||
'landing.ref.step2.title': 'ئەوان داواکاری دەکەن',
|
||||
'landing.ref.step2.desc': 'هاوڕێکەت داواکاری هاووڵاتیبوون لە PezkuwiChain دەکات.',
|
||||
'landing.ref.step3.label': 'هەنگاوی ٣',
|
||||
'landing.ref.step3.title': 'تۆ ئەمڕۆیی دەکەیت — ئەوانیش واژوو دەکەن',
|
||||
'landing.ref.step3.desc': 'بە ئامادەکارییان بە، پاشان مامەڵەکەی هاووڵاتیبوون واژوو دەکەن. پوانەی باوەڕت +١٠ دەبێت.',
|
||||
'landing.cta.eyebrow': '// بەشدارییکردن لە تۆڕ',
|
||||
'landing.cta.h2': 'هاووڵاتیبوون',
|
||||
'landing.cta.h2em': 'یەک واژوو دووریە.',
|
||||
'landing.cta.p': 'جزدانەکەت پەیوەند بکە، NFT هاووڵاتیبوونت بخچە، و لەسەر پێشنیارەکانی دەوڵەتی دیجیتاڵ دەنگ بدە.',
|
||||
'landing.cta.become': 'بە هاووڵاتی بە ←',
|
||||
'landing.cta.validator': 'دڵنیاکەرەوە بخستەکار',
|
||||
'landing.cta.services': 'خزمەتگوزارییە بەردەستەکان',
|
||||
'landing.pallets.eyebrow': '// ٠٢ب · ئیکۆسیستەمی جزدان',
|
||||
'landing.pallets.h2': 'هەموو ئامرازێک،',
|
||||
'landing.pallets.h2em': 'یەک جزدان.',
|
||||
'landing.pallets.p': 'شازدە پالێت لە چوار ستوون ڕێکخراون — دارایی، حوکمڕانی، کۆمەڵایەتی، و پەروەردە.',
|
||||
'landing.pallets.finance': 'دارایی',
|
||||
'landing.pallets.financeCount': '٨ مۆدیوول',
|
||||
'landing.pallets.governance': 'حوکمڕانی',
|
||||
'landing.pallets.governanceCount': '٨ مۆدیوول',
|
||||
'landing.pallets.social': 'کۆمەڵایەتی',
|
||||
'landing.pallets.socialCount': '٥ زیندوو · ٣ بەزووی',
|
||||
'landing.pallets.education': 'پەروەردە',
|
||||
'landing.pallets.educationCount': '٤ زیندوو · ٤ بەزووی',
|
||||
'landing.pallets.comingSoon': 'بەزووی دێت',
|
||||
'landing.pallets.wallet': 'جزدان',
|
||||
'landing.pallets.bank': 'بانک',
|
||||
'landing.pallets.exchange': 'گۆڕینگە',
|
||||
'landing.pallets.dex': 'Pez-DEX',
|
||||
'landing.pallets.p2p': 'P2P',
|
||||
'landing.pallets.b2b': 'B2B',
|
||||
'landing.pallets.zekat': 'باج/زەکات',
|
||||
'landing.pallets.launchpad': 'لۆنچپاد',
|
||||
'landing.pallets.president': 'سەرۆک',
|
||||
'landing.pallets.assembly': 'پەرلەمان',
|
||||
'landing.pallets.vote': 'دەنگدان',
|
||||
'landing.pallets.validators': 'دڵنیاکەرەوەکان',
|
||||
'landing.pallets.justice': 'دادگەری',
|
||||
'landing.pallets.proposals': 'پێشنیارەکان',
|
||||
'landing.pallets.polls': 'دەنگپرسییەکان',
|
||||
'landing.pallets.identity': 'ناسنامە',
|
||||
'landing.pallets.whatskurd': 'whatsKURD',
|
||||
'landing.pallets.forum': 'فۆرۆم',
|
||||
'landing.pallets.kurdmedia': 'KurdMedia',
|
||||
'landing.pallets.events': 'ڕووداوەکان',
|
||||
'landing.pallets.help': 'یارمەتی',
|
||||
'landing.pallets.music': 'میوزیک',
|
||||
'landing.pallets.rewshenbir': 'ڕووشەنبیر',
|
||||
'landing.pallets.referral': 'ئامادەکاری',
|
||||
'landing.pallets.university': 'زانکۆ',
|
||||
'landing.pallets.perwerde': 'پەروەردە',
|
||||
'landing.pallets.certificates': 'بڕوانامەکان',
|
||||
'landing.pallets.research': 'توێژینەوە',
|
||||
'landing.pallets.library': 'پەرتووکخانە',
|
||||
'landing.pallets.tutor': 'مامۆستا',
|
||||
'landing.pallets.labs': 'تاقیگە',
|
||||
'landing.pallets.languages': 'زمانەکان',
|
||||
'landing.footer.desc': 'Layer 1ـێکی گشتی و بێ-مۆڵەتەیی بۆ سەروەری دیجیتاڵی نەتەوە و خەڵکی بێ-دەوڵەت — کورد یەکەم، هەموو خەڵکەکانیش.',
|
||||
'landing.footer.mainnet': 'تۆڕی سەرەکی · ٦ چرکەی بلۆک',
|
||||
'landing.footer.network': 'تۆڕ',
|
||||
'landing.footer.use': 'بەکارهێنان',
|
||||
'landing.footer.build': 'بنیادنان',
|
||||
'landing.footer.community': 'کۆمەڵگە',
|
||||
'landing.footer.explorer': 'گەڕاندن',
|
||||
'landing.footer.telemetry': 'تێلێمیتری',
|
||||
'landing.footer.validators': 'دڵنیاکەرەوەکان',
|
||||
'landing.footer.faucet': 'فاوسیت',
|
||||
'landing.footer.wallet': 'جزدان',
|
||||
'landing.footer.trade': 'بازرگانی',
|
||||
'landing.footer.vote': 'دەنگدان',
|
||||
'landing.footer.grants': 'گرانتەکان',
|
||||
'landing.footer.docs': 'بەڵگەنامەکان',
|
||||
'landing.footer.api': 'ئامرازی API',
|
||||
'landing.footer.sdk': 'SDK',
|
||||
'landing.footer.github': 'GitHub',
|
||||
'landing.footer.forum': 'فۆرۆم',
|
||||
'landing.footer.discord': 'Discord',
|
||||
'landing.footer.telegram': 'Telegram',
|
||||
'landing.footer.twitter': 'X / Twitter',
|
||||
'landing.footer.copyright': '© ٢٠٢٦ PezkuwiChain · کۆدی سەرچاوەی کراوە لە ژێر MIT',
|
||||
'landing.footer.builtBy': 'دروستکراوی خەڵکی دیجیتاڵی PezkuwiChain بۆ هەموو نەتەوە و خەڵکی بێ-دەوڵەت',
|
||||
};
|
||||
|
||||
@@ -2199,7 +2199,9 @@ export default {
|
||||
'walletModal.mobileTitle': 'Mobile Wallet',
|
||||
'walletModal.mobileDesc': 'For mobile — scan QR code with pezWallet app',
|
||||
'walletModal.mobileConnect': 'Connect with pezWallet',
|
||||
'walletModal.mobileComingSoon': 'Coming soon on Play Store',
|
||||
'walletModal.mobileComingSoon': 'Download on Play Store',
|
||||
'walletModal.mobilePlayStore': 'Download on Play Store',
|
||||
'walletModal.connectingExtension': 'Approve in extension...',
|
||||
'walletModal.or': 'or',
|
||||
'walletModal.connectWC': 'Connect with pezWallet (Mobile)',
|
||||
'walletModal.wcScanQR': 'Scan with pezWallet to connect',
|
||||
@@ -2208,7 +2210,7 @@ export default {
|
||||
'walletModal.wcWaiting': 'Waiting for wallet to connect...',
|
||||
'walletModal.wcWaitingMobile': 'Approve the connection in pezWallet',
|
||||
'walletModal.wcOpenApp': 'Open pezWallet',
|
||||
'walletModal.wcInstallHint': "Don't have pezWallet? It will be available on Play Store soon.",
|
||||
'walletModal.wcInstallHint': "Don't have pezWallet? Download it on Play Store.",
|
||||
'walletModal.wcConnected': 'Connected!',
|
||||
'walletModal.wcInstructions': 'Open pezWallet app → Settings → WalletConnect → Scan QR code',
|
||||
'walletModal.wcRetry': 'Try Again',
|
||||
@@ -3223,7 +3225,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'Network Disconnected',
|
||||
'networkStats.disconnectedDesc': 'Make sure your validator node is running at ws://127.0.0.1:9944',
|
||||
'networkStats.disconnectedDesc': 'Unable to connect to the network. Please try again later.',
|
||||
'networkStats.connecting': 'Connecting to Network...',
|
||||
'networkStats.title': 'Network Status',
|
||||
'networkStats.connected': 'Connected',
|
||||
|
||||
+194
-3
@@ -1800,14 +1800,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'کیف پول موبایل',
|
||||
'walletModal.mobileDesc': 'برای موبایل — کد QR را با اپلیکیشن pezWallet اسکن کنید',
|
||||
'walletModal.mobileConnect': 'اتصال با pezWallet',
|
||||
'walletModal.mobileComingSoon': 'به زودی در Play Store',
|
||||
'walletModal.mobileComingSoon': 'دانلود از Play Store',
|
||||
'walletModal.mobilePlayStore': 'دانلود از Play Store',
|
||||
'walletModal.connectingExtension': 'در افزونه تأیید کنید...',
|
||||
'walletModal.or': 'یا',
|
||||
'walletModal.connectWC': 'اتصال با pezWallet (موبایل)',
|
||||
'walletModal.wcScanQR': 'برای اتصال با pezWallet اسکن کنید',
|
||||
'walletModal.wcOpenWallet': 'اتصال با اپلیکیشن pezWallet',
|
||||
'walletModal.wcWaitingMobile': 'اتصال را در pezWallet تأیید کنید',
|
||||
'walletModal.wcOpenApp': 'باز کردن pezWallet',
|
||||
'walletModal.wcInstallHint': 'pezWallet ندارید؟ به زودی در Play Store در دسترس خواهد بود.',
|
||||
'walletModal.wcInstallHint': 'pezWallet ندارید؟ از Play Store دانلود کنید.',
|
||||
'walletModal.wcGenerating': 'در حال ایجاد کد QR...',
|
||||
'walletModal.wcWaiting': 'در انتظار اتصال کیف پول...',
|
||||
'walletModal.wcConnected': 'متصل شد!',
|
||||
@@ -2926,7 +2928,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'شبکه قطع شده',
|
||||
'networkStats.disconnectedDesc': 'مطمئن شوید که نود اعتبارسنج شما در ws://127.0.0.1:9944 در حال اجرا است',
|
||||
'networkStats.disconnectedDesc': 'اتصال به شبکه برقرار نشد. لطفاً بعداً دوباره امتحان کنید.',
|
||||
'networkStats.connecting': 'در حال اتصال به شبکه...',
|
||||
'networkStats.title': 'وضعیت شبکه',
|
||||
'networkStats.connected': 'متصل',
|
||||
@@ -3972,4 +3974,193 @@ export default {
|
||||
'help.feature.community': 'ارتباط با جامعه',
|
||||
'help.whatskurd.title': 'پیامرسانی WhatsKURD',
|
||||
'help.whatskurd.desc': 'از طریق سیستم پیامرسانی بلاکچین با ما تماس بگیرید',
|
||||
|
||||
// Landing page
|
||||
'landing.nav.network': 'شبکه',
|
||||
'landing.nav.governance': 'حکمرانی',
|
||||
'landing.nav.trading': 'معامله',
|
||||
'landing.nav.citizens': 'شهروندان',
|
||||
'landing.nav.docs': 'مستندات',
|
||||
'landing.nav.mainnet': 'شبکه اصلی',
|
||||
'landing.nav.connectWallet': 'اتصال کیف پول',
|
||||
'landing.nav.login': 'ورود',
|
||||
'landing.nav.peers': 'اعضا',
|
||||
'landing.hero.badge': 'دولت دیجیتال کردستان · اکنون در شبکه اصلی',
|
||||
'landing.hero.badgeVersion': 'v1.0',
|
||||
'landing.hero.h1part1': 'زنجیرهای مستقل',
|
||||
'landing.hero.h1part2': 'برای ملتی',
|
||||
'landing.hero.h1accent': 'بدون مرز',
|
||||
'landing.hero.h1part3': '.',
|
||||
'landing.hero.sub': 'PezkuwiChain یک بلاکچین عمومی و بدون مجوز است — ابتدا برای ملت کرد ساخته شده، و برای هر ملت بیدولت و فرهنگی در سراسر جهان.',
|
||||
'landing.hero.cta.explore': 'کاوش در شبکه ←',
|
||||
'landing.hero.cta.whitepaper': 'مطالعه وایتپیپر',
|
||||
'landing.hero.sun.name': 'روژ · خورشید کردستان',
|
||||
'landing.hero.sun.rays': '۲۱ شعاع · ↻ از ۱۹۳۲',
|
||||
'landing.statbar.network': 'شبکه',
|
||||
'landing.statbar.connected': 'متصل',
|
||||
'landing.statbar.latestBlock': 'آخرین بلوک',
|
||||
'landing.statbar.finalized': 'نهاییشده',
|
||||
'landing.statbar.finalizedMeta': '~۲ بلوک عقب',
|
||||
'landing.statbar.validators': 'اعتبارسنجان',
|
||||
'landing.statbar.validatorsMeta': 'بلوکها را تأیید میکنند',
|
||||
'landing.statbar.collators': 'جمعآورندگان',
|
||||
'landing.statbar.collatorsMeta': 'بلوکها را تولید میکنند',
|
||||
'landing.statbar.nominators': 'نامزدکنندگان',
|
||||
'landing.statbar.nominatorsMeta': 'برای اعتبارسنجان سهام میگذارند',
|
||||
'landing.ticker.block': 'بلوک',
|
||||
'landing.ticker.validators': 'اعتبارسنج',
|
||||
'landing.ticker.nominators': 'نامزدکننده',
|
||||
'landing.ticker.citizens': 'شهروند',
|
||||
'landing.ticker.staked': 'سهامگذاریشده',
|
||||
'landing.ticker.proposals': 'پیشنهادها',
|
||||
'landing.ticker.peers': 'عضو',
|
||||
'landing.network.eyebrow': '// ۰۱ · شبکه زنده',
|
||||
'landing.network.h2': 'اعدادی که',
|
||||
'landing.network.h2em': 'خودشان را اداره میکنند.',
|
||||
'landing.network.p': 'هر معیار زیر مستقیماً از زنجیره رله دریافت میشود — آنچه اعتبارسنجان میبینند، شما میبینید.',
|
||||
'landing.network.activeProposals': 'پیشنهادهای فعال',
|
||||
'landing.network.proposalsMeta': '↗ حکمرانی زنده است',
|
||||
'landing.network.totalVoters': 'مجموع رأیدهندگان',
|
||||
'landing.network.votersMeta': 'رأیگیری با اقناع',
|
||||
'landing.network.tokensStaked': 'توکنهای سهامگذاریشده',
|
||||
'landing.network.stakedMeta': 'از کل عرضه',
|
||||
'landing.network.citizens': 'شهروندان در جهان',
|
||||
'landing.network.citizensMeta': 'در ۱۸۷ کشور',
|
||||
'landing.features.eyebrow': '// ۰۲ · آنچه میتوانید انجام دهید',
|
||||
'landing.features.h2': 'یک زنجیره، هر',
|
||||
'landing.features.h2em': 'ابزار مدنی.',
|
||||
'landing.features.p': 'یک Layer 1 مستقل — در ابتدا از Polkadot فورک شده، اکنون یک پایگاه کد کاملاً مستقل.',
|
||||
'landing.features.01.eyebrow': '۰۱ · حکمرانی',
|
||||
'landing.features.01.h3': 'درباره قوانین حاکم بر شبکه و ملت رأی بدهید.',
|
||||
'landing.features.01.p': 'رفراندومهای OpenGov با رأیگیری اقناعی، تفویض اختیار، و دوره تصمیمگیری ۷ روزه.',
|
||||
'landing.features.01.link': 'باز کردن داشبورد حکمرانی ←',
|
||||
'landing.features.02.eyebrow': '۰۲ · شهروندی',
|
||||
'landing.features.02.h3': 'هویت شما، امضاشده توسط دولت.',
|
||||
'landing.features.02.p': 'NFT روحپیوند در زنجیره مردم. امتیاز اعتماد، ویژگیهای تأییدشده، وزن رأی.',
|
||||
'landing.features.02.link': 'شهروند شوید ←',
|
||||
'landing.features.03.eyebrow': '۰۳ · اقتصاد',
|
||||
'landing.features.03.h3': 'با HEZ معامله، وام و تأمین مالی ملت کنید.',
|
||||
'landing.features.03.p': 'استخرهای DEX در Asset Hub، P2P با پشتوانه امانت، و خزانهای که کمکهزینهها را تأمین میکند.',
|
||||
'landing.features.03.link': 'باز کردن اقتصاد ←',
|
||||
'landing.features.04.eyebrow': '۰۴ · اعتبارسنجان',
|
||||
'landing.features.04.p': 'Nominated Proof-of-Stake هر بلوک را ایمن میکند. HEZ سهامگذاری کنید تا از اعتبارسنج حمایت کنید.',
|
||||
'landing.features.04.h3suffix': 'گره. ۱۲ ملت. صفر توقف از لحظه تأسیس.',
|
||||
'landing.features.04.link': 'راهاندازی گره ←',
|
||||
'landing.arch.eyebrow': '// ۰۳ · معماری',
|
||||
'landing.arch.h2': 'سه زنجیره،',
|
||||
'landing.arch.h2em': 'یک ملت.',
|
||||
'landing.arch.p': 'سه زنجیره مستقل، کاملاً متعلق به شبکه.',
|
||||
'landing.arch.rc.tag': 'Pezkuwi RC · زنجیره رله',
|
||||
'landing.arch.rc.h4': 'Pezkuwi Relay',
|
||||
'landing.arch.rc.p': 'حکمرانی، انتخاب اعتبارسنج، و هماهنگی بین زنجیرهها.',
|
||||
'landing.arch.ah.tag': 'Pezkuwi AH · Asset Hub',
|
||||
'landing.arch.ah.h4': 'HezDex در Asset Hub',
|
||||
'landing.arch.ah.p': 'انتشار HEZ و PEZ، استخرهای نقدینگی، سوآپهای درونزنجیره، سهامگذاری.',
|
||||
'landing.arch.people.tag': 'Pezkuwi People · زنجیره مردم',
|
||||
'landing.arch.people.h4': 'هویت Tîkî',
|
||||
'landing.arch.people.p': 'NFT شهروندی، KYC، امتیاز اعتماد، گراف اجتماعی.',
|
||||
'landing.arch.stats.block': 'بلوک',
|
||||
'landing.arch.stats.time': 'زمان',
|
||||
'landing.arch.stats.validators': 'اعتبارسنج',
|
||||
'landing.arch.stats.staked': 'سهامگذاریشده',
|
||||
'landing.arch.stats.collators': 'جمعآورنده',
|
||||
'landing.arch.stats.nominators': 'نامزدکننده',
|
||||
'landing.arch.stats.citizens': 'شهروند',
|
||||
'landing.arch.stats.countries': 'کشور',
|
||||
'landing.tok.eyebrow': '// ۰۴ · توکنومیکس',
|
||||
'landing.tok.h2': 'دو توکن.',
|
||||
'landing.tok.h2em': 'دو نقش.',
|
||||
'landing.tok.p': 'HEZ واحد کاربردی پایدار شبکه است. PEZ توکن پاداش شهروندان است.',
|
||||
'landing.tok.tabHez': 'HEZ · کاربردی',
|
||||
'landing.tok.tabPez': 'PEZ · شهروند',
|
||||
'landing.tok.total': 'مجموع',
|
||||
'landing.ref.eyebrow': '// مرجع باشید',
|
||||
'landing.ref.h2': 'فقط دعوت نمیکنید —',
|
||||
'landing.ref.h2em': 'معرفی میکنید.',
|
||||
'landing.ref.p': 'هر دوستی که تأییدش کنید شهروند PezkuwiChain میشود و به شما +۱۰ امتیاز اعتماد میدهد.',
|
||||
'landing.ref.step1.label': 'مرحله ۱',
|
||||
'landing.ref.step1.title': 'لینک خود را به اشتراک بگذارید',
|
||||
'landing.ref.step1.desc': 'لینک ارجاع شخصیتان را از کیف پول Pezkuwi به کسی که به او اعتماد دارید ارسال کنید.',
|
||||
'landing.ref.step2.label': 'مرحله ۲',
|
||||
'landing.ref.step2.title': 'آنها درخواست میدهند',
|
||||
'landing.ref.step2.desc': 'دوست شما درخواست شهروندی در PezkuwiChain را ثبت میکند و منتظر تأیید شما میماند.',
|
||||
'landing.ref.step3.label': 'مرحله ۳',
|
||||
'landing.ref.step3.title': 'شما تأیید میکنید — آنها امضا میکنند',
|
||||
'landing.ref.step3.desc': 'مرجع آنها باشید، سپس تراکنش شهروندی را امضا میکنند. امتیاز اعتماد شما +۱۰ میشود.',
|
||||
'landing.cta.eyebrow': '// به شبکه بپیوندید',
|
||||
'landing.cta.h2': 'شهروندی',
|
||||
'landing.cta.h2em': 'یک امضا فاصله است.',
|
||||
'landing.cta.p': 'کیف پولتان را وصل کنید، NFT شهروندیتان را ضرب کنید، و درباره پیشنهادها رأی بدهید.',
|
||||
'landing.cta.become': 'شهروند شوید ←',
|
||||
'landing.cta.validator': 'اجرای اعتبارسنج',
|
||||
'landing.cta.services': 'خدمات موجود',
|
||||
'landing.pallets.eyebrow': '// ۰۲ب · اکوسیستم کیف پول',
|
||||
'landing.pallets.h2': 'هر ابزاری،',
|
||||
'landing.pallets.h2em': 'یک کیف پول.',
|
||||
'landing.pallets.p': 'شانزده پالت در چهار ستون سازمانیافته — مالی، حکمرانی، اجتماعی، و آموزشی.',
|
||||
'landing.pallets.finance': 'مالی',
|
||||
'landing.pallets.financeCount': '۸ ماژول',
|
||||
'landing.pallets.governance': 'حکمرانی',
|
||||
'landing.pallets.governanceCount': '۸ ماژول',
|
||||
'landing.pallets.social': 'اجتماعی',
|
||||
'landing.pallets.socialCount': '۵ زنده · ۳ بهزودی',
|
||||
'landing.pallets.education': 'آموزش',
|
||||
'landing.pallets.educationCount': '۴ زنده · ۴ بهزودی',
|
||||
'landing.pallets.comingSoon': 'بهزودی',
|
||||
'landing.pallets.wallet': 'کیف پول',
|
||||
'landing.pallets.bank': 'بانک',
|
||||
'landing.pallets.exchange': 'صرافی',
|
||||
'landing.pallets.dex': 'Pez-DEX',
|
||||
'landing.pallets.p2p': 'P2P',
|
||||
'landing.pallets.b2b': 'B2B',
|
||||
'landing.pallets.zekat': 'مالیات/زکات',
|
||||
'landing.pallets.launchpad': 'لانچپد',
|
||||
'landing.pallets.president': 'رئیسجمهور',
|
||||
'landing.pallets.assembly': 'پارلمان',
|
||||
'landing.pallets.vote': 'رأیگیری',
|
||||
'landing.pallets.validators': 'اعتبارسنجان',
|
||||
'landing.pallets.justice': 'عدالت',
|
||||
'landing.pallets.proposals': 'پیشنهادها',
|
||||
'landing.pallets.polls': 'نظرسنجیها',
|
||||
'landing.pallets.identity': 'هویت',
|
||||
'landing.pallets.whatskurd': 'whatsKURD',
|
||||
'landing.pallets.forum': 'انجمن',
|
||||
'landing.pallets.kurdmedia': 'KurdMedia',
|
||||
'landing.pallets.events': 'رویدادها',
|
||||
'landing.pallets.help': 'کمک',
|
||||
'landing.pallets.music': 'موسیقی',
|
||||
'landing.pallets.rewshenbir': 'روشنبیر',
|
||||
'landing.pallets.referral': 'ارجاع',
|
||||
'landing.pallets.university': 'دانشگاه',
|
||||
'landing.pallets.perwerde': 'پەروەردە',
|
||||
'landing.pallets.certificates': 'گواهینامهها',
|
||||
'landing.pallets.research': 'پژوهش',
|
||||
'landing.pallets.library': 'کتابخانه',
|
||||
'landing.pallets.tutor': 'معلم خصوصی',
|
||||
'landing.pallets.labs': 'آزمایشگاهها',
|
||||
'landing.pallets.languages': 'زبانها',
|
||||
'landing.footer.desc': 'یک Layer 1 عمومی و بدون مجوز برای حاکمیت دیجیتال ملتهای بیدولت و فرهنگی — کردها اول، همه مردم بعد.',
|
||||
'landing.footer.mainnet': 'شبکه اصلی · بلوک هر ۶ ثانیه',
|
||||
'landing.footer.network': 'شبکه',
|
||||
'landing.footer.use': 'استفاده',
|
||||
'landing.footer.build': 'ساخت',
|
||||
'landing.footer.community': 'جامعه',
|
||||
'landing.footer.explorer': 'کاوشگر',
|
||||
'landing.footer.telemetry': 'تلهمتری',
|
||||
'landing.footer.validators': 'اعتبارسنجان',
|
||||
'landing.footer.faucet': 'شیر آب',
|
||||
'landing.footer.wallet': 'کیف پول',
|
||||
'landing.footer.trade': 'معامله',
|
||||
'landing.footer.vote': 'رأیگیری',
|
||||
'landing.footer.grants': 'کمکهزینهها',
|
||||
'landing.footer.docs': 'مستندات',
|
||||
'landing.footer.api': 'مرجع API',
|
||||
'landing.footer.sdk': 'SDK',
|
||||
'landing.footer.github': 'GitHub',
|
||||
'landing.footer.forum': 'انجمن',
|
||||
'landing.footer.discord': 'Discord',
|
||||
'landing.footer.telegram': 'Telegram',
|
||||
'landing.footer.twitter': 'X / Twitter',
|
||||
'landing.footer.copyright': '© ۲۰۲۶ PezkuwiChain · متنباز تحت مجوز MIT',
|
||||
'landing.footer.builtBy': 'ساختهشده توسط مردم دیجیتال کرد برای هر ملت و مردم بیدولت',
|
||||
};
|
||||
|
||||
@@ -1857,14 +1857,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'Berîka Mobîl',
|
||||
'walletModal.mobileDesc': 'Ji bo mobîl — bi pezWallet QR kodê bişopîne',
|
||||
'walletModal.mobileConnect': 'Bi pezWallet Ve Girêbide',
|
||||
'walletModal.mobileComingSoon': 'Di nêzîk de li Play Store',
|
||||
'walletModal.mobileComingSoon': 'Ji Play Store dakêşin',
|
||||
'walletModal.mobilePlayStore': 'Ji Play Store dakêşin',
|
||||
'walletModal.connectingExtension': 'Di pêvekê de erê bikin...',
|
||||
'walletModal.or': 'an jî',
|
||||
'walletModal.connectWC': 'Bi pezWallet ve girêbide (Mobîl)',
|
||||
'walletModal.wcScanQR': 'Ji bo girêdanê bi pezWallet re bişopîne',
|
||||
'walletModal.wcOpenWallet': 'Bi pezWallet ve girêbide',
|
||||
'walletModal.wcWaitingMobile': 'Di pezWallet de girêdanê bipejirîne',
|
||||
'walletModal.wcOpenApp': 'pezWallet Veke',
|
||||
'walletModal.wcInstallHint': 'pezWallet tune? Di demek nêzîk de li Play Store dê hebe.',
|
||||
'walletModal.wcInstallHint': 'pezWallet tune? Ji Play Store dakêşin.',
|
||||
'walletModal.wcGenerating': 'QR kod tê çêkirin...',
|
||||
'walletModal.wcWaiting': 'Li benda girêdana berîkê...',
|
||||
'walletModal.wcConnected': 'Girêdayî!',
|
||||
@@ -2909,7 +2911,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'Girêdana Torê Qut Bû',
|
||||
'networkStats.disconnectedDesc': 'Piştrast bikin ku girêka piştrastkirina we li ws://127.0.0.1:9944 dixebite',
|
||||
'networkStats.disconnectedDesc': 'Girêdana bi torê re çênebû. Ji kerema xwe paşê dîsa biceribîne.',
|
||||
'networkStats.connecting': 'Bi Torê ve Tê Girêdan...',
|
||||
'networkStats.title': 'Rewşa Torê',
|
||||
'networkStats.connected': 'Girêdayî',
|
||||
|
||||
@@ -1851,14 +1851,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'Mobil Cüzdan',
|
||||
'walletModal.mobileDesc': 'Mobil için — pezWallet uygulamasıyla QR kodu tarayın',
|
||||
'walletModal.mobileConnect': 'pezWallet ile Bağlan',
|
||||
'walletModal.mobileComingSoon': 'Yakında Play Store\'da',
|
||||
'walletModal.mobileComingSoon': 'Play Store\'dan İndir',
|
||||
'walletModal.mobilePlayStore': 'Play Store\'dan İndir',
|
||||
'walletModal.connectingExtension': 'Uzantıda onaylayın...',
|
||||
'walletModal.or': 'veya',
|
||||
'walletModal.connectWC': 'pezWallet ile Bağlan (Mobil)',
|
||||
'walletModal.wcScanQR': 'Bağlanmak için pezWallet ile tarayın',
|
||||
'walletModal.wcOpenWallet': 'pezWallet uygulamasıyla bağlan',
|
||||
'walletModal.wcWaitingMobile': 'pezWallet\'ta bağlantıyı onaylayın',
|
||||
'walletModal.wcOpenApp': 'pezWallet\'ı Aç',
|
||||
'walletModal.wcInstallHint': 'pezWallet yok mu? Yakında Play Store\'da olacak.',
|
||||
'walletModal.wcInstallHint': 'pezWallet yok mu? Play Store\'dan indirin.',
|
||||
'walletModal.wcGenerating': 'QR kod oluşturuluyor...',
|
||||
'walletModal.wcWaiting': 'Cüzdan bağlantısı bekleniyor...',
|
||||
'walletModal.wcConnected': 'Bağlandı!',
|
||||
@@ -2912,7 +2914,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'Ağ Bağlantısı Kesildi',
|
||||
'networkStats.disconnectedDesc': 'Doğrulayıcı düğümünüzün ws://127.0.0.1:9944 adresinde çalıştığından emin olun',
|
||||
'networkStats.disconnectedDesc': 'Ağa bağlanılamadı. Lütfen daha sonra tekrar deneyin.',
|
||||
'networkStats.connecting': 'Ağa Bağlanılıyor...',
|
||||
'networkStats.title': 'Ağ Durumu',
|
||||
'networkStats.connected': 'Bağlandı',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -12,7 +12,6 @@ const BEREKETLI_API = `${BEREKETLI_URL}/v1`;
|
||||
*/
|
||||
export default function Bereketli() {
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -20,8 +19,10 @@ export default function Bereketli() {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
// Not signed in: skip SSO and send the user to the Bereketli site,
|
||||
// which handles its own login. Never dead-end on this interstitial.
|
||||
if (!session?.access_token) {
|
||||
setError(t('bereketli.noSession', 'Lütfen önce giriş yapın'));
|
||||
window.location.href = BEREKETLI_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,27 +47,14 @@ export default function Bereketli() {
|
||||
});
|
||||
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Bağlantı hatası');
|
||||
// SSO failed (expired token, network, etc.) — fall back to the public
|
||||
// Bereketli site instead of stranding the user on app.pezkuwichain.io.
|
||||
if (import.meta.env.DEV) console.warn('Bereketli SSO failed, falling back:', err);
|
||||
window.location.href = BEREKETLI_URL;
|
||||
}
|
||||
})();
|
||||
}, [t]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-6">
|
||||
<div className="text-center space-y-4">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg text-sm"
|
||||
>
|
||||
{t('common.backToHome', 'Ana Sayfaya Dön')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<div className="text-center space-y-3">
|
||||
|
||||
@@ -163,7 +163,7 @@ const Login: React.FC = () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
const BOT_ID = '8690398980';
|
||||
const BOT_ID = window.location.hostname === 'pex.mom' ? '8690398980' : '8754021997';
|
||||
const origin = window.location.origin;
|
||||
const popup = window.open(
|
||||
`https://oauth.telegram.org/auth?bot_id=${BOT_ID}&origin=${encodeURIComponent(origin)}&embed=1&request_access=write`,
|
||||
@@ -196,7 +196,7 @@ const Login: React.FC = () => {
|
||||
'apikey': supabaseKey,
|
||||
'Authorization': `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: JSON.stringify(tgData),
|
||||
body: JSON.stringify({ ...tgData, bot_id: BOT_ID }),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
@@ -42,11 +42,13 @@ Deno.serve(async (req) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN_PEZMOM')
|
||||
if (!botToken) throw new Error('TELEGRAM_BOT_TOKEN_PEZMOM not configured')
|
||||
|
||||
const body = await req.json()
|
||||
const { id, first_name, last_name, username, photo_url, auth_date, hash } = body
|
||||
const { id, first_name, last_name, username, photo_url, auth_date, hash, bot_id } = body
|
||||
|
||||
const botToken = bot_id === '8690398980'
|
||||
? Deno.env.get('TELEGRAM_BOT_TOKEN_PEZMOM')
|
||||
: Deno.env.get('TELEGRAM_BOT_TOKEN_PEXSEC')
|
||||
if (!botToken) throw new Error('Bot token not configured for bot_id: ' + bot_id)
|
||||
|
||||
if (!id || !hash || !auth_date) {
|
||||
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import subresourceIntegrity from 'vite-plugin-subresource-integrity';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command }) => ({
|
||||
@@ -38,6 +39,10 @@ export default defineConfig(({ command }) => ({
|
||||
},
|
||||
protocolImports: true,
|
||||
}),
|
||||
// SRI: production build sırasında <script>/<link> tag'lerine
|
||||
// sha384 integrity hash ekle. CDN/proxy compromise olsa bile
|
||||
// tampered asset browser tarafından load edilmez.
|
||||
command === 'build' ? subresourceIntegrity({ algorithm: 'sha384' }) : null,
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
mainFields: ['browser', 'module', 'main', 'exports'],
|
||||
|
||||
Reference in New Issue
Block a user