mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 05:51:09 +00:00
Compare commits
31 Commits
14d6da24db
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 070d682759 | |||
| cd56ab8fb6 | |||
| b012fcaaac | |||
| 7a1d3e7917 | |||
| 2ee3caac0d | |||
| 78e93e9766 | |||
| 83d66feacc | |||
| 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,416 @@ 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, production deps only)
|
||||
working-directory: ./web
|
||||
run: |
|
||||
npm install
|
||||
npm audit --audit-level=critical
|
||||
# Audit only production dependencies. Build tooling (vite, esbuild,
|
||||
# vite-plugin-node-polyfills → elliptic, etc.) ships to no user, and
|
||||
# advisories on those dev deps kept blocking production deploys.
|
||||
npm audit --audit-level=high --omit=dev
|
||||
|
||||
- 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
+300
-271
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
|
||||
<linearGradient id="fireGradient" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#1B5E20;stop-opacity:1"></stop>
|
||||
<stop offset="30%" style="stop-color:#FF6F00;stop-opacity:1"></stop>
|
||||
<stop offset="60%" style="stop-color:#FFD600;stop-opacity:1"></stop>
|
||||
<stop offset="100%" style="stop-color:#D32F2F;stop-opacity:1"></stop>
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<linearGradient id="innerFlame" x1="0%" y1="100%" x2="0%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#FFD600;stop-opacity:1"></stop>
|
||||
<stop offset="50%" style="stop-color:#FF8F00;stop-opacity:1"></stop>
|
||||
<stop offset="100%" style="stop-color:#FF5722;stop-opacity:1"></stop>
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="8" result="coloredBlur"></feGaussianBlur>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
|
||||
<radialGradient id="sunGlow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" style="stop-color:#FFD600;stop-opacity:0.6"></stop>
|
||||
<stop offset="100%" style="stop-color:#FFD600;stop-opacity:0"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
|
||||
<circle cx="256" cy="256" r="240" fill="#1a1a2e" opacity="0.9"></circle>
|
||||
|
||||
|
||||
<circle cx="256" cy="280" r="180" fill="url(#sunGlow)" filter="url(#glow)"></circle>
|
||||
|
||||
|
||||
<path d="M256 60
 C280 120 340 160 360 220
 C380 280 360 340 340 380
 C320 420 280 460 256 470
 C232 460 192 420 172 380
 C152 340 132 280 152 220
 C172 160 232 120 256 60Z" fill="url(#fireGradient)" filter="url(#glow)">
|
||||
|
||||
</path>
|
||||
|
||||
|
||||
<path d="M256 140
 C270 180 310 210 320 260
 C330 310 320 350 300 380
 C280 410 268 430 256 440
 C244 430 232 410 212 380
 C192 350 182 310 192 260
 C202 210 242 180 256 140Z" fill="url(#innerFlame)" opacity="0.95">
|
||||
|
||||
</path>
|
||||
|
||||
|
||||
<ellipse cx="256" cy="320" rx="50" ry="80" fill="#FFFDE7" opacity="0.8">
|
||||
|
||||
|
||||
</ellipse>
|
||||
|
||||
|
||||
<circle cx="200" cy="180" r="8" fill="#4CAF50" opacity="0.8">
|
||||
|
||||
|
||||
</circle>
|
||||
<circle cx="312" cy="200" r="6" fill="#4CAF50" opacity="0.7">
|
||||
|
||||
|
||||
</circle>
|
||||
<circle cx="230" cy="150" r="5" fill="#81C784" opacity="0.6">
|
||||
|
||||
</circle>
|
||||
<circle cx="280" cy="165" r="7" fill="#66BB6A" opacity="0.7">
|
||||
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
+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,20 +102,26 @@ 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' },
|
||||
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', comingSoon: true },
|
||||
{ title: 'mobile.app.help', icon: '❓', route: '/help' },
|
||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', href: 'https://kurdishtts.pezkiwi.app' },
|
||||
{ title: 'mobile.app.loto', icon: '🔥', imgIcon: '/loto-icon.svg', route: '/forum', href: 'https://loto.pex.mom' },
|
||||
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
||||
{ title: 'mobile.app.rewshenbir',icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
||||
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
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}
|
||||
>
|
||||
|
||||
@@ -87,8 +87,8 @@ const APP_SECTIONS: AppSection[] = [
|
||||
{ title: 'mobile.app.whatsKurd', icon: '💬', route: '/social/whatskurd' },
|
||||
{ title: 'mobile.app.forum', icon: '📰', route: '/forum' },
|
||||
{ title: 'mobile.app.kurdMedia', icon: '📺', route: '/social/kurdmedia' },
|
||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', comingSoon: true },
|
||||
{ title: 'mobile.app.help', icon: '❓', route: '/help' },
|
||||
{ title: 'mobile.app.events', icon: '📅', route: '/forum', href: 'https://kurdishtts.pezkiwi.app' },
|
||||
{ title: 'mobile.app.loto', icon: '🔥', imgIcon: '/loto-icon.svg', route: '/forum', href: 'https://loto.pex.mom' },
|
||||
{ title: 'mobile.app.music', icon: '🎵', route: '/forum', comingSoon: true },
|
||||
{ title: 'mobile.app.rewshenbir', icon: '📡', imgIcon: '/rewshenbir-icon.png', route: '/rewshenbir', href: 'https://rewshenbir.pezkuwi.app' },
|
||||
{ title: 'mobile.app.referral', icon: '👥', route: '/dashboard', requiresAuth: true },
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -1086,7 +1099,7 @@ const LandingPageDesktop: React.FC = () => {
|
||||
<PalletItem icon="lp-i-chat" label={t('landing.pallets.whatskurd')} to="/social/whatskurd" requiresLogin />
|
||||
<PalletItem icon="lp-i-forum" label={t('landing.pallets.forum')} to="/forum" />
|
||||
<PalletItem icon="lp-i-media" label={t('landing.pallets.kurdmedia')} to="/social/kurdmedia" requiresLogin />
|
||||
<PalletItem icon="lp-i-cal" label={t('landing.pallets.events')} locked />
|
||||
<PalletItem icon="lp-i-cal" label={t('landing.pallets.events')} external="https://kurdishtts.pezkiwi.app" />
|
||||
<PalletItem icon="lp-i-help" label={t('landing.pallets.help')} to="/help" />
|
||||
<PalletItem icon="lp-i-music" label={t('landing.pallets.music')} locked />
|
||||
<PalletItem imgSrc="/rewshenbir-icon.png" label={t('landing.pallets.rewshenbir')} external="https://rewshenbir.pezkuwi.app" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store, Zap, Blocks, MessageSquare } from 'lucide-react';
|
||||
import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store, Zap, Blocks, MessageSquare, CreditCard } from 'lucide-react';
|
||||
import { AdList } from './AdList';
|
||||
import { CreateAd } from './CreateAd';
|
||||
import { NotificationBell } from './NotificationBell';
|
||||
@@ -191,6 +191,15 @@ export function P2PDashboard() {
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
href="https://buy-sell.pezkiwi.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative flex flex-col items-center gap-0.5 px-3 py-1.5 rounded-lg hover:bg-amber-900/30 transition-colors"
|
||||
>
|
||||
<CreditCard className="w-5 h-5 text-amber-400" />
|
||||
<span className="text-[10px] text-amber-300">{t('p2pNav.buyVisa')}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+197
-4
@@ -1561,6 +1561,7 @@ export default {
|
||||
'p2pNav.orders': 'الطلبات',
|
||||
'p2pNav.ads': 'الإعلانات',
|
||||
'p2pNav.messages': 'الرسائل',
|
||||
'p2pNav.buyVisa': 'اشترِ بفيزا',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'الرسائل',
|
||||
@@ -1840,14 +1841,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 +2895,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'الشبكة غير متصلة',
|
||||
'networkStats.disconnectedDesc': 'تأكد من تشغيل عقدة المدقق على ws://127.0.0.1:9944',
|
||||
'networkStats.disconnectedDesc': 'تعذر الاتصال بالشبكة. يرجى المحاولة لاحقاً.',
|
||||
'networkStats.connecting': 'جاري الاتصال بالشبكة...',
|
||||
'networkStats.title': 'حالة الشبكة',
|
||||
'networkStats.connected': 'متصل',
|
||||
@@ -3785,7 +3788,7 @@ export default {
|
||||
'mobile.app.bank': 'البنك',
|
||||
'mobile.app.exchange': 'البورصة',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'الضريبة/الزكاة',
|
||||
'mobile.app.launchpad': 'منصة الإطلاق',
|
||||
@@ -3802,6 +3805,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'كورد ميديا',
|
||||
'mobile.app.events': 'الفعاليات',
|
||||
'mobile.app.help': 'المساعدة',
|
||||
'mobile.app.loto': 'لوتو',
|
||||
'mobile.app.music': 'الموسيقى',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.referral': 'الإحالة',
|
||||
@@ -3938,4 +3942,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': 'بناه الشعب الرقمي الكردي لكل أمة وشعب عديم الجنسية',
|
||||
};
|
||||
|
||||
+197
-4
@@ -1551,6 +1551,7 @@ export default {
|
||||
'p2pNav.orders': 'داواکاریەکان',
|
||||
'p2pNav.ads': 'ڕیکلامەکان',
|
||||
'p2pNav.messages': 'پەیامەکان',
|
||||
'p2pNav.buyVisa': 'بە ڤیزا بکڕە',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'پەیامەکان',
|
||||
@@ -1830,14 +1831,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 +2885,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'تۆڕ پچڕاوە',
|
||||
'networkStats.disconnectedDesc': 'دڵنیابەرەوە کە گرێی پشتڕاستکەرەوەکەت لە ws://127.0.0.1:9944 کاردەکات',
|
||||
'networkStats.disconnectedDesc': 'پەیوەندی بە تۆڕەکە نەکرا. تکایە دواتر هەوڵبدەرەوە.',
|
||||
'networkStats.connecting': 'پەیوەستبوون بە تۆڕ...',
|
||||
'networkStats.title': 'بارودۆخی تۆڕ',
|
||||
'networkStats.connected': 'پەیوەستە',
|
||||
@@ -3775,7 +3778,7 @@ export default {
|
||||
'mobile.app.bank': 'بانک',
|
||||
'mobile.app.exchange': 'ئاڵوگۆڕ',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'باج/زەکات',
|
||||
'mobile.app.launchpad': 'دەستپێکردن',
|
||||
@@ -3792,6 +3795,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'کوردمیدیا',
|
||||
'mobile.app.events': 'چالاکی',
|
||||
'mobile.app.help': 'یارمەتی',
|
||||
'mobile.app.loto': 'لۆتۆ',
|
||||
'mobile.app.music': 'مۆسیقا',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.referral': 'ئاماژە',
|
||||
@@ -3928,4 +3932,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 بۆ هەموو نەتەوە و خەڵکی بێ-دەوڵەت',
|
||||
};
|
||||
|
||||
@@ -1915,6 +1915,7 @@ export default {
|
||||
'p2pNav.orders': 'Orders',
|
||||
'p2pNav.ads': 'Ads',
|
||||
'p2pNav.messages': 'Messages',
|
||||
'p2pNav.buyVisa': 'Buy with Visa',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'Messages',
|
||||
@@ -2199,7 +2200,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 +2211,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 +3226,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',
|
||||
@@ -3837,7 +3840,7 @@ export default {
|
||||
'mobile.app.bank': 'Bank',
|
||||
'mobile.app.exchange': 'Exchange',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'Bac/Zekat',
|
||||
'mobile.app.launchpad': 'Launchpad',
|
||||
@@ -3854,6 +3857,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'KurdMedia',
|
||||
'mobile.app.events': 'Events',
|
||||
'mobile.app.help': 'Help',
|
||||
'mobile.app.loto': 'Loto',
|
||||
'mobile.app.music': 'Music',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.rewshenbir': 'Rewshenbir',
|
||||
|
||||
+197
-4
@@ -1585,6 +1585,7 @@ export default {
|
||||
'p2pNav.orders': 'سفارشات',
|
||||
'p2pNav.ads': 'آگهیها',
|
||||
'p2pNav.messages': 'پیامها',
|
||||
'p2pNav.buyVisa': 'خرید با ویزا',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'پیامها',
|
||||
@@ -1800,14 +1801,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 +2929,7 @@ export default {
|
||||
|
||||
// Network Stats
|
||||
'networkStats.disconnected': 'شبکه قطع شده',
|
||||
'networkStats.disconnectedDesc': 'مطمئن شوید که نود اعتبارسنج شما در ws://127.0.0.1:9944 در حال اجرا است',
|
||||
'networkStats.disconnectedDesc': 'اتصال به شبکه برقرار نشد. لطفاً بعداً دوباره امتحان کنید.',
|
||||
'networkStats.connecting': 'در حال اتصال به شبکه...',
|
||||
'networkStats.title': 'وضعیت شبکه',
|
||||
'networkStats.connected': 'متصل',
|
||||
@@ -3819,7 +3822,7 @@ export default {
|
||||
'mobile.app.bank': 'بانک',
|
||||
'mobile.app.exchange': 'صرافی',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'مالیات/زکات',
|
||||
'mobile.app.launchpad': 'سکوی پرتاب',
|
||||
@@ -3836,6 +3839,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'کوردمدیا',
|
||||
'mobile.app.events': 'رویدادها',
|
||||
'mobile.app.help': 'کمک',
|
||||
'mobile.app.loto': 'لاتاری',
|
||||
'mobile.app.music': 'موسیقی',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.referral': 'ارجاع',
|
||||
@@ -3972,4 +3976,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': 'ساختهشده توسط مردم دیجیتال کرد برای هر ملت و مردم بیدولت',
|
||||
};
|
||||
|
||||
@@ -1573,6 +1573,7 @@ export default {
|
||||
'p2pNav.orders': 'Ferman',
|
||||
'p2pNav.ads': 'Reklam',
|
||||
'p2pNav.messages': 'Peyam',
|
||||
'p2pNav.buyVisa': 'Bi Visa bikire',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'Peyam',
|
||||
@@ -1857,14 +1858,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 +2912,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î',
|
||||
@@ -3802,7 +3805,7 @@ export default {
|
||||
'mobile.app.bank': 'Bank',
|
||||
'mobile.app.exchange': 'Danûstandin',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'Bac/Zekat',
|
||||
'mobile.app.launchpad': 'Destpêk',
|
||||
@@ -3819,6 +3822,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'KurdMedya',
|
||||
'mobile.app.events': 'Çalakî',
|
||||
'mobile.app.help': 'Alîkarî',
|
||||
'mobile.app.loto': 'Loto',
|
||||
'mobile.app.music': 'Muzîk',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.rewshenbir': 'Rewşenbir',
|
||||
|
||||
@@ -1567,6 +1567,7 @@ export default {
|
||||
'p2pNav.orders': 'Siparişler',
|
||||
'p2pNav.ads': 'İlanlar',
|
||||
'p2pNav.messages': 'Mesajlar',
|
||||
'p2pNav.buyVisa': 'Visa ile Al',
|
||||
|
||||
// P2P Messages Inbox
|
||||
'p2pMessages.title': 'Mesajlar',
|
||||
@@ -1851,14 +1852,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 +2915,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ı',
|
||||
@@ -3805,7 +3808,7 @@ export default {
|
||||
'mobile.app.bank': 'Banka',
|
||||
'mobile.app.exchange': 'Borsa',
|
||||
'mobile.app.dex': 'Pez-DEX',
|
||||
'mobile.app.p2p': 'P2P',
|
||||
'mobile.app.p2p': 'P2P/Buy-Sell',
|
||||
'mobile.app.b2b': 'B2B',
|
||||
'mobile.app.bacZekat': 'Vergi/Zekat',
|
||||
'mobile.app.launchpad': 'Launchpad',
|
||||
@@ -3822,6 +3825,7 @@ export default {
|
||||
'mobile.app.kurdMedia': 'KurdMedya',
|
||||
'mobile.app.events': 'Etkinlikler',
|
||||
'mobile.app.help': 'Yardım',
|
||||
'mobile.app.loto': 'Loto',
|
||||
'mobile.app.music': 'Müzik',
|
||||
'mobile.app.vpn': 'VPN',
|
||||
'mobile.app.rewshenbir': 'Rewshenbir',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,6 +9,7 @@ interface MediaChannel {
|
||||
descriptionKu: string;
|
||||
description: string;
|
||||
color: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface SocialPlatform {
|
||||
@@ -21,7 +22,7 @@ interface SocialPlatform {
|
||||
|
||||
const MEDIA_CHANNELS: MediaChannel[] = [
|
||||
{ id: 'dkstv', nameKu: 'DKS TV', name: 'DKS TV', icon: '📺', descriptionKu: 'Televizyona Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Television', color: '#E53935' },
|
||||
{ id: 'dksgzt', nameKu: 'DKS Rojname', name: 'DKS Gazette', icon: '📰', descriptionKu: 'Nûçe û Daxuyaniyên Fermî', description: 'Official News & Announcements', color: '#1E88E5' },
|
||||
{ id: 'dksgzt', nameKu: 'DKS Rojname', name: 'DKS Gazette', icon: '📰', descriptionKu: 'Nûçe û Daxuyaniyên Fermî', description: 'Official News & Announcements', color: '#1E88E5', url: 'https://news.pex.mom' },
|
||||
{ id: 'dksradio', nameKu: 'DKS Radyo', name: 'DKS Radio', icon: '📻', descriptionKu: 'Radyoya Dewleta Dijîtal a Kurdistanê', description: 'Digital Kurdistan State Radio', color: '#7B1FA2' },
|
||||
{ id: 'dksmusic', nameKu: 'DKS Muzîk', name: 'DKS Music', icon: '🎵', descriptionKu: 'Weşana Muzîka Kurdî', description: 'Kurdish Music Streaming', color: '#00897B' },
|
||||
{ id: 'dkspodcast',nameKu: 'DKS Podcast', name: 'DKS Podcast', icon: '🎙️', descriptionKu: 'Podcast û Gotûbêjên Kurdî', description: 'Kurdish Podcasts & Talks', color: '#F4511E' },
|
||||
@@ -71,20 +72,38 @@ export default function KurdMediaPage() {
|
||||
<p className="text-sm text-gray-300 mb-1">{t('kurdMedia.channels.desc', 'Weşanên fermî yên Dewleta Dijîtal a Kurdistanê.')}</p>
|
||||
<p className="text-xs text-gray-500 mb-4">{t('kurdMedia.channels.descEn', 'Official broadcasts of Digital Kurdistan State. TV, radio, news and more.')}</p>
|
||||
<div className="space-y-3">
|
||||
{MEDIA_CHANNELS.map(ch => (
|
||||
<div key={ch.id} className="flex items-center gap-3 bg-gray-800 rounded-xl p-3">
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: ch.color }}>
|
||||
{ch.icon}
|
||||
{MEDIA_CHANNELS.map(ch => {
|
||||
const inner = (
|
||||
<>
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: ch.color }}>
|
||||
{ch.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-white text-sm">{ch.nameKu}</p>
|
||||
<p className="text-xs text-gray-400 truncate">{ch.descriptionKu}</p>
|
||||
</div>
|
||||
{ch.url ? (
|
||||
<span className="text-[10px] font-bold text-green-400 bg-green-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
||||
{t('kurdMedia.open', 'Open')} ↗
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] font-bold text-yellow-400 bg-yellow-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
||||
{t('kurdMedia.soon', 'Soon')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return ch.url ? (
|
||||
<a key={ch.id} href={ch.url} target="_blank" rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 bg-gray-800 rounded-xl p-3 hover:bg-gray-700 transition-colors">
|
||||
{inner}
|
||||
</a>
|
||||
) : (
|
||||
<div key={ch.id} className="flex items-center gap-3 bg-gray-800 rounded-xl p-3">
|
||||
{inner}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold text-white text-sm">{ch.nameKu}</p>
|
||||
<p className="text-xs text-gray-400 truncate">{ch.descriptionKu}</p>
|
||||
</div>
|
||||
<span className="text-[10px] font-bold text-yellow-400 bg-yellow-400/10 px-2 py-1 rounded-full flex-shrink-0">
|
||||
{t('kurdMedia.soon', 'Soon')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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